killswitch and domain list, also aexcluded apps
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user