import { parseProxy } from "./parseProxy"; import { itemTypeLabel, normalizeItemValue, type DraftItemType, } from "./profileItems"; export type RouteMode = "external" | "local-singbox"; export interface ConfigSnapshotItem { type: DraftItemType; value: string; } export interface ConfigSnapshot { routeMode: RouteMode; proxy: string; selectedServerId: string; selectedServerTag: string; items: ConfigSnapshotItem[]; } export interface PendingChangeRow { id: string; label: string; before?: string; after: string; tone?: "added" | "removed" | "changed"; } export function configSnapshotFromUi( routeMode: RouteMode, proxyInput: string, items: Array<{ type: DraftItemType; value: string }>, selectedServerId?: string, selectedServerTag?: string, ): ConfigSnapshot { return { routeMode, proxy: routeMode === "external" ? normalizeProxySnapshot(proxyInput) : "", selectedServerId: routeMode === "local-singbox" ? (selectedServerId?.trim() ?? "") : "", selectedServerTag: routeMode === "local-singbox" ? (selectedServerTag?.trim() ?? "") : "", items: normalizeSnapshotItems(items), }; } export function configChangeRows( applied: ConfigSnapshot, current: ConfigSnapshot, ): PendingChangeRow[] { if (sameConfigSnapshot(applied, current)) return []; const rows: PendingChangeRow[] = []; if (applied.routeMode !== current.routeMode) { rows.push({ id: "route-mode", label: "Маршрут", before: routeModeLabel(applied.routeMode), after: routeModeLabel(current.routeMode), }); } if ( applied.proxy !== current.proxy && (applied.routeMode === "external" || current.routeMode === "external") ) { rows.push({ id: "external-proxy", label: "SOCKS5", before: snapshotProxyChangeText(applied), after: snapshotProxyChangeText(current), }); } if ( applied.selectedServerId !== current.selectedServerId && (applied.routeMode === "local-singbox" || current.routeMode === "local-singbox") ) { rows.push({ id: "vpn-server", label: "VPN сервер", before: snapshotServerChangeText(applied), after: snapshotServerChangeText(current), }); } rows.push(...snapshotItemChangeRows(applied.items, current.items)); return rows; } export function sameConfigSnapshot( left: ConfigSnapshot, right: ConfigSnapshot, ) { return ( left.routeMode === right.routeMode && left.proxy === right.proxy && left.selectedServerId === right.selectedServerId && left.selectedServerTag === right.selectedServerTag && left.items.length === right.items.length && left.items.every((item, index) => { const other = right.items[index]; return item.type === other.type && item.value === other.value; }) ); } export function routeModeLabel(routeMode: RouteMode) { return routeMode === "local-singbox" ? "Локальный прокси" : "Внешний прокси"; } export function displaySnapshotProxy(proxy: string) { return proxy.replace(/^socks5:\/\//, "") || "не указан"; } export function displayServerTag(tag: string) { const withoutFlags = tag .replace(/[\u{1f1e6}-\u{1f1ff}]/gu, "") .replace(/\s*->\s*/g, " -> ") .replace(/\s*->\s*$/g, "") .replace(/^\s*->\s*/g, "") .replace(/\s{2,}/g, " ") .trim(); return withoutFlags || tag; } function normalizeProxySnapshot(value: string) { try { const parsed = parseProxy(value); return `${parsed.protocol}://${parsed.host.trim().toLowerCase()}:${parsed.port}`; } catch { return value.trim().toLowerCase(); } } function normalizeSnapshotItems( items: Array<{ type: DraftItemType; value: string }>, ): ConfigSnapshotItem[] { return items .map((item) => ({ type: item.type, value: normalizeItemValue(item.value, item.type).toLowerCase(), })) .filter((item) => item.value) .sort((left, right) => `${left.type}:${left.value}`.localeCompare( `${right.type}:${right.value}`, ), ); } function snapshotProxyChangeText(snapshot: ConfigSnapshot) { return snapshot.routeMode === "external" ? displaySnapshotProxy(snapshot.proxy) : "не используется"; } function snapshotServerChangeText(snapshot: ConfigSnapshot) { if (snapshot.routeMode !== "local-singbox") return "не используется"; return snapshot.selectedServerTag ? displayServerTag(snapshot.selectedServerTag) : "сервер не выбран"; } function snapshotItemChangeRows( appliedItems: ConfigSnapshotItem[], currentItems: ConfigSnapshotItem[], ): PendingChangeRow[] { const appliedKeys = new Set(appliedItems.map(snapshotItemKey)); const currentKeys = new Set(currentItems.map(snapshotItemKey)); const added = currentItems.filter( (item) => !appliedKeys.has(snapshotItemKey(item)), ); const removed = appliedItems.filter( (item) => !currentKeys.has(snapshotItemKey(item)), ); return [ ...added.map((item) => ({ id: `app-add-${snapshotItemKey(item)}`, label: "Добавлено", after: `+ ${formatSnapshotItem(item)}`, tone: "added" as const, })), ...removed.map((item) => ({ id: `app-remove-${snapshotItemKey(item)}`, label: "Удалено", after: `- ${formatSnapshotItem(item)}`, tone: "removed" as const, })), ]; } function snapshotItemKey(item: ConfigSnapshotItem) { return `${item.type}:${item.value}`; } function formatSnapshotItem(item: ConfigSnapshotItem) { return `${itemTypeLabel(item.type)} ${item.value}`; }