Refactor proxy routing and installer flows

This commit is contained in:
2026-07-08 11:05:52 +03:00
parent e745633d91
commit 88d5b94133
14 changed files with 1899 additions and 487 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { open } from '@tauri-apps/plugin-dialog';
import { Cpu, FileCode2, FolderOpen, Gauge, Info, Link2, Trash2, Wand2 } from 'lucide-react';
import { Cpu, FileCode2, FolderOpen, Gauge, Link2, Trash2, Wand2 } from 'lucide-react';
import {
applyProfiles,
fetchSingBoxSubscription,
@@ -34,7 +34,7 @@ import {
type SingBoxSetupStatus,
} from '../api/tauriCommands';
import type { ComponentStatus, Profile, ProfileItemInput, ProfileItemType, SubscriptionServer, Target } from '../domain/types';
import { Button, IconButton, LogDock, ServiceControlRow, Tabs } from '../ui';
import { Button, DetailsPopover, IconButton, LogDock, ServiceControlRow, Tabs } from '../ui';
import { getApplyReadiness } from './readiness';
import { serviceControlState } from './viewModel';
@@ -76,6 +76,48 @@ interface ConfigSnapshot {
items: ConfigSnapshotItem[];
}
interface ConnectionCheckView {
tone: StatusTone;
title: string;
text: string;
endpoint: string;
details: string[];
disabledReason?: string;
loading: boolean;
}
interface RouteChainSegment {
id: string;
label: string;
value: string;
tone: StatusTone;
details: string[];
}
interface ConnectionCheckInput {
routeMode: RouteMode;
proxyInput: string;
proxyPing: PingServerResponse | null;
singbox: ComponentStatus | undefined;
singBoxStatus: LocalSingBoxStatusResponse | null;
selectedServer: SubscriptionServer | null;
selectedServerPing: PingServerResponse | undefined;
isDetectingComponents: boolean;
isProxyChecking: boolean;
serverPingTag: string | null;
}
interface RouteChainInput {
routeMode: RouteMode;
proxyInput: string;
proxyfier: ComponentStatus | undefined;
singbox: ComponentStatus | undefined;
singBoxStatus: LocalSingBoxStatusResponse | null;
selectedServer: SubscriptionServer | null;
appCount: number;
isDetectingComponents: boolean;
}
const MAIN_TARGET_ID = 'main-proxy';
const MAIN_PROFILE_ID = 'main-profile';
const LOCAL_SINGBOX_TARGET_ID = 'local-singbox';
@@ -123,8 +165,6 @@ export function App() {
const [subscriptionInput, setSubscriptionInput] = useState('');
const [serverPings, setServerPings] = useState<Record<string, PingServerResponse>>({});
const [proxyPing, setProxyPing] = useState<PingServerResponse | null>(null);
const [isSingBoxSetupOpen, setIsSingBoxSetupOpen] = useState(false);
const [isSingBoxInfoOpen, setIsSingBoxInfoOpen] = useState(false);
const [generatedConfigPath, setGeneratedConfigPath] = useState('');
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
const [activeLogId, setActiveLogId] = useState<string | null>(null);
@@ -157,6 +197,11 @@ export function App() {
);
const isSingBoxInstalled = Boolean(singbox?.installed);
const selectedServerTag = singBoxStatus?.config.selectedServerTag;
const selectedServer = useMemo(
() => singBoxStatus?.cache?.servers.find((server) => server.tag === selectedServerTag) ?? null,
[selectedServerTag, singBoxStatus],
);
const selectedServerPing = selectedServerTag ? serverPings[selectedServerTag] : undefined;
const currentSnapshot = useMemo(
() => configSnapshotFromUi(routeMode, proxyInput, items, selectedServerTag),
[items, proxyInput, routeMode, selectedServerTag],
@@ -820,6 +865,19 @@ export function App() {
}
}
async function pingSelectedSingBoxServer() {
if (!selectedServer) {
showNotice({
kind: 'error',
title: 'Сервер не выбран',
text: 'Выбери сервер Local sing-box перед проверкой.',
});
return;
}
await pingSingleSingBoxServer(selectedServer);
}
async function generateSingBoxNow() {
setSingBoxAction('generate');
try {
@@ -1173,10 +1231,9 @@ export function App() {
externalProxyError,
busy: isApplying || Boolean(serviceAction) || Boolean(singBoxAction),
});
const showBlocker = !readiness.ready && !isLoading && !isDetectingComponents;
const disabledReason = !readiness.ready && readiness.title && readiness.text
? `${readiness.title}. ${readiness.text}`
: undefined;
const blockerAlreadyVisible = routeBlockerIsVisibleInProxyPanel(context, routeMode, readiness.title);
const showBlocker = !readiness.ready && !isLoading && !isDetectingComponents && !blockerAlreadyVisible;
const showPending = readiness.ready && hasUnappliedChanges;
return (
<>
@@ -1185,7 +1242,7 @@ export function App() {
<strong>{readiness.title}</strong>
<span>{readiness.text}</span>
</div>
) : showState && hasUnappliedChanges ? (
) : showState && showPending ? (
<div className="apply-state pending" role="status">
<strong>Изменения еще не применены в ProxiFyre</strong>
<span>{applyStateText(routeMode, isSingBoxInstalled, Boolean(singbox?.running))}</span>
@@ -1201,7 +1258,6 @@ export function App() {
disabled={!readiness.ready}
loading={isApplying}
loadingLabel={applyButtonLabel(context, true, hasUnappliedChanges, singBoxAction)}
title={disabledReason}
>
{applyButtonLabel(context, isApplying, hasUnappliedChanges, singBoxAction)}
</Button>
@@ -1250,30 +1306,60 @@ export function App() {
placeholder="socks5://127.0.0.1:1080"
spellCheck={false}
/>
{proxyValidation ? <span className="field-error">{proxyValidation}</span> : null}
</label>
<div className="proxy-check-line">
<div className={`proxy-check-state ${proxyPing ? pingTone(proxyPing) : proxyValidation ? 'warning' : 'muted'}`}>
<strong>{proxyPing ? pingResultTitle(proxyPing) : proxyValidation ? 'Проверь формат' : 'Проверка не запускалась'}</strong>
<span>{proxyPing ? pingResultText(proxyPing) : proxyValidation ?? 'TCP check покажет, доступен ли host:port.'}</span>
</div>
<Button
type="button"
variant="neutral"
size="lg"
onClick={() => void pingExternalProxy()}
disabled={Boolean(proxyValidation)}
loading={isProxyChecking}
loadingLabel="Проверяю"
>
Проверить
</Button>
</div>
);
}
function renderConnectionCheck(check: ConnectionCheckView) {
const buttonDisabled = Boolean(check.disabledReason);
return (
<div className="connection-check" aria-label="Проверка соединения">
<div className="connection-check-main">
<span>Проверка endpoint</span>
<strong>{check.title}</strong>
<p>{check.text}</p>
</div>
<DetailsPopover
className="connection-endpoint"
details={check.details}
popoverLabel="Проверка endpoint"
align="end"
aria-label={`Endpoint: ${check.endpoint}. ${check.details.join('. ')}`}
>
<span>Endpoint</span>
<strong>{check.endpoint}</strong>
</DetailsPopover>
<Button
type="button"
variant="neutral"
size="lg"
onClick={() => {
if (routeMode === 'external') {
void pingExternalProxy();
} else {
void pingSelectedSingBoxServer();
}
}}
disabled={buttonDisabled}
loading={check.loading}
loadingLabel="Проверяю"
>
Проверить
</Button>
</div>
);
}
function renderSingBoxCard() {
const state = serviceControlState(singbox, isDetectingComponents);
const setupSummary = singBoxSetupStatus
? singBoxSetupStatus.ready
? 'состав готов'
: `не хватает: ${singBoxSetupStatus.missingCount}`
: 'состав не проверен';
const primaryAction = singbox?.installed
? {
label: singbox.running ? 'Остановить' : 'Запустить',
@@ -1297,7 +1383,7 @@ export function App() {
state={state}
visualState={singBoxAction ? 'working' : null}
className="singbox-card"
title={singBoxTitle(singbox, isDetectingComponents)}
title="Local sing-box"
detail={singBoxDetails(singbox, singBoxStatus, isDetectingComponents)}
primaryAction={primaryAction}
menu={singbox?.installed ? {
@@ -1313,107 +1399,18 @@ export function App() {
}],
} : undefined}
inlineActions={(
<>
<Button
type="button"
variant="neutral"
size="sm"
className="setup-toggle"
onClick={() => setIsSingBoxSetupOpen((current) => !current)}
disabled={isDetectingComponents && !singBoxSetupStatus}
aria-expanded={isSingBoxSetupOpen}
>
{singbox?.installed ? 'Состав Local sing-box' : 'Что будет установлено'}
{singBoxSetupStatus ? (
<span>{singBoxSetupStatus.ready ? 'все есть' : `не хватает: ${singBoxSetupStatus.missingCount}`}</span>
) : null}
</Button>
<IconButton
type="button"
variant="neutral"
className="info-toggle"
onClick={() => setIsSingBoxInfoOpen((current) => !current)}
label="Подробности Local sing-box"
aria-expanded={isSingBoxInfoOpen}
title="Подробности"
icon={<Info size={16} strokeWidth={2} />}
/>
</>
<DetailsPopover
className="setup-summary"
details={singBoxDetailLines(singbox, singBoxStatus, singBoxSetupStatus, selectedServerTag)}
popoverLabel="Состав Local sing-box"
aria-label="Подробности Local sing-box"
>
<span>Детали</span>
<strong>{setupSummary}</strong>
</DetailsPopover>
)}
>
{isSingBoxInfoOpen ? (
<div className="singbox-info-popover">
<div className="singbox-info-grid">
<span>Локально</span>
<strong>{localSingBoxAddress(singBoxStatus)}</strong>
<span>LAN</span>
<strong>{lanSingBoxAddress(singBoxStatus) ?? 'недоступен'}</strong>
<span>Сервер</span>
<strong>
{selectedServerTag
? displayServerTag(selectedServerTag)
: 'не выбран'}
</strong>
<span>Файл</span>
<strong>{singbox?.path ?? 'не найден'}</strong>
<span>Конфиг</span>
<strong>{singBoxStatus?.generatedConfigPath ?? 'не создан'}</strong>
</div>
<div className="singbox-info-actions">
<Button
type="button"
onClick={() => void pingSingBoxServers()}
disabled={Boolean(singBoxAction) || !singBoxStatus?.cache?.servers.length}
variant="neutral"
size="sm"
>
<Gauge size={15} strokeWidth={1.9} />
Ping все
</Button>
<Button
type="button"
onClick={() => void generateSingBoxNow()}
disabled={Boolean(singBoxAction) || !selectedServerTag}
variant="neutral"
size="sm"
>
<Wand2 size={15} strokeWidth={1.9} />
Конфиг
</Button>
</div>
</div>
) : null}
{isSingBoxSetupOpen ? (
<div className="setup-details">
{singBoxSetupStatus ? (
singBoxSetupStatus.items.map((item) => (
<div className={`setup-item ${item.installed ? 'installed' : 'missing'}`} key={item.id}>
<span className="setup-state-dot" aria-hidden="true" />
<div>
<strong>{item.name}</strong>
<span>{setupItemDetails(item.installed, item.version, item.details)}</span>
</div>
</div>
))
) : (
<div className="setup-item">
<span className="setup-state-dot" aria-hidden="true" />
<div>
<strong>Проверяю состав</strong>
<span>Ищу sing-box, WinSW wrapper и службу.</span>
</div>
</div>
)}
</div>
) : null}
{isSingBoxInstalled ? renderSingBoxWorkspace() : (
<div className="singbox-install-note">
<strong>Local sing-box не установлен</strong>
<span>Установи компонент, чтобы подключить подписку, выбрать сервер и включить локальный маршрут.</span>
</div>
)}
{isSingBoxInstalled ? renderSingBoxWorkspace() : null}
</ServiceControlRow>
);
}
@@ -1446,11 +1443,37 @@ export function App() {
onClick={() => void forgetSingBoxSubscriptionData()}
disabled={Boolean(singBoxAction) || !singBoxStatus?.config.hasSubscription}
label="Очистить подписку Local sing-box"
title="Очистить"
tooltip="Очистить"
icon={<Trash2 size={18} strokeWidth={1.9} />}
/>
</div>
<div className="singbox-workspace-head">
<span>Серверы подписки</span>
<div className="singbox-workspace-actions">
<Button
type="button"
onClick={() => void pingSingBoxServers()}
disabled={Boolean(singBoxAction) || !singBoxStatus?.cache?.servers.length}
variant="neutral"
size="sm"
leftIcon={<Gauge size={15} strokeWidth={1.9} />}
>
Ping все
</Button>
<Button
type="button"
onClick={() => void generateSingBoxNow()}
disabled={Boolean(singBoxAction) || !selectedServerTag}
variant="neutral"
size="sm"
leftIcon={<Wand2 size={15} strokeWidth={1.9} />}
>
Конфиг
</Button>
</div>
</div>
{singBoxStatus?.cache?.servers.length ? (
<div className="server-list">
{singBoxStatus.cache.servers.map((server) => {
@@ -1462,7 +1485,8 @@ export function App() {
type="button"
className="server-select-button"
onClick={() => void chooseSingBoxServer(server)}
title={serverTooltip(server, ping)}
data-tooltip={serverTooltip(server, ping)}
aria-label={`Выбрать ${displayServerTag(server.tag)}. ${serverTooltip(server, ping)}`}
>
<span className="server-select-dot" aria-hidden="true" />
<strong>{displayServerTag(server.tag)}</strong>
@@ -1474,7 +1498,7 @@ export function App() {
disabled={Boolean(serverPingTag)}
loading={serverPingTag === server.tag}
label={`Проверить ${displayServerTag(server.tag)}`}
title="Ping"
tooltip="Ping"
icon={<Gauge size={14} strokeWidth={1.9} />}
/>
</div>
@@ -1488,6 +1512,34 @@ export function App() {
);
}
function renderProxyOverview() {
const check = connectionCheckView({
routeMode,
proxyInput,
proxyPing,
singbox,
singBoxStatus,
selectedServer,
selectedServerPing,
isDetectingComponents,
isProxyChecking,
serverPingTag,
});
return (
<section className={`proxy-overview ${check.tone}`} aria-labelledby="proxy-overview-title">
<div className="proxy-overview-head">
<div>
<span>Настройки VPN / Прокси</span>
<h2 id="proxy-overview-title">Маршрут и проверка соединения</h2>
</div>
<strong>{routeMode === 'local-singbox' ? 'Локальный прокси' : 'Внешний прокси'}</strong>
</div>
{renderConnectionCheck(check)}
</section>
);
}
function renderProxyPanel() {
return (
<section
@@ -1496,13 +1548,7 @@ export function App() {
id="panel-proxy"
aria-labelledby="tab-proxy"
>
<div className="panel-section-head">
<div>
<span>Настройки VPN / Прокси</span>
<h2>Маршрут и проверка соединения</h2>
</div>
<strong>{routeMode === 'local-singbox' ? 'Локальный прокси' : 'Внешний прокси'}</strong>
</div>
{renderProxyOverview()}
<section className="route-panel" aria-label="Маршрут приложений">
<div className="route-switch">
@@ -1527,16 +1573,46 @@ export function App() {
{routeMode === 'external' ? renderExternalProxyControls() : renderSingBoxCard()}
</section>
<div className="route-preview">
<span>Маршрут</span>
<strong>{routePreviewText(routeMode, proxyInput, singBoxStatus)}</strong>
</div>
{renderRouteChain()}
{renderApplyActions('proxy')}
</section>
);
}
function renderRouteChain() {
const segments = routeChainSegments({
routeMode,
proxyInput,
proxyfier,
singbox,
singBoxStatus,
selectedServer,
appCount: items.length,
isDetectingComponents,
});
return (
<section className="route-chain" aria-label="Текущий маршрут">
{segments.map((segment, index) => (
<DetailsPopover
className={`route-chain-segment ${segment.tone}`}
details={segment.details}
popoverLabel={segment.label}
align={index >= segments.length - 2 ? 'end' : 'start'}
aria-label={`${segment.label}: ${segment.value}. ${segment.details.join('. ')}`}
key={segment.id}
>
<span className="route-chain-dot" aria-hidden="true" />
<span>{segment.label}</span>
<strong>{segment.value}</strong>
{index < segments.length - 1 ? <span className="route-chain-arrow" aria-hidden="true">-&gt;</span> : null}
</DetailsPopover>
))}
</section>
);
}
function renderActivePanel() {
if (activePanel === 'proxifyre') return renderProxiFyrePanel();
if (activePanel === 'proxy') return renderProxyPanel();
@@ -1772,6 +1848,227 @@ function routeEndpointLabel(
}
}
function connectionCheckView(input: ConnectionCheckInput): ConnectionCheckView {
if (input.routeMode === 'external') {
const proxyValidation = input.proxyInput.trim() ? safeProxyError(input.proxyInput) : 'Введи адрес SOCKS5 прокси.';
const endpoint = routeEndpointLabel('external', input.proxyInput, input.singBoxStatus);
const details = [
'Режим: внешний SOCKS5',
`Endpoint: ${endpoint}`,
'Проверка: TCP connect до proxy host:port.',
'Local sing-box для внешнего маршрута не требуется.',
];
if (proxyValidation) {
return {
tone: 'warning',
title: 'Endpoint не готов',
text: proxyValidation,
endpoint,
details,
disabledReason: proxyValidation,
loading: input.isProxyChecking,
};
}
if (input.proxyPing) {
return {
tone: pingTone(input.proxyPing),
title: input.proxyPing.ok ? 'Endpoint отвечает' : 'Endpoint не отвечает',
text: pingResultText(input.proxyPing),
endpoint,
details,
loading: input.isProxyChecking,
};
}
return {
tone: 'muted',
title: 'Проверка не запускалась',
text: 'Проверит доступность SOCKS5 endpoint без изменения маршрута.',
endpoint,
details,
loading: input.isProxyChecking,
};
}
const localAddress = localSingBoxAddress(input.singBoxStatus);
const selectedEndpoint = input.selectedServer
? `${displayServerTag(input.selectedServer.tag)} · ${formatHostPort(input.selectedServer.server, input.selectedServer.serverPort)}`
: 'сервер не выбран';
const details = [
'Режим: локальный sing-box',
`Local listener: ${localAddress}`,
`LAN: ${lanSingBoxAddress(input.singBoxStatus) ?? 'недоступен'}`,
`Сервер: ${selectedEndpoint}`,
'Проверка: TCP connect до выбранного сервера подписки.',
];
if (input.isDetectingComponents) {
return {
tone: 'checking',
title: 'Проверяю компоненты',
text: 'Обновляю состояние Local sing-box перед проверкой.',
endpoint: localAddress,
details,
disabledReason: 'Дождись завершения проверки компонентов.',
loading: Boolean(input.serverPingTag),
};
}
if (!input.singbox?.installed) {
return {
tone: 'warning',
title: 'Локальный runtime не готов',
text: 'Установи Local sing-box, затем загрузи подписку и выбери сервер.',
endpoint: localAddress,
details,
disabledReason: 'Local sing-box не установлен.',
loading: false,
};
}
if (!input.singbox.running) {
return {
tone: 'warning',
title: 'Служба остановлена',
text: 'Запусти Local sing-box перед проверкой локального маршрута.',
endpoint: localAddress,
details,
disabledReason: 'Local sing-box остановлен.',
loading: false,
};
}
if (!input.selectedServer) {
return {
tone: 'warning',
title: 'Сервер не выбран',
text: 'Выбери сервер подписки для проверки.',
endpoint: selectedEndpoint,
details,
disabledReason: 'Сервер Local sing-box не выбран.',
loading: false,
};
}
if (input.selectedServerPing) {
return {
tone: pingTone(input.selectedServerPing),
title: input.selectedServerPing.ok ? 'Сервер отвечает' : 'Сервер не отвечает',
text: pingResultText(input.selectedServerPing),
endpoint: selectedEndpoint,
details,
loading: input.serverPingTag === input.selectedServer.tag,
};
}
return {
tone: 'muted',
title: 'Проверка не запускалась',
text: 'Проверит выбранный сервер подписки без применения маршрута.',
endpoint: selectedEndpoint,
details,
loading: input.serverPingTag === input.selectedServer.tag,
};
}
function routeChainSegments(input: RouteChainInput): RouteChainSegment[] {
const proxyValidation = input.routeMode === 'external' && input.proxyInput.trim()
? safeProxyError(input.proxyInput)
: null;
const localServer = input.singBoxStatus?.config.selectedServerTag
? displayServerTag(input.singBoxStatus.config.selectedServerTag)
: 'сервер не выбран';
const endpoint = input.routeMode === 'local-singbox'
? localSingBoxAddress(input.singBoxStatus)
: routeEndpointLabel('external', input.proxyInput, input.singBoxStatus);
const endpointTone: StatusTone = input.routeMode === 'external'
? proxyValidation || !input.proxyInput.trim() ? 'warning' : 'ok'
: input.singbox?.running ? 'ok' : input.singbox?.installed ? 'warning' : 'warning';
const exitTone: StatusTone = input.routeMode === 'external'
? proxyValidation || !input.proxyInput.trim() ? 'warning' : 'ok'
: input.singbox?.running && input.selectedServer ? 'ok' : 'warning';
return [
{
id: 'apps',
label: 'Приложения',
value: appCountCompact(input.appCount),
tone: input.appCount > 0 ? 'ok' : 'warning',
details: [
input.appCount > 0 ? appCountText(input.appCount) : 'Добавь хотя бы одно приложение на вкладке ProxiFyre.',
],
},
{
id: 'proxifyre',
label: 'ProxiFyre',
value: input.isDetectingComponents ? 'проверяю' : input.proxyfier?.running ? 'запущен' : input.proxyfier?.installed ? 'остановлен' : 'не найден',
tone: componentChainTone(input.proxyfier, input.isDetectingComponents),
details: [
proxyfierTitle(input.proxyfier, input.isDetectingComponents),
proxyfierDetails(input.proxyfier, input.isDetectingComponents),
],
},
{
id: 'endpoint',
label: input.routeMode === 'local-singbox' ? 'Local endpoint' : 'SOCKS5 endpoint',
value: endpoint,
tone: endpointTone,
details: input.routeMode === 'local-singbox'
? singBoxDetailLines(input.singbox, input.singBoxStatus, null, input.singBoxStatus?.config.selectedServerTag)
: [
`Endpoint: ${endpoint}`,
proxyValidation ?? 'Формат внешнего SOCKS5 корректен.',
'Local sing-box не участвует во внешнем маршруте.',
],
},
{
id: 'exit',
label: input.routeMode === 'local-singbox' ? 'VPN сервер' : 'Выход',
value: input.routeMode === 'local-singbox' ? localServer : 'внешний SOCKS5',
tone: exitTone,
details: input.routeMode === 'local-singbox'
? [
input.selectedServer ? serverLabel(input.selectedServer) : 'Сервер Local sing-box не выбран.',
'Применение создаст sing-box config и обновит ProxiFyre.',
]
: [
'Выбранные приложения идут через внешний SOCKS5.',
'Local sing-box не нужен для этого маршрута.',
],
},
];
}
function routeBlockerIsVisibleInProxyPanel(
context: 'proxifyre' | 'proxy',
routeMode: RouteMode,
title: string | undefined,
) {
if (context !== 'proxy' || !title) return false;
if (routeMode === 'external') {
return title === 'Прокси не указан' || title === 'Проверь формат прокси';
}
return title === 'Local sing-box не установлен' || title === 'Сервер не выбран';
}
function componentChainTone(component: ComponentStatus | undefined, checking: boolean): StatusTone {
if (checking) return 'checking';
if (!component?.installed) return 'warning';
if (!component.running) return 'warning';
return 'ok';
}
function appCountCompact(count: number) {
if (count === 1) return '1 приложение';
if (count > 1 && count < 5) return `${count} приложения`;
return `${count} приложений`;
}
function appCountText(count: number) {
if (count === 1) return '1 приложение маршрутизируется через профиль.';
if (count > 1 && count < 5) return `${count} приложения маршрутизируются через профиль.`;
@@ -1800,21 +2097,6 @@ function pingResultText(ping: PingServerResponse) {
return `${ping.server}:${ping.serverPort}: ${ping.error ?? 'нет ответа'}`;
}
function routePreviewText(
routeMode: RouteMode,
proxyInput: string,
status: LocalSingBoxStatusResponse | null,
) {
if (routeMode === 'local-singbox') {
const server = status?.config.selectedServerTag
? displayServerTag(status.config.selectedServerTag)
: 'сервер не выбран';
return `Выбранные приложения -> ProxiFyre -> Local sing-box ${localSingBoxAddress(status)} -> ${server}`;
}
return `Выбранные приложения -> ProxiFyre -> внешний прокси ${routeEndpointLabel('external', proxyInput, status)}`;
}
function applyButtonLabel(
context: 'proxifyre' | 'proxy',
isApplying: boolean,
@@ -1961,12 +2243,6 @@ function itemIcon(type: DraftItemType) {
return <FileCode2 size={18} strokeWidth={1.9} />;
}
function setupItemDetails(installed: boolean, version: string | undefined, details: string) {
if (!installed) return `Нужно установить. ${details}`;
if (version) return `${version}. ${details}`;
return details;
}
function proxifyreSetupPlaceholders(): ProxiFyreSetupStatus['items'] {
return [
{ id: 'vc-runtime', name: 'Среда запуска', installed: false, details: 'Проверяю' },
@@ -2019,14 +2295,6 @@ function proxyfierDetails(component: ComponentStatus | undefined, checking: bool
return component.problems[0] ?? 'Путь установки не найден.';
}
function singBoxTitle(component: ComponentStatus | undefined, checking: boolean) {
if (checking) return 'Проверяю Local sing-box';
if (!component) return 'Local sing-box не проверен';
if (component.running) return 'Local sing-box найден и запущен';
if (component.installed) return 'Local sing-box найден';
return 'Local sing-box не установлен';
}
function singBoxDetails(
component: ComponentStatus | undefined,
status: LocalSingBoxStatusResponse | null,
@@ -2043,7 +2311,30 @@ function singBoxDetails(
if (status?.config.hasSubscription) {
return status.config.subscriptionDisplayUrl ?? 'Подписка сохранена.';
}
return component?.problems[0] ?? 'Установи компонент, чтобы подключить подписку и выбрать сервер.';
return component?.problems[0] ?? 'Установи компонент для подписки и локального маршрута.';
}
function singBoxDetailLines(
component: ComponentStatus | undefined,
status: LocalSingBoxStatusResponse | null,
setupStatus: SingBoxSetupStatus | null,
selectedServerTag: string | undefined,
) {
const setupDetails = setupStatus
? setupStatus.items
.map((item) => `${item.name}: ${setupItemShortStatus(item)}`)
.join('; ')
: 'состав не проверен';
return [
`Локально: ${localSingBoxAddress(status)}`,
`LAN: ${lanSingBoxAddress(status) ?? 'недоступен'}`,
`Сервер: ${selectedServerTag ? displayServerTag(selectedServerTag) : 'не выбран'}`,
`Файл: ${component?.path ?? 'не найден'}`,
`Конфиг: ${status?.generatedConfigPath ?? 'не создан'}`,
`Подписка: ${status?.config.subscriptionDisplayUrl ?? (status?.config.hasSubscription ? 'сохранена' : 'не загружена')}`,
`Состав: ${setupDetails}`,
];
}
function componentDetails(component: ComponentStatus | undefined, checking: boolean) {