Refactor application structure and simplify implementation

This commit is contained in:
2026-07-22 00:08:09 +03:00
parent dbba3806cc
commit 90b2eb507c
74 changed files with 11362 additions and 6814 deletions
+201
View File
@@ -0,0 +1,201 @@
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}`;
}