import { useEffect, useMemo, useRef, useState, type CSSProperties, } from "react"; import { open } from "@tauri-apps/plugin-dialog"; import { Cpu, FileCode2, FolderOpen, Gauge, Link2, ShieldAlert, Trash2, Wand2, } from "lucide-react"; import { applyConfiguration, configureProxiFyreFirewallRules, fetchSingBoxSubscription, forgetSingBoxSubscription, generateSingBoxConfig, getComponents, getProxiFyreSetupProgress, getProxiFyreSetupStatus, getSavedState, getSingBoxSetupStatus, getSingBoxStatus, getStartupSnapshot, installProxiFyre, installSingBox, pingAllSingBoxServers, pingProxyTarget, pingSingBoxServer, restartAsAdmin, saveSingBoxSubscription, selectSingBoxServer, startProxiFyreService, startSingBoxService, stopProxiFyreService, stopSingBoxService, uninstallProxiFyre, uninstallSingBox, type AdminStatusResponse, type LocalSingBoxStatusResponse, type PingServerResponse, type ProxyTargetCheckResponse, type ProxiFyreSetupProgress, type ProxiFyreSetupStatus, type SingBoxSetupStatus, } from "../api/tauriCommands"; import type { ComponentStatus, Profile, SubscriptionServer, Target, } from "../domain/types"; import { BusyRing, Button, DetailsPopover, IconButton, LogDock, ServiceControlRow, Tabs, } from "../ui"; import { ProxiFyreSetupStrip } from "./components/ProxiFyreSetupStrip"; import { SummaryStatusControl } from "./components/SummaryStatusControl"; import { ConnectionCheckPanel } from "./components/ConnectionCheckPanel"; import { AppList } from "./components/AppList"; import { useNoticeLog } from "./hooks/useNoticeLog"; import { parseProxy, type ParsedProxy } from "./lib/parseProxy"; import { normalizeItemValue, type DraftItemType } from "./lib/profileItems"; import { configChangeRows, configSnapshotFromUi, displayServerTag, type ConfigSnapshot, type RouteMode, } from "./lib/snapshots"; import { getApplyReadiness } from "./readiness"; import { serviceControlState, systemSummaryState, connectionCheckView, summaryRouteFlow, summaryRouteChainSegments, routeChainSegments, safeProxyError, pingTone, proxyCheckNoticeKind, proxyCheckNoticeTitle, proxyCheckText, changesApplyButtonLabel, routeProxyCheckTarget, targetForUi, targetForExternalProxy, itemsForProfiles, formatProxy, profileItemInput, emptyItemMessage, localSetupProgress, proxyfierTitle, proxyfierDetails, singBoxDetails, singBoxDetailLines, componentDetails, noticeFromConfigurationApply, upsertComponent, sameValue, formatLogTime, serverTooltip, pingSummary, errorMessage, type DraftItem, type RouteChainInput, } from "./viewModel"; type ProxiFyreAction = "start" | "stop" | "restart" | "install" | "uninstall"; type SingBoxAction = | "start" | "stop" | "install" | "uninstall" | "fetch" | "forget" | "generate" | "ping"; type ServiceVisualState = "active" | "settling" | null; type PanelId = "summary" | "proxifyre" | "proxy"; type TabTransitionDirection = "left" | "right"; const MAIN_TARGET_ID = "main-proxy"; const MAIN_PROFILE_ID = "main-profile"; const LOCAL_SINGBOX_TARGET_ID = "local-singbox"; const PANEL_ORDER: PanelId[] = ["proxifyre", "summary", "proxy"]; const SHOW_DEV_SUBSCRIPTION_IDENTITY = import.meta.env.DEV; const fallbackComponents: ComponentStatus[] = [ { id: "proxyfier", name: "ProxiFyre", state: "missing", installed: false, running: false, problems: ["ProxiFyre не найден"], actions: [], }, { id: "singbox", name: "Локальный sing-box", state: "missing", installed: false, running: false, problems: [], actions: [], }, ]; export function App() { const [activePanel, setActivePanel] = useState("summary"); const [tabTransitionDirection, setTabTransitionDirection] = useState("right"); const [proxyInput, setProxyInput] = useState(""); const [routeMode, setRouteMode] = useState("external"); const [profileId, setProfileId] = useState(MAIN_PROFILE_ID); const [targetId, setTargetId] = useState(MAIN_TARGET_ID); const [items, setItems] = useState([]); const [appliedSnapshot, setAppliedSnapshot] = useState( null, ); const [isProcessInputOpen, setIsProcessInputOpen] = useState(false); const [processInput, setProcessInput] = useState(""); const [pickerAction, setPickerAction] = useState<"exe" | "folder" | null>( null, ); const [components, setComponents] = useState(fallbackComponents); const [setupStatus, setSetupStatus] = useState( null, ); const [setupProgress, setSetupProgress] = useState(null); const [singBoxStatus, setSingBoxStatus] = useState(null); const [singBoxSetupStatus, setSingBoxSetupStatus] = useState(null); const [subscriptionInput, setSubscriptionInput] = useState(""); const [serverPings, setServerPings] = useState< Record >({}); const [proxyCheck, setProxyCheck] = useState( null, ); const [adminStatus, setAdminStatus] = useState( null, ); const [, setGeneratedConfigPath] = useState(""); const { entries: logEntries, activeEntry: activeLog, open: isLogOpen, showNotice, toggle: toggleLog, } = useNoticeLog(); const [isLoading, setIsLoading] = useState(true); const [isDetectingComponents, setIsDetectingComponents] = useState(true); const [isApplying, setIsApplying] = useState(false); const [isRestartingAsAdmin, setIsRestartingAsAdmin] = useState(false); const [isProxyChecking, setIsProxyChecking] = useState(false); const [serverPingTag, setServerPingTag] = useState(null); const [serviceAction, setServiceAction] = useState( null, ); const [singBoxAction, setSingBoxAction] = useState( null, ); const [isServiceMenuOpen, setIsServiceMenuOpen] = useState(false); const [isSingBoxMenuOpen, setIsSingBoxMenuOpen] = useState(false); const [serviceVisualState, setServiceVisualState] = useState(null); const serviceVisualTimerRef = useRef(null); const proxyfier = useMemo( () => components.find((component) => component.id === "proxyfier"), [components], ); const singbox = useMemo( () => singBoxStatus?.component ?? components.find((component) => component.id === "singbox"), [components, singBoxStatus], ); const isSingBoxInstalled = Boolean(singbox?.installed); const selectedServerTag = singBoxStatus?.config.selectedServerTag; const selectedServerId = singBoxStatus?.config.selectedServerId; const selectedServer = useMemo( () => singBoxStatus?.cache?.servers.find((server) => selectedServerId ? server.id === selectedServerId : server.tag === selectedServerTag, ) ?? null, [selectedServerId, selectedServerTag, singBoxStatus], ); const currentSnapshot = useMemo( () => configSnapshotFromUi( routeMode, proxyInput, items, selectedServerId, selectedServerTag, ), [items, proxyInput, routeMode, selectedServerId, selectedServerTag], ); const pendingChanges = useMemo( () => appliedSnapshot ? configChangeRows(appliedSnapshot, currentSnapshot) : [], [appliedSnapshot, currentSnapshot], ); const hasUnappliedChanges = pendingChanges.length > 0; const hasAdminPrompt = Boolean(adminStatus?.canRestartElevated); const shellStyle = hasUnappliedChanges ? ({ "--change-row-count": String(pendingChanges.length) } as CSSProperties) : undefined; const systemSummary = systemSummaryState({ isLoading, isDetectingComponents, proxyfier, singbox, routeMode: appliedSnapshot?.routeMode ?? routeMode, singBoxStatus, proxyCheck, }); useEffect(() => { void refresh(); }, []); useEffect(() => { return () => { if (serviceVisualTimerRef.current !== null) { window.clearTimeout(serviceVisualTimerRef.current); } }; }, []); useEffect(() => { if (serviceAction !== "install" && serviceAction !== "uninstall") return undefined; let cancelled = false; const pollProgress = async () => { try { const progress = await getProxiFyreSetupProgress(); if (!cancelled) setSetupProgress(progress); } catch { // Progress is best-effort; the main install/uninstall action still reports the real error. } }; void pollProgress(); const timer = window.setInterval(() => void pollProgress(), 650); return () => { cancelled = true; window.clearInterval(timer); }; }, [serviceAction]); async function refresh() { setIsLoading(true); setIsDetectingComponents(true); try { const [snapshot, progress] = await Promise.all([ getStartupSnapshot(), getProxiFyreSetupProgress(), ]); setAdminStatus(snapshot.adminStatus); setComponents(snapshot.components); setSetupStatus(snapshot.proxifyreSetupStatus); setSetupProgress(progress); setSingBoxStatus(snapshot.singboxStatus); setSingBoxSetupStatus(snapshot.singboxSetupStatus); applySavedState( snapshot.savedState.profiles, snapshot.savedState.targets, snapshot.savedState.generatedConfigPath, snapshot.singboxStatus, ); } catch { showNotice({ kind: "info", title: "Режим предпросмотра", text: "Запусти приложение через Tauri, чтобы увидеть найденный ProxiFyre и применить конфиг.", }); } finally { setIsLoading(false); setIsDetectingComponents(false); } } async function restartApplicationAsAdmin() { setIsRestartingAsAdmin(true); try { await restartAsAdmin(); setIsRestartingAsAdmin(false); } catch (error) { setIsRestartingAsAdmin(false); showNotice({ kind: "error", title: "Перезапуск отменен", text: errorMessage(error), }); } } function applySavedState( profiles: Profile[], targets: Target[], generatedPath: string, singBoxStatusForSnapshot = singBoxStatus, ) { const activeProfiles = profiles.filter((profile) => profile.enabled); const mainProfile = profiles.find( (profile) => profile.id === MAIN_PROFILE_ID, ); const activeProfile = mainProfile ?? activeProfiles[0]; const activeTarget = targetForUi(targets, activeProfile); const externalTarget = targetForExternalProxy(targets); const editableProfiles = mainProfile ? [mainProfile] : activeProfiles; const savedProxyInput = externalTarget ? formatProxy(externalTarget) : ""; const savedItems = itemsForProfiles(editableProfiles); const savedRouteMode = activeTarget?.id === LOCAL_SINGBOX_TARGET_ID || activeProfile?.targetId === LOCAL_SINGBOX_TARGET_ID ? "local-singbox" : "external"; setProxyInput(savedProxyInput); setProxyCheck(null); setItems(savedItems); setProfileId(mainProfile?.id ?? MAIN_PROFILE_ID); setTargetId(externalTarget?.id ?? MAIN_TARGET_ID); setRouteMode(savedRouteMode); setGeneratedConfigPath(generatedPath); setAppliedSnapshot( configSnapshotFromUi( savedRouteMode, savedProxyInput, savedItems, singBoxStatusForSnapshot?.config.selectedServerId, singBoxStatusForSnapshot?.config.selectedServerTag, ), ); } function addItem(type: DraftItemType, rawValue: string) { const value = normalizeItemValue(rawValue, type); if (!value) { showNotice({ kind: "error", title: "Нечего добавить", text: emptyItemMessage(type), }); return false; } if ( items.some((item) => item.type === type && sameValue(item.value, value)) ) { showNotice({ kind: "info", title: "Уже добавлено", text: value, }); return false; } setItems((current) => [ ...current, { id: `${type}-${crypto.randomUUID()}`, type, value, }, ]); return true; } function addProcess() { if (addItem("process", processInput)) { setProcessInput(""); setIsProcessInputOpen(false); } } function removeItem(id: string) { setItems((current) => current.filter((item) => item.id !== id)); } function changeRouteMode(nextMode: RouteMode) { setRouteMode(nextMode); setProxyCheck(null); } function changeProxyInput(nextValue: string) { setProxyInput(nextValue); setProxyCheck(null); } function switchPanel(nextPanel: PanelId) { if (activePanel === nextPanel) return; const currentIndex = PANEL_ORDER.indexOf(activePanel); const nextIndex = PANEL_ORDER.indexOf(nextPanel); setTabTransitionDirection(nextIndex > currentIndex ? "right" : "left"); setActivePanel(nextPanel); } async function pickAndAddItem( type: Extract, ) { setPickerAction(type); try { const selectedPath = await pickPath(type); if (selectedPath) { addItem(type, selectedPath); } } catch (error) { showNotice({ kind: "error", title: type === "exe" ? "EXE не выбран" : "Папка не выбрана", text: errorMessage(error), }); } finally { setPickerAction(null); } } async function updateConfig() { let parsedProxy: ParsedProxy | null = null; try { if (!items.length) throw new Error("Добавь хотя бы один процесс, EXE-файл или папку."); if (routeMode === "external") { parsedProxy = parseProxy(proxyInput); } else if (!isSingBoxInstalled) { throw new Error("Сначала установи Local sing-box."); } else if (!singBoxStatus?.config.selectedServerTag) { throw new Error("Выбери сервер Local sing-box."); } } catch (error) { showNotice({ kind: "error", title: "Проверь данные", text: errorMessage(error), }); return; } setIsApplying(true); try { if (routeMode === "external" && !parsedProxy) throw new Error("Прокси не разобран."); const result = await applyConfiguration({ routeMode, profile: { id: profileId, name: "Приложения через прокси", enabled: true, targetId: routeMode === "local-singbox" ? LOCAL_SINGBOX_TARGET_ID : targetId, protocols: ["TCP", "UDP"], items: items.map(profileItemInput), }, externalTarget: parsedProxy ? { id: targetId, name: "Основной прокси", kind: "external", protocol: parsedProxy.protocol, host: parsedProxy.host, port: parsedProxy.port, } : undefined, disableOtherProfiles: true, }); const [ saved, detectedComponents, detectedSetupStatus, detectedSingBoxStatus, detectedSingBoxSetupStatus, ] = await Promise.all([ getSavedState(), getComponents(), getProxiFyreSetupStatus(), getSingBoxStatus(), getSingBoxSetupStatus(), ]); applySavedState( saved.profiles, saved.targets, result.generatedConfigPath, ); setComponents(detectedComponents); setSetupStatus(detectedSetupStatus); setSingBoxStatus(detectedSingBoxStatus); setSingBoxSetupStatus(detectedSingBoxSetupStatus); showNotice(noticeFromConfigurationApply(result)); } catch (error) { showNotice({ kind: "error", title: "Конфиг не обновлен", text: errorMessage(error), }); } finally { setIsApplying(false); } } async function setProxiFyreServiceRunning(shouldRun: boolean) { const action = shouldRun ? "start" : "stop"; setServiceAction(action); setIsServiceMenuOpen(false); startServiceVisual(); try { await nextFrame(); const component = shouldRun ? await startProxiFyreService() : await stopProxiFyreService(); setComponents((current) => upsertComponent(current, component)); showNotice({ kind: "success", title: shouldRun ? "Служба запущена" : "Служба остановлена", text: proxyfierDetails(component, false), }); } catch (error) { showNotice({ kind: "error", title: shouldRun ? "Служба не запущена" : "Служба не остановлена", text: errorMessage(error), }); } finally { setServiceAction(null); settleServiceVisual(); } } async function installProxiFyrePackage() { const confirmed = window.confirm( "Установить ProxiFyre? ProxyWarden запросит права администратора, установит ProxiFyre, Windows Packet Filter и при необходимости Visual C++ Runtime, затем создаст и запустит Windows-службу. После установки приложение отдельно предложит добавить правила Windows Firewall.", ); if (!confirmed) return; setServiceAction("install"); setIsServiceMenuOpen(false); setSetupProgress( localSetupProgress( "install", "packet-filter", 1, "Готовлю установку сетевого драйвера.", ), ); startServiceVisual(); try { await nextFrame(); const component = await installProxiFyre(); const [detectedSetupStatus, detectedProgress] = await Promise.all([ getProxiFyreSetupStatus(), getProxiFyreSetupProgress(), ]); setComponents((current) => upsertComponent(current, component)); setSetupStatus(detectedSetupStatus); setSetupProgress(detectedProgress); showNotice({ kind: "success", title: "ProxiFyre установлен", text: proxyfierDetails(component, false), }); const firewallConfirmed = window.confirm( "Добавить разрешающие правила Windows Firewall для установленного ProxiFyre.exe? Будут созданы отдельные входящее и исходящее правила только для подтвержденного пути программы. Windows снова запросит права администратора.", ); if (firewallConfirmed) { try { await configureProxiFyreFirewallRules(); const firewallProgress = await getProxiFyreSetupProgress(); setSetupProgress(firewallProgress); showNotice({ kind: "success", title: "Windows Firewall настроен", text: "Входящее и исходящее правила добавлены для установленного ProxiFyre.exe.", }); } catch (error) { showNotice({ kind: "error", title: "Windows Firewall не настроен", text: `ProxiFyre установлен, но правила не добавлены. ${errorMessage(error)}`, }); } } else { showNotice({ kind: "info", title: "Правила Firewall пропущены", text: "ProxiFyre установлен без правил Windows Firewall. Их можно будет добавить повторной установкой компонента.", }); } } catch (error) { void getProxiFyreSetupProgress() .then(setSetupProgress) .catch(() => undefined); showNotice({ kind: "error", title: "ProxiFyre не установлен", text: errorMessage(error), }); } finally { setServiceAction(null); settleServiceVisual(); } } async function uninstallProxiFyrePackage() { const confirmed = window.confirm( "Удалить ProxiFyre и Windows Packet Filter с компьютера? Это остановит службу, удалит папку ProxiFyre и сетевой драйвер. Другие программы WireSock могут перестать работать до повторной установки драйвера.", ); if (!confirmed) return; setServiceAction("uninstall"); setIsServiceMenuOpen(false); setSetupProgress( localSetupProgress( "uninstall", "proxifyre", 1, "Готовлю удаление ProxiFyre и сетевого драйвера.", ), ); startServiceVisual(); try { await nextFrame(); const component = await uninstallProxiFyre(); const [detectedSetupStatus, detectedProgress] = await Promise.all([ getProxiFyreSetupStatus(), getProxiFyreSetupProgress(), ]); setComponents((current) => upsertComponent(current, component)); setSetupStatus(detectedSetupStatus); setSetupProgress(detectedProgress); showNotice({ kind: "success", title: "ProxiFyre удален", text: "Служба, папка установки ProxiFyre и Windows Packet Filter удалены.", }); } catch (error) { void getProxiFyreSetupProgress() .then(setSetupProgress) .catch(() => undefined); showNotice({ kind: "error", title: "ProxiFyre не удален", text: errorMessage(error), }); } finally { setServiceAction(null); settleServiceVisual(); } } async function refreshSingBoxState() { const [ detectedSingBoxStatus, detectedSingBoxSetupStatus, detectedComponents, ] = await Promise.all([ getSingBoxStatus(), getSingBoxSetupStatus(), getComponents(), ]); setSingBoxStatus(detectedSingBoxStatus); setSingBoxSetupStatus(detectedSingBoxSetupStatus); setComponents(detectedComponents); return detectedSingBoxStatus; } async function setSingBoxServiceRunning(shouldRun: boolean) { const action: SingBoxAction = shouldRun ? "start" : "stop"; setSingBoxAction(action); setIsSingBoxMenuOpen(false); try { await nextFrame(); const component = shouldRun ? await startSingBoxService() : await stopSingBoxService(); setComponents((current) => upsertComponent(current, component)); await refreshSingBoxState(); setProxyCheck(null); showNotice({ kind: "success", title: shouldRun ? "sing-box запущен" : "sing-box остановлен", text: componentDetails(component, false), }); } catch (error) { showNotice({ kind: "error", title: shouldRun ? "sing-box не запущен" : "sing-box не остановлен", text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function installSingBoxPackage() { const confirmed = window.confirm( "Установить Local sing-box? ProxyWarden запросит права администратора, скачает sing-box и WinSW, затем создаст и запустит Windows-службу ProxyWardenSingBox.", ); if (!confirmed) return; setSingBoxAction("install"); setIsSingBoxMenuOpen(false); try { await nextFrame(); const component = await installSingBox(); setComponents((current) => upsertComponent(current, component)); await refreshSingBoxState(); setProxyCheck(null); showNotice({ kind: "success", title: "Local sing-box установлен", text: componentDetails(component, false), }); } catch (error) { showNotice({ kind: "error", title: "Local sing-box не установлен", text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function uninstallSingBoxPackage() { const confirmed = window.confirm( "Удалить Local sing-box с компьютера? Будет удалена служба и папка установки sing-box.", ); if (!confirmed) return; setSingBoxAction("uninstall"); setIsSingBoxMenuOpen(false); try { await nextFrame(); const component = await uninstallSingBox(); setComponents((current) => upsertComponent(current, component)); await refreshSingBoxState(); setProxyCheck(null); showNotice({ kind: "success", title: "Local sing-box удален", text: "Служба и папка установки Local sing-box удалены.", }); } catch (error) { showNotice({ kind: "error", title: "Local sing-box не удален", text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function syncSingBoxSubscription() { const subscriptionUrl = subscriptionInput.trim(); if (!subscriptionUrl && !singBoxStatus?.config.hasSubscription) { showNotice({ kind: "error", title: "Ссылка не указана", text: "Вставь ссылку подписки Local sing-box.", }); return; } setSingBoxAction("fetch"); try { if (subscriptionUrl) { await saveSingBoxSubscription(subscriptionUrl); } const status = await fetchSingBoxSubscription(); setSingBoxStatus(status); setComponents((current) => upsertComponent(current, status.component)); setSubscriptionInput(""); setServerPings({}); setProxyCheck(null); showNotice({ kind: "success", title: "Подписка обновлена", text: `Серверов: ${status.cache?.servers.length ?? 0}`, }); } catch (error) { showNotice({ kind: "error", title: "Подписка не обновлена", text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function forgetSingBoxSubscriptionData() { setSingBoxAction("forget"); setIsSingBoxMenuOpen(false); try { const status = await forgetSingBoxSubscription(); setSingBoxStatus(status); setComponents((current) => upsertComponent(current, status.component)); setServerPings({}); setProxyCheck(null); showNotice({ kind: "info", title: "Подписка очищена", text: "Ссылка, cache и выбранный сервер Local sing-box удалены.", }); } catch (error) { showNotice({ kind: "error", title: "Подписка не очищена", text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function chooseSingBoxServer(server: SubscriptionServer) { try { const status = await selectSingBoxServer(server); setSingBoxStatus(status); setComponents((current) => upsertComponent(current, status.component)); setProxyCheck(null); } catch (error) { showNotice({ kind: "error", title: "Сервер не выбран", text: errorMessage(error), }); } } async function pingSingBoxServers() { setSingBoxAction("ping"); try { const results = await pingAllSingBoxServers(); setServerPings( Object.fromEntries(results.map((result) => [result.id, result])), ); showNotice({ kind: "info", title: "Ping завершен", text: pingSummary(results), }); } catch (error) { showNotice({ kind: "error", title: "Ping не выполнен", text: errorMessage(error), }); } finally { setSingBoxAction(null); } } async function pingSingleSingBoxServer(server: SubscriptionServer) { setServerPingTag(server.id); try { const result = await pingSingBoxServer(server); setServerPings((current) => ({ ...current, [result.id]: result, })); showNotice({ kind: result.ok ? "success" : "error", title: result.ok ? "Сервер отвечает" : "Сервер не ответил", text: serverTooltip(server, result), }); } catch (error) { showNotice({ kind: "error", title: "Ping не выполнен", text: errorMessage(error), }); } finally { setServerPingTag(null); } } async function checkRouteProxy() { let target: ParsedProxy; try { target = routeProxyCheckTarget(routeMode, proxyInput, singBoxStatus); } catch (error) { showNotice({ kind: "error", title: "Маршрут не проверен", text: errorMessage(error), }); return; } setProxyCheck(null); setIsProxyChecking(true); try { const result = await pingProxyTarget(target.host, target.port); setProxyCheck(result); showNotice({ kind: proxyCheckNoticeKind(result), title: proxyCheckNoticeTitle(result), text: proxyCheckText(result), }); } catch (error) { showNotice({ kind: "error", title: "Маршрут не проверен", text: errorMessage(error), }); } finally { setIsProxyChecking(false); } } async function generateSingBoxNow() { setSingBoxAction("generate"); try { const result = await generateSingBoxConfig(); await refreshSingBoxState(); showNotice({ kind: "success", title: "Конфиг sing-box создан", text: result.generatedConfigPath, }); } catch (error) { showNotice({ kind: "error", title: "Конфиг sing-box не создан", text: errorMessage(error), }); } finally { setSingBoxAction(null); } } function startServiceVisual() { if (serviceVisualTimerRef.current !== null) { window.clearTimeout(serviceVisualTimerRef.current); serviceVisualTimerRef.current = null; } setServiceVisualState("active"); } function settleServiceVisual() { if (serviceVisualTimerRef.current !== null) { window.clearTimeout(serviceVisualTimerRef.current); } setServiceVisualState("settling"); serviceVisualTimerRef.current = window.setTimeout(() => { setServiceVisualState(null); serviceVisualTimerRef.current = null; }, 700); } function renderAdminPrompt() { if (!hasAdminPrompt) return null; return ( ); } function renderTabs() { const tabs: Array<{ id: PanelId; label: string }> = [ { id: "proxifyre", label: "ProxiFyre" }, { id: "summary", label: "ProxyWarden" }, { id: "proxy", label: "VPN / Прокси" }, ]; return ( ); } function renderSummaryPanel() { return (

ProxyWarden

void setProxiFyreServiceRunning(running)} /> {renderRouteChain("vertical")}
); } function renderProxiFyreCard() { const state = serviceControlState(proxyfier, isDetectingComponents); const visualState = serviceVisualState === "active" ? "working" : serviceVisualState === "settling" ? "settling" : null; const packetFilterInstalled = Boolean( setupStatus?.items.some( (item) => item.id === "packet-filter" && item.installed, ), ); const canCleanupSetup = Boolean( proxyfier?.installed || packetFilterInstalled, ); const shouldInstallProxiFyre = !proxyfier?.installed || !proxyfier.serviceStatus; const primaryAction = shouldInstallProxiFyre ? { label: proxyfier?.installed ? "Переустановить" : "Установить", onClick: () => void installProxiFyrePackage(), variant: "primary" as const, loading: serviceAction === "install", loadingLabel: "Устанавливаю", disabled: isDetectingComponents || Boolean(serviceAction), } : { label: proxyfier.running ? "Остановить" : "Запустить", onClick: () => void setProxiFyreServiceRunning(!proxyfier.running), variant: proxyfier.running ? ("danger" as const) : ("neutral" as const), loading: serviceAction === "start" || serviceAction === "stop" || serviceAction === "restart", loadingLabel: serviceAction === "restart" ? "Перезапускаю" : serviceAction === "start" ? "Запускаю" : "Останавливаю", disabled: isDetectingComponents || Boolean(serviceAction), }; return ( void uninstallProxiFyrePackage(), }, ], } : undefined } /> ); } function renderProxiFyreSetupStrip() { return ( ); } function renderAppsSection() { return (

Приложения

{items.length}
{isProcessInputOpen ? (
setProcessInput(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") addProcess(); if (event.key === "Escape") { setProcessInput(""); setIsProcessInputOpen(false); } }} placeholder="Discord" spellCheck={false} autoFocus />
) : (
Новое приложение Добавь процесс, EXE-файл или папку
setIsProcessInputOpen(true)} label="Добавить процесс" icon={} /> void pickAndAddItem("exe")} disabled={Boolean(pickerAction)} loading={pickerAction === "exe"} label="Добавить EXE-файл" icon={} /> void pickAndAddItem("folder")} disabled={Boolean(pickerAction)} loading={pickerAction === "folder"} label="Добавить папку" icon={} />
)}
); } function renderChangesDock() { if (!hasUnappliedChanges || !appliedSnapshot) return null; const externalProxyError = routeMode === "external" ? safeProxyError(proxyInput) : null; const readiness = getApplyReadiness({ routeMode, appCount: items.length, proxiFyreInstalled: Boolean(proxyfier?.installed), singBoxInstalled: isSingBoxInstalled, singBoxRunning: Boolean(singbox?.running), selectedServerTag: singBoxStatus?.config.selectedServerTag, externalProxyValue: proxyInput, externalProxyError, busy: isApplying || Boolean(serviceAction) || Boolean(singBoxAction), }); const applyButtonText = isLoading || isDetectingComponents ? "Проверяю готовность" : readiness.ready ? changesApplyButtonLabel(isApplying, singBoxAction, serviceAction) : (readiness.title ?? "Применение недоступно"); return ( ); } function renderProxiFyrePanel() { return (
{renderProxiFyreCard()} {renderProxiFyreSetupStrip()} {renderAppsSection()}
); } function renderExternalProxyControls() { const proxyValidation = proxyInput.trim() ? safeProxyError(proxyInput) : null; return (
); } function renderSingBoxCard() { const state = serviceControlState(singbox, isDetectingComponents); const setupSummary = singBoxSetupStatus ? singBoxSetupStatus.ready ? "состав готов" : `не хватает: ${singBoxSetupStatus.missingCount}` : "состав не проверен"; const primaryAction = singbox?.installed ? { label: singbox.running ? "Остановить" : "Запустить", onClick: () => void setSingBoxServiceRunning(!singbox.running), variant: singbox.running ? ("danger" as const) : ("neutral" as const), loading: singBoxAction === "start" || singBoxAction === "stop", loadingLabel: singBoxAction === "start" ? "Запускаю" : "Останавливаю", disabled: isDetectingComponents || Boolean(singBoxAction), } : { label: "Установить", onClick: () => void installSingBoxPackage(), variant: "primary" as const, loading: singBoxAction === "install", loadingLabel: "Устанавливаю", disabled: isDetectingComponents || Boolean(singBoxAction), }; return ( void uninstallSingBoxPackage(), }, ], } : undefined } inlineActions={ } > {isSingBoxInstalled ? renderSingBoxWorkspace() : null} ); } function renderSingBoxWorkspace() { return (
setSubscriptionInput(event.target.value)} placeholder={ singBoxStatus?.config.subscriptionDisplayUrl ?? "https://example.com/sub" } spellCheck={false} /> void forgetSingBoxSubscriptionData()} disabled={ Boolean(singBoxAction) || !singBoxStatus?.config.hasSubscription } label="Очистить подписку Local sing-box" tooltip="Очистить" icon={} />

