killswitch and domain list, also aexcluded apps

This commit is contained in:
2026-04-30 16:12:36 +07:00
parent d1f53ad84c
commit 6891e64c4f
17 changed files with 373 additions and 5692 deletions
+141
View File
@@ -0,0 +1,141 @@
import { useEffect, useState, useMemo, useDeferredValue } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
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"; // Для индикации загрузки
export function AppExclusions() {
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);
if (isAndroid) {
getInstalledApps()
.then(setApps)
.catch(console.error)
.finally(() => setIsLoading(false));
} else {
// Имитация для десктопа/разработки
setTimeout(() => {
setApps([
{ appName: "Google Chrome", packageName: "com.android.chrome" },
{ appName: "Telegram", packageName: "org.telegram.messenger" },
{ appName: "YouTube", packageName: "com.google.android.youtube" },
]);
setIsLoading(false);
}, 500);
}
}
}, [isOpen, apps.length]);
// Оптимизированная фильтрация
const filteredApps = useMemo(() => {
const term = deferredSearch.toLowerCase();
return apps.filter(
(app) =>
app.appName.toLowerCase().includes(term) ||
app.packageName.toLowerCase().includes(term),
);
}, [apps, deferredSearch]);
const toggleApp = (pkg: string) => {
if (excludedApps.includes(pkg)) {
setExcludedApps(excludedApps.filter((p) => p !== pkg));
} else {
setExcludedApps([...excludedApps, pkg]);
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
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="bg-primary/20 text-primary px-2.5 py-0.5 rounded-full text-xs font-bold">
{excludedApps.length}
</span>
</Button>
</DialogTrigger>
<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>
</DialogHeader>
<div className="px-6 pb-4">
<input
type="text"
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"
/>
</div>
<div className="flex-1 overflow-y-auto px-6 pb-6 custom-scrollbar">
{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>
</div>
) : (
<div className="space-y-2">
{filteredApps.map((app) => (
<div
key={app.packageName}
className="flex items-center justify-between p-3 rounded-xl bg-white/5 hover:bg-white/10 border border-transparent transition-all cursor-pointer group"
onClick={() => toggleApp(app.packageName)}
>
<div className="flex flex-col overflow-hidden pr-4">
<span className="text-sm font-medium truncate text-foreground group-hover:text-primary transition-colors">
{app.appName}
</span>
<span className="text-[10px] text-muted-foreground truncate font-mono">
{app.packageName}
</span>
</div>
<Switch
checked={excludedApps.includes(app.packageName)}
onCheckedChange={() => toggleApp(app.packageName)}
onClick={(e) => e.stopPropagation()}
/>
</div>
))}
{!isLoading && filteredApps.length === 0 && (
<p className="text-center text-muted-foreground text-sm py-12">
Ничего не найдено
</p>
)}
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}
+5
View File
@@ -8,10 +8,12 @@ import { useEffect } from "react";
import { listen } from "@tauri-apps/api/event";
import VpnControlButton from "@/components/shared/VpnControlButton";
import { useTheme } from "@/ThemeProvider";
import { useSettingsStore } from "@/store/useSettingsStore";
export function VpnControl() {
const { status, setStatus } = useVpnStore();
const { t } = useTranslation();
const { killSwitch, excludedApps, excludedDomains } = useSettingsStore();
// Подключаем тему
const { isDark } = useTheme();
@@ -27,6 +29,9 @@ export function VpnControl() {
prefix: 24,
dns: "10.0.0.2",
mtu: 1380,
killswitchEnabled: killSwitch,
excludedApps: excludedApps,
excludedDomains: excludedDomains,
};
await startVpn(config, "147.45.43.70", 443);
} else {