) {
@@ -228,10 +297,16 @@ export function App() {
}
async function updateConfig() {
- let parsedProxy: ParsedProxy;
+ let parsedProxy: ParsedProxy | null = null;
try {
- parsedProxy = parseProxy(proxyInput);
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',
@@ -243,19 +318,28 @@ export function App() {
setIsApplying(true);
try {
- await saveTarget({
- id: targetId,
- name: 'Основной прокси',
- kind: 'external',
- protocol: parsedProxy.protocol,
- host: parsedProxy.host,
- port: parsedProxy.port,
- });
+ let singBoxGeneratedPath = '';
+ if (routeMode === 'external') {
+ if (!parsedProxy) throw new Error('Прокси не разобран.');
+ await saveTarget({
+ id: targetId,
+ name: 'Основной прокси',
+ kind: 'external',
+ protocol: parsedProxy.protocol,
+ host: parsedProxy.host,
+ port: parsedProxy.port,
+ });
+ } else {
+ const singBoxResult = await generateSingBoxConfig();
+ singBoxGeneratedPath = singBoxResult.generatedConfigPath;
+ await ensureSingBoxRunningForApply();
+ }
+
await saveProfile({
id: profileId,
name: 'Приложения через прокси',
enabled: true,
- targetId,
+ targetId: routeMode === 'local-singbox' ? LOCAL_SINGBOX_TARGET_ID : targetId,
protocols: ['TCP', 'UDP'],
items: items.map(profileItemInput),
});
@@ -266,16 +350,21 @@ export function App() {
);
const result = await applyProfiles();
- const [saved, detectedComponents, detectedSetupStatus] = await Promise.all([
+ 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);
- showNotice(noticeFromApply(result));
+ setSingBoxStatus(detectedSingBoxStatus);
+ setSingBoxSetupStatus(detectedSingBoxSetupStatus);
+ setHasUnappliedChanges(false);
+ showNotice(routeMode === 'local-singbox' ? noticeFromLocalApply(result, singBoxGeneratedPath) : noticeFromApply(result));
} catch (error) {
showNotice({
kind: 'error',
@@ -287,6 +376,30 @@ export function App() {
}
}
+ async function ensureSingBoxRunningForApply() {
+ if (routeMode !== 'local-singbox' || !singbox?.installed) return;
+
+ setIsSingBoxMenuOpen(false);
+ try {
+ await nextFrame();
+ if (singbox.running) {
+ setSingBoxAction('stop');
+ const stopped = await stopSingBoxService();
+ setComponents((current) => upsertComponent(current, stopped));
+ }
+
+ setSingBoxAction('start');
+ const component = await startSingBoxService();
+ setComponents((current) => upsertComponent(current, component));
+ const status = await refreshSingBoxState();
+ if (!status.component.running) {
+ throw new Error('Local sing-box установлен, но служба не запустилась.');
+ }
+ } finally {
+ setSingBoxAction(null);
+ }
+ }
+
async function openConfig() {
setIsOpeningConfig(true);
try {
@@ -395,6 +508,216 @@ export function App() {
}
}
+ 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();
+ 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() {
+ setSingBoxAction('install');
+ setIsSingBoxMenuOpen(false);
+ try {
+ await nextFrame();
+ const component = await installSingBox();
+ setComponents((current) => upsertComponent(current, component));
+ await refreshSingBoxState();
+ 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();
+ 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({});
+ setHasUnappliedChanges(true);
+ 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({});
+ setHasUnappliedChanges(true);
+ 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));
+ setHasUnappliedChanges(true);
+ } 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.tag, result])));
+ showNotice({
+ kind: 'info',
+ title: 'Ping завершен',
+ text: pingSummary(results),
+ });
+ } catch (error) {
+ showNotice({
+ kind: 'error',
+ title: 'Ping не выполнен',
+ text: errorMessage(error),
+ });
+ } finally {
+ setSingBoxAction(null);
+ }
+ }
+
+ 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);
@@ -541,15 +864,245 @@ export function App() {
) : null}
-
+
+
+ {routeMode === 'local-singbox' ? (
+
+
+
+
+
+
+
+
+
+
{singBoxTitle(singbox, isDetectingComponents)}
+
{singBoxDetails(singbox, singBoxStatus, isDetectingComponents)}
+
+
+
+
+
+
+ {singbox?.installed ? (
+ <>
+
+
+
+ {isSingBoxMenuOpen ? (
+
+
+
+ ) : null}
+
+ >
+ ) : (
+
+ )}
+
+
+ {isSingBoxInfoOpen ? (
+
+
+ Локально
+ {localSingBoxAddress(singBoxStatus)}
+ LAN
+ {lanSingBoxAddress(singBoxStatus) ?? 'недоступен'}
+ Сервер
+
+ {singBoxStatus?.config.selectedServerTag
+ ? displayServerTag(singBoxStatus.config.selectedServerTag)
+ : 'не выбран'}
+
+ Файл
+ {singbox?.path ?? 'не найден'}
+ Конфиг
+ {singBoxStatus?.generatedConfigPath ?? 'не создан'}
+
+
+
+
+
+
+ ) : null}
+
+ {isSingBoxSetupOpen ? (
+
+ {singBoxSetupStatus ? (
+ singBoxSetupStatus.items.map((item) => (
+
+
+
+ {item.name}
+ {setupItemDetails(item.installed, item.version, item.details)}
+
+
+ ))
+ ) : (
+
+
+
+ Проверяю состав
+ Ищу sing-box, WinSW wrapper и службу.
+
+
+ )}
+
+ ) : null}
+
+ {isSingBoxInstalled ? (
+
+
+
+
+
+ setSubscriptionInput(event.target.value)}
+ placeholder={singBoxStatus?.config.subscriptionDisplayUrl ?? 'https://example.com/sub'}
+ spellCheck={false}
+ />
+
+
+
+
+ {singBoxStatus?.cache?.servers.length ? (
+
+ {singBoxStatus.cache.servers.map((server) => (
+
+ ))}
+
+ ) : (
+
Подписка Local sing-box еще не загружена.
+ )}
+
+ ) : (
+
+ Local sing-box не установлен
+ Установи компонент, чтобы подключить подписку, выбрать сервер и включить локальный маршрут.
+
+ )}
+
+ ) : null}
@@ -653,9 +1206,24 @@ export function App() {
+ {hasUnappliedChanges ? (
+
+ Изменения еще не применены в ProxiFyre
+ {applyStateText(routeMode, isSingBoxInstalled, Boolean(singbox?.running))}
+
+ ) : null}
+