202 lines
5.5 KiB
TypeScript
202 lines
5.5 KiB
TypeScript
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}`;
|
|
}
|