{connected ? 'VPN включён' : 'VPN выключен'}
{canStart ? 'Нажмите, чтобы включить' : 'Добавьте ссылку и выберите сервер'}
)}{error}
}import React, { useEffect, useRef, useState } from 'react'; import { api } from '../api.js'; import { connectionAction, copyText, formatConnectionDuration, localProxyUrls, subscriptionDomain, subscriptionDaysLeft, subscriptionUsage, } from '../utils/clientControls.js'; import { formatBytes } from '../utils/format.js'; export function ClientOverviewPage({ state, busy, error, subscriptionUrl, setSubscriptionUrl, servers, pendingTag, setPendingTag, onFetchSubscription, onRefreshSubscriptionInfo, onForgetSubscription, onApply, onRestart, onStop, }) { const isGateway = state?.mode === 'gateway'; const connected = Boolean(state?.singboxRunning); const hasSubscription = Boolean(state?.hasSubscription); const selectedTag = pendingTag || state?.selectedTag || ''; const showPower = hasSubscription && Boolean(selectedTag); const canStart = Boolean(selectedTag || state?.configExists); const [now, setNow] = useState(Date.now()); const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription); const [pings, setPings] = useState({}); const [accessTab, setAccessTab] = useState('gateway'); const [copyFeedback, setCopyFeedback] = useState(null); const [refreshingInfo, setRefreshingInfo] = useState(false); const [usageUpdated, setUsageUpdated] = useState(false); const [serverRevealVersion, setServerRevealVersion] = useState(0); const [serversLeaving, setServersLeaving] = useState(false); const subscriptionInputRef = useRef(null); const subscriptionRef = useRef(null); const copyTimerRef = useRef(null); const serverKey = servers.map((server) => `${server.tag}:${server.server}:${server.server_port}`).join('|'); const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1'; const proxyUrls = localProxyUrls(state?.proxyPort, gatewayAddress); const usage = subscriptionUsage(state?.userInfo); const [displayedUsed, setDisplayedUsed] = useState(usage.used); const hasUsage = Boolean( state?.userInfo && ['upload', 'download', 'total', 'expire'].some((key) => key in state.userInfo), ); useEffect(() => { setNow(Date.now()); if (!connected || !state?.singboxStartedAt) return undefined; const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, [connected, state?.singboxStartedAt]); useEffect(() => { if (!servers.length) { setPings({}); return undefined; } let cancelled = false; setPings(Object.fromEntries(servers.map((server) => [server.tag, { checking: true }]))); api.servers.pingAll() .then((data) => { if (cancelled) return; setPings(Object.fromEntries((data.results || []).map((ping) => [ String(ping.tag || '').trim(), ping, ]))); }) .catch(() => { if (!cancelled) { setPings(Object.fromEntries(servers.map((server) => [server.tag, { ok: false }]))); } }); return () => { cancelled = true; }; }, [serverKey]); useEffect(() => { if (editingSubscription) subscriptionInputRef.current?.focus(); }, [editingSubscription]); useEffect(() => { if (!hasSubscription) setEditingSubscription(true); }, [hasSubscription]); useEffect(() => { if (!editingSubscription || !state?.hasSubscription || subscriptionUrl) return undefined; const timer = setTimeout(() => setEditingSubscription(false), 5000); return () => clearTimeout(timer); }, [editingSubscription, state?.hasSubscription, subscriptionUrl]); useEffect(() => { if (!editingSubscription || !hasSubscription) return undefined; const closeOnOutsideClick = (event) => { if (subscriptionRef.current?.contains(event.target)) return; setSubscriptionUrl(''); setEditingSubscription(false); }; document.addEventListener('pointerdown', closeOnOutsideClick); return () => document.removeEventListener('pointerdown', closeOnOutsideClick); }, [editingSubscription, hasSubscription, setSubscriptionUrl]); useEffect(() => { if (!state?.hasSubscription) return undefined; const refresh = () => onRefreshSubscriptionInfo().catch(() => {}); refresh(); const timer = setInterval(refresh, 60_000); return () => clearInterval(timer); }, [state?.hasSubscription]); useEffect(() => { const from = displayedUsed; const to = usage.used; if (from === to) return undefined; const startedAt = performance.now(); let frame; const tick = (now) => { const progress = Math.min(1, (now - startedAt) / 900); const eased = 1 - Math.pow(1 - progress, 4); setDisplayedUsed(from + (to - from) * eased); if (progress < 1) frame = requestAnimationFrame(tick); }; frame = requestAnimationFrame(tick); return () => cancelAnimationFrame(frame); }, [usage.used]); useEffect(() => () => clearTimeout(copyTimerRef.current), []); async function toggleConnection() { const action = connectionAction({ connected, selectedTag, configExists: state?.configExists }); if (action?.type === 'stop') return onStop(); if (action?.type === 'apply') return onApply(action.selectedTag); if (action?.type === 'restart') return onRestart(); } function selectServer(tag) { setPendingTag(tag); if (connected && tag) onApply(tag); } async function submitSubscription(event) { event.preventDefault(); if (!subscriptionUrl.trim()) return; await onFetchSubscription(); setSubscriptionUrl(''); setEditingSubscription(false); } async function copyProxy(kind) { const value = kind === 'gateway' ? gatewayAddress : proxyUrls[kind]; clearTimeout(copyTimerRef.current); setCopyFeedback({ kind, failed: false }); copyTimerRef.current = setTimeout(() => setCopyFeedback(null), 800); try { await copyText(value); } catch { setCopyFeedback({ kind, failed: true }); } } async function refreshInfo() { const startedAt = performance.now(); setRefreshingInfo(true); try { await onRefreshSubscriptionInfo(); setUsageUpdated(false); requestAnimationFrame(() => setUsageUpdated(true)); setTimeout(() => setUsageUpdated(false), 900); setServersLeaving(true); await new Promise((resolve) => setTimeout(resolve, 420 + Math.max(0, servers.length - 1) * 90)); setServerRevealVersion((version) => version + 1); setServersLeaving(false); } finally { const elapsed = performance.now() - startedAt; const completeCyclesAt = Math.max(900, Math.ceil(elapsed / 900) * 900); await new Promise((resolve) => setTimeout(resolve, completeCyclesAt - elapsed)); setRefreshingInfo(false); } } return (
{canStart ? 'Нажмите, чтобы включить' : 'Добавьте ссылку и выберите сервер'}
)}{error}
}