localization in header
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useMemo, useDeferredValue } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -11,21 +12,20 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { getInstalledApps, AppInfo } from "vpn-plugin";
|
||||
import { useSettingsStore } from "@/store/useSettingsStore";
|
||||
import { isAndroid } from "@/lib/platform";
|
||||
import { Loader2 } from "lucide-react"; // Для индикации загрузки
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export function AppExclusions() {
|
||||
const { t } = useTranslation();
|
||||
const [apps, setApps] = useState<AppInfo[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Оптимизация: точечные селекторы вместо деструктуризации всего стора
|
||||
const excludedApps = useSettingsStore((state) => state.excludedApps);
|
||||
const setExcludedApps = useSettingsStore((state) => state.setExcludedApps);
|
||||
|
||||
const deferredSearch = useDeferredValue(search);
|
||||
|
||||
// Загрузка приложений только при открытии и если список пуст
|
||||
useEffect(() => {
|
||||
if (isOpen && apps.length === 0) {
|
||||
setIsLoading(true);
|
||||
@@ -35,7 +35,6 @@ export function AppExclusions() {
|
||||
.catch(console.error)
|
||||
.finally(() => setIsLoading(false));
|
||||
} else {
|
||||
// Имитация для десктопа/разработки
|
||||
setTimeout(() => {
|
||||
setApps([
|
||||
{ appName: "Google Chrome", packageName: "com.android.chrome" },
|
||||
@@ -48,7 +47,6 @@ export function AppExclusions() {
|
||||
}
|
||||
}, [isOpen, apps.length]);
|
||||
|
||||
// Оптимизированная фильтрация
|
||||
const filteredApps = useMemo(() => {
|
||||
const term = deferredSearch.toLowerCase();
|
||||
return apps.filter(
|
||||
@@ -73,7 +71,9 @@ export function AppExclusions() {
|
||||
variant="outline"
|
||||
className="w-full flex justify-between items-center h-12 rounded-xl bg-white/5 border-white/10 hover:bg-white/10 transition-all"
|
||||
>
|
||||
<span className="font-medium text-foreground">Excluded Apps</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{t("excluded_apps")}
|
||||
</span>
|
||||
<span className="bg-primary/20 text-primary px-2.5 py-0.5 rounded-full text-xs font-bold">
|
||||
{excludedApps.length}
|
||||
</span>
|
||||
@@ -82,13 +82,13 @@ export function AppExclusions() {
|
||||
|
||||
<DialogContent className="max-h-[85vh] flex flex-col p-0 overflow-hidden border-white/10 bg-background/95 backdrop-blur-2xl">
|
||||
<DialogHeader className="p-6 pb-2">
|
||||
<DialogTitle>Выберите приложения</DialogTitle>
|
||||
<DialogTitle>{t("select_apps")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 pb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Поиск по имени или пакету..."
|
||||
placeholder={t("search_apps_placeholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full p-3 rounded-xl bg-white/5 border border-white/10 text-sm outline-none focus:border-primary transition-colors text-foreground"
|
||||
@@ -99,9 +99,7 @@ export function AppExclusions() {
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 gap-3 text-muted-foreground">
|
||||
<Loader2 className="size-8 animate-spin text-primary" />
|
||||
<p className="text-sm font-medium">
|
||||
Получение списка приложений...
|
||||
</p>
|
||||
<p className="text-sm font-medium">{t("loading_apps")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -129,7 +127,7 @@ export function AppExclusions() {
|
||||
|
||||
{!isLoading && filteredApps.length === 0 && (
|
||||
<p className="text-center text-muted-foreground text-sm py-12">
|
||||
Ничего не найдено
|
||||
{t("no_apps_found")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Menu } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
import { LanguageToggle } from "./ToggleLanguge";
|
||||
|
||||
export function Header({ onMenuClick }: { onMenuClick: () => void }) {
|
||||
return (
|
||||
@@ -21,8 +22,9 @@ export function Header({ onMenuClick }: { onMenuClick: () => void }) {
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Справа: Тема (занимает место по контенту) */}
|
||||
<div className="flex justify-end">
|
||||
{/* Справа: Смена языка и темы */}
|
||||
<div className="flex justify-end gap-2">
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,18 @@ export function LanguageToggle() {
|
||||
const toggleLanguage = () => {
|
||||
const currentLang = i18n.language || "en";
|
||||
const newLang = currentLang.startsWith("en") ? "ru" : "en";
|
||||
|
||||
i18n.changeLanguage(newLang);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="ghost" onClick={toggleLanguage} className="min-w-10">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleLanguage}
|
||||
className="rounded-full transition-all duration-300 hover:scale-105 font-bold text-xs"
|
||||
>
|
||||
{i18n.language?.substring(0, 2).toUpperCase() || "EN"}
|
||||
<span className="sr-only">Toggle language</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user