Refactor VPN proxy routing and session handling
This commit is contained in:
@@ -1,18 +1,21 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { Cpu, FileCode2, FolderOpen } from 'lucide-react';
|
||||
import {
|
||||
applyProfiles,
|
||||
getComponents,
|
||||
getProfiles,
|
||||
getStatus,
|
||||
getTargets,
|
||||
getSavedState,
|
||||
openConfigLocation,
|
||||
saveProfile,
|
||||
saveTarget,
|
||||
startProxiFyreService,
|
||||
stopProxiFyreService,
|
||||
type ApplyProfilesResponse,
|
||||
} from '../api/tauriCommands';
|
||||
import type { ComponentStatus, Profile, ProfileItemInput, ProfileItemType, Target } from '../domain/types';
|
||||
|
||||
type DraftItemType = Extract<ProfileItemType, 'process' | 'exe'>;
|
||||
type DraftItemType = Extract<ProfileItemType, 'process' | 'folder' | 'exe'>;
|
||||
type ServiceVisualState = 'active' | 'settling' | null;
|
||||
|
||||
interface DraftItem {
|
||||
id: string;
|
||||
@@ -26,8 +29,14 @@ interface Notice {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface LogEntry extends Notice {
|
||||
id: string;
|
||||
at: number;
|
||||
}
|
||||
|
||||
const MAIN_TARGET_ID = 'main-proxy';
|
||||
const MAIN_PROFILE_ID = 'main-profile';
|
||||
const LOG_VISIBLE_MS = 6500;
|
||||
|
||||
const fallbackComponents: ComponentStatus[] = [
|
||||
{
|
||||
@@ -47,50 +56,63 @@ export function App() {
|
||||
const [targetId, setTargetId] = useState(MAIN_TARGET_ID);
|
||||
const [items, setItems] = useState<DraftItem[]>([]);
|
||||
const [loadedProfiles, setLoadedProfiles] = useState<Profile[]>([]);
|
||||
const [newItemType, setNewItemType] = useState<DraftItemType>('process');
|
||||
const [newItemValue, setNewItemValue] = useState('');
|
||||
const [isProcessInputOpen, setIsProcessInputOpen] = useState(false);
|
||||
const [processInput, setProcessInput] = useState('');
|
||||
const [pickerAction, setPickerAction] = useState<'exe' | 'folder' | null>(null);
|
||||
const [components, setComponents] = useState<ComponentStatus[]>(fallbackComponents);
|
||||
const [generatedConfigPath, setGeneratedConfigPath] = useState('');
|
||||
const [notice, setNotice] = useState<Notice | null>(null);
|
||||
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
|
||||
const [activeLogId, setActiveLogId] = useState<string | null>(null);
|
||||
const [isLogOpen, setIsLogOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isDetectingComponents, setIsDetectingComponents] = useState(true);
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const [isOpeningConfig, setIsOpeningConfig] = useState(false);
|
||||
const [serviceAction, setServiceAction] = useState<'start' | 'stop' | null>(null);
|
||||
const [serviceVisualState, setServiceVisualState] = useState<ServiceVisualState>(null);
|
||||
const serviceVisualTimerRef = useRef<number | null>(null);
|
||||
|
||||
const proxyfier = useMemo(
|
||||
() => components.find((component) => component.id === 'proxyfier'),
|
||||
[components],
|
||||
);
|
||||
const activeLog = useMemo(
|
||||
() => logEntries.find((entry) => entry.id === activeLogId) ?? null,
|
||||
[activeLogId, logEntries],
|
||||
);
|
||||
const finderStateClass = isDetectingComponents ? 'checking' : proxyfier?.installed ? 'found' : 'missing';
|
||||
const finderVisualClass =
|
||||
serviceVisualState === 'active' ? 'working' : serviceVisualState === 'settling' ? 'settling' : '';
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (serviceVisualTimerRef.current !== null) {
|
||||
window.clearTimeout(serviceVisualTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeLogId) return undefined;
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
setActiveLogId((current) => (current === activeLogId ? null : current));
|
||||
}, LOG_VISIBLE_MS);
|
||||
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [activeLogId]);
|
||||
|
||||
async function refresh() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [status, profiles, targets, detectedComponents] = await Promise.all([
|
||||
getStatus(),
|
||||
getProfiles(),
|
||||
getTargets(),
|
||||
getComponents(),
|
||||
]);
|
||||
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, status.activeTarget, activeProfile);
|
||||
const editableProfiles = mainProfile ? [mainProfile] : activeProfiles;
|
||||
|
||||
if (activeTarget) setProxyInput(formatProxy(activeTarget));
|
||||
setItems(itemsForProfiles(editableProfiles));
|
||||
setLoadedProfiles(profiles);
|
||||
setProfileId(mainProfile?.id ?? MAIN_PROFILE_ID);
|
||||
setTargetId(activeTarget?.id ?? activeProfile?.targetId ?? MAIN_TARGET_ID);
|
||||
|
||||
setComponents(detectedComponents);
|
||||
setGeneratedConfigPath(status.generatedConfigPath);
|
||||
setNotice(null);
|
||||
const saved = await getSavedState();
|
||||
applySavedState(saved.profiles, saved.targets, saved.generatedConfigPath);
|
||||
} catch {
|
||||
setNotice({
|
||||
showNotice({
|
||||
kind: 'info',
|
||||
title: 'Режим предпросмотра',
|
||||
text: 'Запусти приложение через Tauri, чтобы увидеть найденный ProxiFyre и применить конфиг.',
|
||||
@@ -98,53 +120,108 @@ export function App() {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
void refreshComponents();
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
const value = normalizeItemValue(newItemValue, newItemType);
|
||||
async function refreshComponents() {
|
||||
setIsDetectingComponents(true);
|
||||
try {
|
||||
const detectedComponents = await getComponents();
|
||||
setComponents(detectedComponents);
|
||||
} catch (error) {
|
||||
showNotice({
|
||||
kind: 'error',
|
||||
title: 'ProxiFyre не проверен',
|
||||
text: errorMessage(error),
|
||||
});
|
||||
} finally {
|
||||
setIsDetectingComponents(false);
|
||||
}
|
||||
}
|
||||
|
||||
function applySavedState(profiles: Profile[], targets: Target[], generatedPath: string) {
|
||||
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 editableProfiles = mainProfile ? [mainProfile] : activeProfiles;
|
||||
|
||||
if (activeTarget) setProxyInput(formatProxy(activeTarget));
|
||||
setItems(itemsForProfiles(editableProfiles));
|
||||
setLoadedProfiles(profiles);
|
||||
setProfileId(mainProfile?.id ?? MAIN_PROFILE_ID);
|
||||
setTargetId(activeTarget?.id ?? activeProfile?.targetId ?? MAIN_TARGET_ID);
|
||||
setGeneratedConfigPath(generatedPath);
|
||||
}
|
||||
|
||||
function addItem(type: DraftItemType, rawValue: string) {
|
||||
const value = normalizeItemValue(rawValue, type);
|
||||
if (!value) {
|
||||
setNotice({
|
||||
showNotice({
|
||||
kind: 'error',
|
||||
title: 'Нечего добавить',
|
||||
text: newItemType === 'process' ? 'Введи имя процесса.' : 'Введи путь к EXE-файлу.',
|
||||
text: emptyItemMessage(type),
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (items.some((item) => item.type === newItemType && sameValue(item.value, value))) {
|
||||
setNotice({
|
||||
if (items.some((item) => item.type === type && sameValue(item.value, value))) {
|
||||
showNotice({
|
||||
kind: 'info',
|
||||
title: 'Уже добавлено',
|
||||
text: value,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
setItems((current) => [
|
||||
...current,
|
||||
{
|
||||
id: `${newItemType}-${Date.now()}`,
|
||||
type: newItemType,
|
||||
id: `${type}-${Date.now()}`,
|
||||
type,
|
||||
value,
|
||||
},
|
||||
]);
|
||||
setNewItemValue('');
|
||||
setNotice(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
function addProcess() {
|
||||
if (addItem('process', processInput)) {
|
||||
setProcessInput('');
|
||||
setIsProcessInputOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
function removeItem(id: string) {
|
||||
setItems((current) => current.filter((item) => item.id !== id));
|
||||
}
|
||||
|
||||
async function pickAndAddItem(type: Extract<DraftItemType, 'exe' | 'folder'>) {
|
||||
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;
|
||||
try {
|
||||
parsedProxy = parseProxy(proxyInput);
|
||||
if (!items.length) {
|
||||
throw new Error('Добавь хотя бы один процесс или EXE-файл.');
|
||||
}
|
||||
if (!items.length) throw new Error('Добавь хотя бы один процесс, EXE-файл или папку.');
|
||||
} catch (error) {
|
||||
setNotice({
|
||||
showNotice({
|
||||
kind: 'error',
|
||||
title: 'Проверь данные',
|
||||
text: errorMessage(error),
|
||||
@@ -177,18 +254,16 @@ export function App() {
|
||||
);
|
||||
|
||||
const result = await applyProfiles();
|
||||
const [status, detectedComponents, profiles] = await Promise.all([
|
||||
getStatus(),
|
||||
const [saved, detectedComponents] = await Promise.all([
|
||||
getSavedState(),
|
||||
getComponents(),
|
||||
getProfiles(),
|
||||
]);
|
||||
|
||||
applySavedState(saved.profiles, saved.targets, result.generatedConfigPath);
|
||||
setComponents(detectedComponents);
|
||||
setGeneratedConfigPath(status.generatedConfigPath);
|
||||
setLoadedProfiles(profiles);
|
||||
setNotice(noticeFromApply(result));
|
||||
showNotice(noticeFromApply(result));
|
||||
} catch (error) {
|
||||
setNotice({
|
||||
showNotice({
|
||||
kind: 'error',
|
||||
title: 'Конфиг не обновлен',
|
||||
text: errorMessage(error),
|
||||
@@ -202,13 +277,13 @@ export function App() {
|
||||
setIsOpeningConfig(true);
|
||||
try {
|
||||
const openedPath = await openConfigLocation();
|
||||
setNotice({
|
||||
showNotice({
|
||||
kind: 'info',
|
||||
title: 'Конфиг открыт',
|
||||
text: openedPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setNotice({
|
||||
showNotice({
|
||||
kind: 'error',
|
||||
title: 'Не удалось открыть конфиг',
|
||||
text: errorMessage(error),
|
||||
@@ -218,6 +293,66 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function setProxiFyreServiceRunning(shouldRun: boolean) {
|
||||
const action = shouldRun ? 'start' : 'stop';
|
||||
setServiceAction(action);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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 showNotice(notice: Notice) {
|
||||
const entry: LogEntry = {
|
||||
...notice,
|
||||
id: `log-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
at: Date.now(),
|
||||
};
|
||||
|
||||
setLogEntries((current) => [entry, ...current].slice(0, 40));
|
||||
setActiveLogId(entry.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="simple-shell">
|
||||
<section className="simple-panel">
|
||||
@@ -226,16 +361,45 @@ export function App() {
|
||||
<small>VPN Proxy</small>
|
||||
<h1>Прокси для приложений</h1>
|
||||
</div>
|
||||
<button type="button" className="ghost-button" onClick={refresh} disabled={isLoading}>
|
||||
{isLoading ? 'Ищу...' : 'Обновить'}
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
onClick={refresh}
|
||||
disabled={isLoading || isDetectingComponents}
|
||||
>
|
||||
{isLoading ? 'Загружаю...' : isDetectingComponents ? 'Проверяю...' : 'Обновить'}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className={`finder-card ${proxyfier?.installed ? 'found' : 'missing'}`}>
|
||||
<div className={`finder-card ${finderStateClass} ${finderVisualClass}`.trim()}>
|
||||
<span className="finder-border-glow" aria-hidden="true">
|
||||
<span className="finder-border-glow-segment top" />
|
||||
<span className="finder-border-glow-segment right" />
|
||||
<span className="finder-border-glow-segment bottom" />
|
||||
<span className="finder-border-glow-segment left" />
|
||||
</span>
|
||||
<span className="status-light" />
|
||||
<div>
|
||||
<strong>{proxyfierTitle(proxyfier)}</strong>
|
||||
<span>{proxyfierDetails(proxyfier)}</span>
|
||||
<div className="finder-text">
|
||||
<strong>{proxyfierTitle(proxyfier, isDetectingComponents)}</strong>
|
||||
<span>{proxyfierDetails(proxyfier, isDetectingComponents)}</span>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -255,35 +419,90 @@ export function App() {
|
||||
<span>{items.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="add-line">
|
||||
<select
|
||||
value={newItemType}
|
||||
onChange={(event) => setNewItemType(event.target.value as DraftItemType)}
|
||||
>
|
||||
<option value="process">Процесс</option>
|
||||
<option value="exe">EXE-файл</option>
|
||||
</select>
|
||||
<input
|
||||
value={newItemValue}
|
||||
onChange={(event) => setNewItemValue(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') addItem();
|
||||
}}
|
||||
placeholder={newItemType === 'process' ? 'Discord' : 'C:\\Apps\\app.exe'}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<button type="button" onClick={addItem}>
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
{isProcessInputOpen ? (
|
||||
<div className="process-add-line">
|
||||
<input
|
||||
value={processInput}
|
||||
onChange={(event) => setProcessInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') addProcess();
|
||||
if (event.key === 'Escape') {
|
||||
setProcessInput('');
|
||||
setIsProcessInputOpen(false);
|
||||
}
|
||||
}}
|
||||
placeholder="Discord"
|
||||
spellCheck={false}
|
||||
autoFocus
|
||||
/>
|
||||
<button type="button" onClick={addProcess}>
|
||||
OK
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="process-cancel-button"
|
||||
onClick={() => {
|
||||
setProcessInput('');
|
||||
setIsProcessInputOpen(false);
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="add-toolbar" aria-label="Добавить приложение">
|
||||
<button
|
||||
type="button"
|
||||
className="add-tile"
|
||||
onClick={() => setIsProcessInputOpen(true)}
|
||||
aria-label="Добавить процесс"
|
||||
title="Процесс"
|
||||
>
|
||||
<Cpu size={22} strokeWidth={1.8} />
|
||||
<span className="sr-only">Процесс</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="add-tile"
|
||||
onClick={() => void pickAndAddItem('exe')}
|
||||
disabled={Boolean(pickerAction)}
|
||||
aria-label="Добавить EXE-файл"
|
||||
title="EXE-файл"
|
||||
>
|
||||
{pickerAction === 'exe' ? <span className="tile-loading">...</span> : <FileCode2 size={22} strokeWidth={1.8} />}
|
||||
<span className="sr-only">EXE-файл</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="add-tile"
|
||||
onClick={() => void pickAndAddItem('folder')}
|
||||
disabled={Boolean(pickerAction)}
|
||||
aria-label="Добавить папку"
|
||||
title="Папка"
|
||||
>
|
||||
{pickerAction === 'folder' ? <span className="tile-loading">...</span> : <FolderOpen size={22} strokeWidth={1.8} />}
|
||||
<span className="sr-only">Папка</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="app-list">
|
||||
{items.length ? (
|
||||
{isLoading ? (
|
||||
<div className="list-skeleton" aria-label="Загрузка приложений">
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
) : items.length ? (
|
||||
items.map((item) => (
|
||||
<div className="app-row" key={item.id}>
|
||||
<div>
|
||||
<strong>{item.value}</strong>
|
||||
<span>{item.type === 'process' ? 'процесс' : 'EXE-файл'}</span>
|
||||
<div className="app-row-main">
|
||||
<span className="item-icon" aria-hidden="true">
|
||||
{itemIcon(item.type)}
|
||||
</span>
|
||||
<div>
|
||||
<strong>{item.value}</strong>
|
||||
<span>{itemTypeLabel(item.type)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick={() => removeItem(item.id)} aria-label={`Удалить ${item.value}`}>
|
||||
Удалить
|
||||
@@ -291,18 +510,11 @@ export function App() {
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">Добавь процесс или путь к EXE-файлу.</div>
|
||||
<div className="empty-state">Добавь процесс, папку или путь к EXE-файлу.</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{notice ? (
|
||||
<div className={`notice-line ${notice.kind}`}>
|
||||
<strong>{notice.title}</strong>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="command-row">
|
||||
<button type="button" className="apply-button" onClick={updateConfig} disabled={isApplying}>
|
||||
{isApplying ? 'Обновляю...' : 'Обновить конфиг'}
|
||||
@@ -319,6 +531,38 @@ export function App() {
|
||||
|
||||
{generatedConfigPath ? <p className="config-path">{generatedConfigPath}</p> : null}
|
||||
</section>
|
||||
|
||||
{logEntries.length ? (
|
||||
<aside className={`log-dock ${activeLog?.kind ?? 'idle'}`} aria-live="polite">
|
||||
<div className={`log-current ${activeLog ? 'visible' : 'hidden'}`}>
|
||||
{activeLog ? (
|
||||
<>
|
||||
<strong>{activeLog.title}</strong>
|
||||
<span>{activeLog.text}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="log-muted">Журнал событий</span>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" className="log-toggle" onClick={() => setIsLogOpen((current) => !current)}>
|
||||
{isLogOpen ? 'Скрыть' : 'Посмотреть'}
|
||||
<span>{logEntries.length}</span>
|
||||
</button>
|
||||
{isLogOpen ? (
|
||||
<div className="log-history">
|
||||
{logEntries.map((entry) => (
|
||||
<div className={`log-history-row ${entry.kind}`} key={entry.id}>
|
||||
<time>{formatLogTime(entry.at)}</time>
|
||||
<div>
|
||||
<strong>{entry.title}</strong>
|
||||
<span>{entry.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</aside>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -358,8 +602,7 @@ function parseProxy(rawValue: string): ParsedProxy {
|
||||
return { protocol: 'socks5', host, port };
|
||||
}
|
||||
|
||||
function targetForUi(targets: Target[], activeTarget: Target | undefined, profile: Profile | undefined) {
|
||||
if (activeTarget) return activeTarget;
|
||||
function targetForUi(targets: Target[], profile: Profile | undefined) {
|
||||
if (profile) return targets.find((target) => target.id === profile.targetId);
|
||||
return targets.find((target) => target.id === MAIN_TARGET_ID) ?? targets.find((target) => target.kind === 'external');
|
||||
}
|
||||
@@ -370,7 +613,7 @@ function itemsForProfiles(profiles: Profile[]): DraftItem[] {
|
||||
|
||||
for (const profile of profiles) {
|
||||
for (const item of profile.items) {
|
||||
if (item.type !== 'process' && item.type !== 'exe') continue;
|
||||
if (item.type !== 'process' && item.type !== 'folder' && item.type !== 'exe') continue;
|
||||
|
||||
const key = `${item.type}:${item.value.trim().toLowerCase()}`;
|
||||
if (seen.has(key)) continue;
|
||||
@@ -395,7 +638,7 @@ function formatProxy(target: Target) {
|
||||
function normalizeItemValue(value: string, type: DraftItemType) {
|
||||
const clean = value.trim().replace(/^"|"$/g, '');
|
||||
if (!clean) return '';
|
||||
if (type === 'exe') return clean;
|
||||
if (type === 'folder' || type === 'exe') return clean;
|
||||
|
||||
return clean
|
||||
.split(/[\\/]/)
|
||||
@@ -404,14 +647,58 @@ function normalizeItemValue(value: string, type: DraftItemType) {
|
||||
.trim() ?? '';
|
||||
}
|
||||
|
||||
async function pickPath(type: Extract<DraftItemType, 'exe' | 'folder'>) {
|
||||
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<void>((resolve) => {
|
||||
window.requestAnimationFrame(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
function profileItemInput(item: DraftItem): ProfileItemInput {
|
||||
return {
|
||||
type: item.type,
|
||||
value: item.value,
|
||||
recursive: false,
|
||||
recursive: item.type === 'folder',
|
||||
};
|
||||
}
|
||||
|
||||
function emptyItemMessage(type: DraftItemType) {
|
||||
if (type === 'process') return 'Введи имя процесса.';
|
||||
if (type === 'folder') return 'Введи путь к папке.';
|
||||
return 'Введи путь к EXE-файлу.';
|
||||
}
|
||||
|
||||
function itemTypeLabel(type: DraftItemType) {
|
||||
if (type === 'process') return 'процесс';
|
||||
if (type === 'folder') return 'папка';
|
||||
return 'EXE-файл';
|
||||
}
|
||||
|
||||
function itemIcon(type: DraftItemType) {
|
||||
if (type === 'process') return <Cpu size={18} strokeWidth={1.9} />;
|
||||
if (type === 'folder') return <FolderOpen size={18} strokeWidth={1.9} />;
|
||||
return <FileCode2 size={18} strokeWidth={1.9} />;
|
||||
}
|
||||
|
||||
function profileInputFromProfile(profile: Profile, enabled: boolean) {
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -427,14 +714,16 @@ function profileInputFromProfile(profile: Profile, enabled: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
function proxyfierTitle(component: ComponentStatus | undefined) {
|
||||
function proxyfierTitle(component: ComponentStatus | undefined, checking: boolean) {
|
||||
if (checking) return 'Проверяю ProxiFyre';
|
||||
if (!component) return 'ProxiFyre не проверен';
|
||||
if (component.running) return 'ProxiFyre найден и запущен';
|
||||
if (component.installed) return 'ProxiFyre найден';
|
||||
return 'ProxiFyre не найден';
|
||||
}
|
||||
|
||||
function proxyfierDetails(component: ComponentStatus | undefined) {
|
||||
function proxyfierDetails(component: ComponentStatus | undefined, checking: boolean) {
|
||||
if (checking) return 'Ищу установленный клиент и состояние службы.';
|
||||
if (!component) return 'Нажми «Обновить», чтобы проверить компьютер.';
|
||||
if (component.path) return component.path;
|
||||
return component.problems[0] ?? 'Путь установки не найден.';
|
||||
@@ -448,10 +737,29 @@ function noticeFromApply(result: ApplyProfilesResponse): Notice {
|
||||
};
|
||||
}
|
||||
|
||||
function upsertComponent(components: ComponentStatus[], component: ComponentStatus) {
|
||||
const index = components.findIndex((current) => current.id === component.id);
|
||||
if (index === -1) return [...components, component];
|
||||
|
||||
return [
|
||||
...components.slice(0, index),
|
||||
component,
|
||||
...components.slice(index + 1),
|
||||
];
|
||||
}
|
||||
|
||||
function sameValue(left: string, right: string) {
|
||||
return left.trim().toLowerCase() === right.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function formatLogTime(timestamp: number) {
|
||||
return new Date(timestamp).toLocaleTimeString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
if (error instanceof Error) return error.message;
|
||||
if (typeof error === 'string') return error;
|
||||
|
||||
Reference in New Issue
Block a user