Загрузка обращается к указанному провайдеру подписки и передает случайный постоянный ID установки в заголовке X-HWID.

Серверы подписки
void pingSingBoxServers()} disabled={ Boolean(singBoxAction) || !singBoxStatus?.cache?.servers.length } loading={singBoxAction === "ping"} label="Проверить все серверы подписки" tooltip="Проверить все серверы" icon={} /> void generateSingBoxNow()} disabled={Boolean(singBoxAction) || !selectedServerTag} loading={singBoxAction === "generate"} label="Создать конфиг Local sing-box" tooltip="Создать конфиг" icon={} />
{singBoxStatus?.cache?.servers.length ? (
{singBoxStatus.cache.servers.map((server) => { const ping = serverPings[server.id]; const selected = selectedServerId ? server.id === selectedServerId : server.tag === selectedServerTag; return (
void pingSingleSingBoxServer(server)} disabled={Boolean(serverPingTag)} loading={serverPingTag === server.id} label={`Проверить ${displayServerTag(server.tag)}`} tooltip="Ping" icon={} />
); })}
) : (
Подписка Local sing-box еще не загружена.
)}
); } function renderProxyOverview() { const check = connectionCheckView({ routeMode, proxyInput, proxyCheck, singbox, singBoxStatus, selectedServer, isDetectingComponents, isProxyChecking, }); return (
{check.tone === "checking" ? : null} void checkRouteProxy()} />
); } function renderProxyPanel() { return (
Режим маршрута
{routeMode === "external" ? renderExternalProxyControls() : renderSingBoxCard()}
{renderProxyOverview()}
); } function renderRouteChain(variant: "horizontal" | "vertical" = "horizontal") { const input: RouteChainInput = { routeMode, proxyInput, proxyfier, singbox, singBoxStatus, selectedServer, appCount: items.length, isDetectingComponents, }; const flow = variant === "vertical" ? summaryRouteFlow(input) : "proxy"; const segments = variant === "vertical" ? summaryRouteChainSegments(input, flow) : routeChainSegments(input); const isVertical = variant === "vertical"; return (
{isVertical ? (
); } function renderActivePanel() { if (activePanel === "proxifyre") return renderProxiFyrePanel(); if (activePanel === "proxy") return renderProxyPanel(); return renderSummaryPanel(); } return (
{renderTabs()}
{renderActivePanel()}
{renderChangesDock()} {renderAdminPrompt()}
); } async function pickPath(type: Extract) { const selected = await open( type === "folder" ? { title: "Выбери папку", directory: true, multiple: false, } : { title: "Выбери EXE-файл", directory: false, multiple: false, filters: [{ name: "EXE-файлы", extensions: ["exe"] }], }, ); if (Array.isArray(selected)) return selected[0] ?? null; return selected; } function nextFrame() { return new Promise((resolve) => { window.requestAnimationFrame(() => resolve()); }); }