Add VPN proxy connection handling

This commit is contained in:
2026-07-07 22:33:15 +03:00
parent 7dbf786c56
commit c5bdb10445
8 changed files with 1108 additions and 29 deletions

View File

@@ -1,20 +1,25 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { open } from '@tauri-apps/plugin-dialog';
import { Cpu, FileCode2, FolderOpen } from 'lucide-react';
import { Cpu, FileCode2, FolderOpen, MoreHorizontal } from 'lucide-react';
import {
applyProfiles,
getComponents,
getProxiFyreSetupStatus,
getSavedState,
installProxiFyre,
openConfigLocation,
saveProfile,
saveTarget,
startProxiFyreService,
stopProxiFyreService,
uninstallProxiFyre,
type ApplyProfilesResponse,
type ProxiFyreSetupStatus,
} from '../api/tauriCommands';
import type { ComponentStatus, Profile, ProfileItemInput, ProfileItemType, Target } from '../domain/types';
type DraftItemType = Extract<ProfileItemType, 'process' | 'folder' | 'exe'>;
type ProxiFyreAction = 'start' | 'stop' | 'install' | 'uninstall';
type ServiceVisualState = 'active' | 'settling' | null;
interface DraftItem {
@@ -60,6 +65,8 @@ export function App() {
const [processInput, setProcessInput] = useState('');
const [pickerAction, setPickerAction] = useState<'exe' | 'folder' | null>(null);
const [components, setComponents] = useState<ComponentStatus[]>(fallbackComponents);
const [setupStatus, setSetupStatus] = useState<ProxiFyreSetupStatus | null>(null);
const [isSetupOpen, setIsSetupOpen] = useState(false);
const [generatedConfigPath, setGeneratedConfigPath] = useState('');
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
const [activeLogId, setActiveLogId] = useState<string | null>(null);
@@ -68,7 +75,8 @@ export function App() {
const [isDetectingComponents, setIsDetectingComponents] = useState(true);
const [isApplying, setIsApplying] = useState(false);
const [isOpeningConfig, setIsOpeningConfig] = useState(false);
const [serviceAction, setServiceAction] = useState<'start' | 'stop' | null>(null);
const [serviceAction, setServiceAction] = useState<ProxiFyreAction | null>(null);
const [isServiceMenuOpen, setIsServiceMenuOpen] = useState(false);
const [serviceVisualState, setServiceVisualState] = useState<ServiceVisualState>(null);
const serviceVisualTimerRef = useRef<number | null>(null);
@@ -127,8 +135,12 @@ export function App() {
async function refreshComponents() {
setIsDetectingComponents(true);
try {
const detectedComponents = await getComponents();
const [detectedComponents, detectedSetupStatus] = await Promise.all([
getComponents(),
getProxiFyreSetupStatus(),
]);
setComponents(detectedComponents);
setSetupStatus(detectedSetupStatus);
} catch (error) {
showNotice({
kind: 'error',
@@ -254,13 +266,15 @@ export function App() {
);
const result = await applyProfiles();
const [saved, detectedComponents] = await Promise.all([
const [saved, detectedComponents, detectedSetupStatus] = await Promise.all([
getSavedState(),
getComponents(),
getProxiFyreSetupStatus(),
]);
applySavedState(saved.profiles, saved.targets, result.generatedConfigPath);
setComponents(detectedComponents);
setSetupStatus(detectedSetupStatus);
showNotice(noticeFromApply(result));
} catch (error) {
showNotice({
@@ -296,6 +310,7 @@ export function App() {
async function setProxiFyreServiceRunning(shouldRun: boolean) {
const action = shouldRun ? 'start' : 'stop';
setServiceAction(action);
setIsServiceMenuOpen(false);
startServiceVisual();
try {
await nextFrame();
@@ -321,6 +336,65 @@ export function App() {
}
}
async function installProxiFyrePackage() {
setServiceAction('install');
setIsServiceMenuOpen(false);
startServiceVisual();
try {
await nextFrame();
const component = await installProxiFyre();
const detectedSetupStatus = await getProxiFyreSetupStatus();
setComponents((current) => upsertComponent(current, component));
setSetupStatus(detectedSetupStatus);
showNotice({
kind: 'success',
title: 'ProxiFyre установлен',
text: proxyfierDetails(component, false),
});
} catch (error) {
showNotice({
kind: 'error',
title: 'ProxiFyre не установлен',
text: errorMessage(error),
});
} finally {
setServiceAction(null);
settleServiceVisual();
}
}
async function uninstallProxiFyrePackage() {
const confirmed = window.confirm(
'Удалить ProxiFyre с компьютера? Будет удалена служба и папка установки ProxiFyre.',
);
if (!confirmed) return;
setServiceAction('uninstall');
setIsServiceMenuOpen(false);
startServiceVisual();
try {
await nextFrame();
const component = await uninstallProxiFyre();
const detectedSetupStatus = await getProxiFyreSetupStatus();
setComponents((current) => upsertComponent(current, component));
setSetupStatus(detectedSetupStatus);
showNotice({
kind: 'success',
title: 'ProxiFyre удален',
text: 'Служба и папка установки ProxiFyre удалены.',
});
} catch (error) {
showNotice({
kind: 'error',
title: 'ProxiFyre не удален',
text: errorMessage(error),
});
} finally {
setServiceAction(null);
settleServiceVisual();
}
}
function startServiceVisual() {
if (serviceVisualTimerRef.current !== null) {
window.clearTimeout(serviceVisualTimerRef.current);
@@ -382,25 +456,89 @@ export function App() {
<div className="finder-text">
<strong>{proxyfierTitle(proxyfier, isDetectingComponents)}</strong>
<span>{proxyfierDetails(proxyfier, isDetectingComponents)}</span>
<button
type="button"
className="setup-toggle"
onClick={() => setIsSetupOpen((current) => !current)}
disabled={isDetectingComponents && !setupStatus}
aria-expanded={isSetupOpen}
>
{proxyfier?.installed ? 'Состав ProxiFyre' : 'Что будет установлено'}
{setupStatus ? (
<span>{setupStatus.ready ? 'все есть' : `не хватает: ${setupStatus.missingCount}`}</span>
) : null}
</button>
</div>
<div className="service-actions" aria-label="Управление службой ProxiFyre">
<button
type="button"
className="service-button"
onClick={() => setProxiFyreServiceRunning(true)}
disabled={isDetectingComponents || Boolean(serviceAction) || !proxyfier?.installed || Boolean(proxyfier?.running)}
>
{serviceAction === 'start' ? '...' : 'Запустить'}
</button>
<button
type="button"
className="service-button stop"
onClick={() => setProxiFyreServiceRunning(false)}
disabled={isDetectingComponents || Boolean(serviceAction) || !proxyfier?.installed || !proxyfier?.running}
>
{serviceAction === 'stop' ? '...' : 'Остановить'}
</button>
{proxyfier?.installed ? (
<>
<button
type="button"
className={`service-button ${proxyfier.running ? 'stop' : ''}`.trim()}
onClick={() => setProxiFyreServiceRunning(!proxyfier.running)}
disabled={isDetectingComponents || Boolean(serviceAction)}
>
{serviceAction === 'start' || serviceAction === 'stop'
? '...'
: proxyfier.running
? 'Остановить'
: 'Запустить'}
</button>
<div className="service-menu">
<button
type="button"
className="service-menu-button"
onClick={() => setIsServiceMenuOpen((current) => !current)}
disabled={isDetectingComponents || Boolean(serviceAction)}
aria-label="Дополнительные действия ProxiFyre"
aria-expanded={isServiceMenuOpen}
title="Еще"
>
<MoreHorizontal size={20} strokeWidth={2} />
</button>
{isServiceMenuOpen ? (
<div className="service-menu-popover">
<button type="button" onClick={() => void uninstallProxiFyrePackage()}>
{serviceAction === 'uninstall' ? 'Удаляю...' : 'Удалить ProxiFyre'}
</button>
</div>
) : null}
</div>
</>
) : (
<button
type="button"
className="service-button install"
onClick={() => void installProxiFyrePackage()}
disabled={isDetectingComponents || Boolean(serviceAction)}
>
{serviceAction === 'install' ? '...' : 'Установить'}
</button>
)}
</div>
{isSetupOpen ? (
<div className="setup-details">
{setupStatus ? (
setupStatus.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>Ищу установленные зависимости ProxiFyre.</span>
</div>
</div>
)}
</div>
) : null}
</div>
<label className="simple-field">
@@ -699,6 +837,12 @@ 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 profileInputFromProfile(profile: Profile, enabled: boolean) {
return {
id: profile.id,