mirror of
https://github.com/itdoginfo/podkop.git
synced 2025-12-16 16:36:56 +03:00
Merge branch 'feat/yacd-exp' into feat/fe-app-podkop
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
.idea
|
.idea
|
||||||
fe-app-podkop/node_modules
|
fe-app-podkop/node_modules
|
||||||
|
fe-app-podkop/.env
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default [
|
|||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
{
|
{
|
||||||
ignores: ['node_modules'],
|
ignores: ['node_modules', 'watch-upload.js'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@@ -10,14 +10,19 @@
|
|||||||
"build": "tsup src/main.ts",
|
"build": "tsup src/main.ts",
|
||||||
"dev": "tsup src/main.ts --watch",
|
"dev": "tsup src/main.ts --watch",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build"
|
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build",
|
||||||
|
"watch:sftp": "node watch-upload.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||||
"@typescript-eslint/parser": "8.45.0",
|
"@typescript-eslint/parser": "8.45.0",
|
||||||
|
"chokidar": "4.0.3",
|
||||||
|
"dotenv": "17.2.3",
|
||||||
"eslint": "9.36.0",
|
"eslint": "9.36.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
"glob": "11.0.3",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
|
"ssh2-sftp-client": "12.0.1",
|
||||||
"tsup": "8.5.0",
|
"tsup": "8.5.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.45.0",
|
"typescript-eslint": "8.45.0",
|
||||||
|
|||||||
2
fe-app-podkop/src/clash/index.ts
Normal file
2
fe-app-podkop/src/clash/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './methods';
|
||||||
28
fe-app-podkop/src/clash/methods/createBaseApiRequest.ts
Normal file
28
fe-app-podkop/src/clash/methods/createBaseApiRequest.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { IBaseApiResponse } from '../types';
|
||||||
|
|
||||||
|
export async function createBaseApiRequest<T>(
|
||||||
|
fetchFn: () => Promise<Response>,
|
||||||
|
): Promise<IBaseApiResponse<T>> {
|
||||||
|
try {
|
||||||
|
const response = await fetchFn();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
success: false as const,
|
||||||
|
message: `${_('HTTP error')} ${response.status}: ${response.statusText}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: T = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true as const,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false as const,
|
||||||
|
message: e instanceof Error ? e.message : _('Unknown error'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
14
fe-app-podkop/src/clash/methods/getConfig.ts
Normal file
14
fe-app-podkop/src/clash/methods/getConfig.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { ClashAPI, IBaseApiResponse } from '../types';
|
||||||
|
import { createBaseApiRequest } from './createBaseApiRequest';
|
||||||
|
import { getClashApiUrl } from '../../helpers';
|
||||||
|
|
||||||
|
export async function getClashConfig(): Promise<
|
||||||
|
IBaseApiResponse<ClashAPI.Config>
|
||||||
|
> {
|
||||||
|
return createBaseApiRequest<ClashAPI.Config>(() =>
|
||||||
|
fetch(`${getClashApiUrl()}/configs`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
20
fe-app-podkop/src/clash/methods/getGroupDelay.ts
Normal file
20
fe-app-podkop/src/clash/methods/getGroupDelay.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ClashAPI, IBaseApiResponse } from '../types';
|
||||||
|
import { createBaseApiRequest } from './createBaseApiRequest';
|
||||||
|
import { getClashApiUrl } from '../../helpers';
|
||||||
|
|
||||||
|
export async function getClashGroupDelay(
|
||||||
|
group: string,
|
||||||
|
url = 'https://www.gstatic.com/generate_204',
|
||||||
|
timeout = 2000,
|
||||||
|
): Promise<IBaseApiResponse<ClashAPI.Delays>> {
|
||||||
|
const endpoint = `${getClashApiUrl()}/group/${group}/delay?url=${encodeURIComponent(
|
||||||
|
url,
|
||||||
|
)}&timeout=${timeout}`;
|
||||||
|
|
||||||
|
return createBaseApiRequest<ClashAPI.Delays>(() =>
|
||||||
|
fetch(endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
14
fe-app-podkop/src/clash/methods/getProxies.ts
Normal file
14
fe-app-podkop/src/clash/methods/getProxies.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { ClashAPI, IBaseApiResponse } from '../types';
|
||||||
|
import { createBaseApiRequest } from './createBaseApiRequest';
|
||||||
|
import { getClashApiUrl } from '../../helpers';
|
||||||
|
|
||||||
|
export async function getClashProxies(): Promise<
|
||||||
|
IBaseApiResponse<ClashAPI.Proxies>
|
||||||
|
> {
|
||||||
|
return createBaseApiRequest<ClashAPI.Proxies>(() =>
|
||||||
|
fetch(`${getClashApiUrl()}/proxies`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
14
fe-app-podkop/src/clash/methods/getVersion.ts
Normal file
14
fe-app-podkop/src/clash/methods/getVersion.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { ClashAPI, IBaseApiResponse } from '../types';
|
||||||
|
import { createBaseApiRequest } from './createBaseApiRequest';
|
||||||
|
import { getClashApiUrl } from '../../helpers';
|
||||||
|
|
||||||
|
export async function getClashVersion(): Promise<
|
||||||
|
IBaseApiResponse<ClashAPI.Version>
|
||||||
|
> {
|
||||||
|
return createBaseApiRequest<ClashAPI.Version>(() =>
|
||||||
|
fetch(`${getClashApiUrl()}/version`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
7
fe-app-podkop/src/clash/methods/index.ts
Normal file
7
fe-app-podkop/src/clash/methods/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './createBaseApiRequest';
|
||||||
|
export * from './getConfig';
|
||||||
|
export * from './getGroupDelay';
|
||||||
|
export * from './getProxies';
|
||||||
|
export * from './getVersion';
|
||||||
|
export * from './triggerProxySelector';
|
||||||
|
export * from './triggerLatencyTest';
|
||||||
35
fe-app-podkop/src/clash/methods/triggerLatencyTest.ts
Normal file
35
fe-app-podkop/src/clash/methods/triggerLatencyTest.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { IBaseApiResponse } from '../types';
|
||||||
|
import { createBaseApiRequest } from './createBaseApiRequest';
|
||||||
|
import { getClashApiUrl } from '../../helpers';
|
||||||
|
|
||||||
|
export async function triggerLatencyGroupTest(
|
||||||
|
tag: string,
|
||||||
|
timeout: number = 5000,
|
||||||
|
url: string = 'https://www.gstatic.com/generate_204',
|
||||||
|
): Promise<IBaseApiResponse<void>> {
|
||||||
|
return createBaseApiRequest<void>(() =>
|
||||||
|
fetch(
|
||||||
|
`${getClashApiUrl()}/group/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function triggerLatencyProxyTest(
|
||||||
|
tag: string,
|
||||||
|
timeout: number = 2000,
|
||||||
|
url: string = 'https://www.gstatic.com/generate_204',
|
||||||
|
): Promise<IBaseApiResponse<void>> {
|
||||||
|
return createBaseApiRequest<void>(() =>
|
||||||
|
fetch(
|
||||||
|
`${getClashApiUrl()}/proxies/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
16
fe-app-podkop/src/clash/methods/triggerProxySelector.ts
Normal file
16
fe-app-podkop/src/clash/methods/triggerProxySelector.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { IBaseApiResponse } from '../types';
|
||||||
|
import { createBaseApiRequest } from './createBaseApiRequest';
|
||||||
|
import { getClashApiUrl } from '../../helpers';
|
||||||
|
|
||||||
|
export async function triggerProxySelector(
|
||||||
|
selector: string,
|
||||||
|
outbound: string,
|
||||||
|
): Promise<IBaseApiResponse<void>> {
|
||||||
|
return createBaseApiRequest<void>(() =>
|
||||||
|
fetch(`${getClashApiUrl()}/proxies/${selector}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: outbound }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
53
fe-app-podkop/src/clash/types.ts
Normal file
53
fe-app-podkop/src/clash/types.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export type IBaseApiResponse<T> =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
export namespace ClashAPI {
|
||||||
|
export interface Version {
|
||||||
|
meta: boolean;
|
||||||
|
premium: boolean;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
port: number;
|
||||||
|
'socks-port': number;
|
||||||
|
'redir-port': number;
|
||||||
|
'tproxy-port': number;
|
||||||
|
'mixed-port': number;
|
||||||
|
'allow-lan': boolean;
|
||||||
|
'bind-address': string;
|
||||||
|
mode: 'Rule' | 'Global' | 'Direct';
|
||||||
|
'mode-list': string[];
|
||||||
|
'log-level': 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
ipv6: boolean;
|
||||||
|
tun: null | Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProxyHistoryEntry {
|
||||||
|
time: string;
|
||||||
|
delay: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProxyBase {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
udp: boolean;
|
||||||
|
history: ProxyHistoryEntry[];
|
||||||
|
now?: string;
|
||||||
|
all?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Proxies {
|
||||||
|
proxies: Record<string, ProxyBase>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Delays = Record<string, number>;
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
interface CopyToClipboardResponse {
|
|
||||||
success: boolean;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function copyToClipboard(text: string): CopyToClipboardResponse {
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
textarea.value = text;
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.execCommand('copy');
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'Copied!',
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
const error = err as Error;
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Failed to copy: ${error.message}`,
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
fe-app-podkop/src/helpers/getClashApiUrl.ts
Normal file
11
fe-app-podkop/src/helpers/getClashApiUrl.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export function getClashApiUrl(): string {
|
||||||
|
const { protocol, hostname } = window.location;
|
||||||
|
|
||||||
|
return `${protocol}//${hostname}:9090`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClashWsUrl(): string {
|
||||||
|
const { hostname } = window.location;
|
||||||
|
|
||||||
|
return `ws://${hostname}:9090`;
|
||||||
|
}
|
||||||
13
fe-app-podkop/src/helpers/getProxyUrlName.ts
Normal file
13
fe-app-podkop/src/helpers/getProxyUrlName.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export function getProxyUrlName(url: string) {
|
||||||
|
try {
|
||||||
|
const [_link, hash] = url.split('#');
|
||||||
|
|
||||||
|
if (!hash) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeURIComponent(hash);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,7 @@ export * from './parseValueList';
|
|||||||
export * from './injectGlobalStyles';
|
export * from './injectGlobalStyles';
|
||||||
export * from './withTimeout';
|
export * from './withTimeout';
|
||||||
export * from './executeShellCommand';
|
export * from './executeShellCommand';
|
||||||
export * from './copyToClipboard';
|
|
||||||
export * from './maskIP';
|
export * from './maskIP';
|
||||||
|
export * from './getProxyUrlName';
|
||||||
|
export * from './onMount';
|
||||||
|
export * from './getClashApiUrl';
|
||||||
|
|||||||
30
fe-app-podkop/src/helpers/onMount.ts
Normal file
30
fe-app-podkop/src/helpers/onMount.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export async function onMount(id: string): Promise<HTMLElement> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
|
||||||
|
if (el && el.offsetParent !== null) {
|
||||||
|
return resolve(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const target = document.getElementById(id);
|
||||||
|
if (target) {
|
||||||
|
const io = new IntersectionObserver((entries) => {
|
||||||
|
const visible = entries.some((e) => e.isIntersecting);
|
||||||
|
if (visible) {
|
||||||
|
observer.disconnect();
|
||||||
|
io.disconnect();
|
||||||
|
resolve(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
io.observe(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
12
fe-app-podkop/src/helpers/prettyBytes.ts
Normal file
12
fe-app-podkop/src/helpers/prettyBytes.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// steal from https://github.com/sindresorhus/pretty-bytes/blob/master/index.js
|
||||||
|
export function prettyBytes(n: number) {
|
||||||
|
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
|
if (n < 1000) {
|
||||||
|
return n + ' B';
|
||||||
|
}
|
||||||
|
const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
|
||||||
|
n = Number((n / Math.pow(1000, exponent)).toPrecision(3));
|
||||||
|
const unit = UNITS[exponent];
|
||||||
|
return n + ' ' + unit;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ export async function withTimeout<T>(
|
|||||||
promise: Promise<T>,
|
promise: Promise<T>,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
operationName: string,
|
operationName: string,
|
||||||
timeoutMessage = 'Operation timed out',
|
timeoutMessage = _('Operation timed out'),
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
let timeoutId;
|
let timeoutId;
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
|
|||||||
25
fe-app-podkop/src/luci.d.ts
vendored
25
fe-app-podkop/src/luci.d.ts
vendored
@@ -1,3 +1,15 @@
|
|||||||
|
type HtmlTag = keyof HTMLElementTagNameMap;
|
||||||
|
|
||||||
|
type HtmlElement<T extends HtmlTag> = HTMLElementTagNameMap[T];
|
||||||
|
|
||||||
|
type HtmlAttributes<T extends HtmlTag = 'div'> = Partial<
|
||||||
|
Omit<HtmlElement<T>, 'style' | 'children'> & {
|
||||||
|
style?: string | Partial<CSSStyleDeclaration>;
|
||||||
|
class?: string;
|
||||||
|
onclick?: (event: MouseEvent) => void;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const fs: {
|
const fs: {
|
||||||
exec(
|
exec(
|
||||||
@@ -10,6 +22,19 @@ declare global {
|
|||||||
code?: number;
|
code?: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const E: <T extends HtmlTag>(
|
||||||
|
type: T,
|
||||||
|
attr?: HtmlAttributes<T> | null,
|
||||||
|
children?: (Node | string)[] | Node | string,
|
||||||
|
) => HTMLElementTagNameMap[T];
|
||||||
|
|
||||||
|
const uci: {
|
||||||
|
load: (packages: string | string[]) => Promise<string>;
|
||||||
|
sections: (conf: string, type?: string, cb?: () => void) => Promise<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _ = (_key: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require baseclass';
|
'require baseclass';
|
||||||
'require fs';
|
'require fs';
|
||||||
|
'require uci';
|
||||||
|
|
||||||
export * from './validators';
|
export * from './validators';
|
||||||
export * from './helpers';
|
export * from './helpers';
|
||||||
|
export * from './clash';
|
||||||
|
export * from './podkop';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
|
|||||||
3
fe-app-podkop/src/podkop/index.ts
Normal file
3
fe-app-podkop/src/podkop/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './methods';
|
||||||
|
export * from './services';
|
||||||
|
export * from './tabs';
|
||||||
5
fe-app-podkop/src/podkop/methods/getConfigSections.ts
Normal file
5
fe-app-podkop/src/podkop/methods/getConfigSections.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Podkop } from '../types';
|
||||||
|
|
||||||
|
export async function getConfigSections(): Promise<Podkop.ConfigSection[]> {
|
||||||
|
return uci.load('podkop').then(() => uci.sections('podkop'));
|
||||||
|
}
|
||||||
153
fe-app-podkop/src/podkop/methods/getDashboardSections.ts
Normal file
153
fe-app-podkop/src/podkop/methods/getDashboardSections.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { Podkop } from '../types';
|
||||||
|
import { getConfigSections } from './getConfigSections';
|
||||||
|
import { getClashProxies } from '../../clash';
|
||||||
|
import { getProxyUrlName } from '../../helpers';
|
||||||
|
|
||||||
|
interface IGetDashboardSectionsResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: Podkop.OutboundGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDashboardSections(): Promise<IGetDashboardSectionsResponse> {
|
||||||
|
const configSections = await getConfigSections();
|
||||||
|
const clashProxies = await getClashProxies();
|
||||||
|
|
||||||
|
if (!clashProxies.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxies = Object.entries(clashProxies.data.proxies).map(
|
||||||
|
([key, value]) => ({
|
||||||
|
code: key,
|
||||||
|
value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = configSections
|
||||||
|
.filter((section) => section.mode !== 'block')
|
||||||
|
.map((section) => {
|
||||||
|
if (section.mode === 'proxy') {
|
||||||
|
if (section.proxy_config_type === 'url') {
|
||||||
|
const outbound = proxies.find(
|
||||||
|
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
withTagSelect: false,
|
||||||
|
code: outbound?.code || section['.name'],
|
||||||
|
displayName: section['.name'],
|
||||||
|
outbounds: [
|
||||||
|
{
|
||||||
|
code: outbound?.code || section['.name'],
|
||||||
|
displayName:
|
||||||
|
getProxyUrlName(section.proxy_string) ||
|
||||||
|
outbound?.value?.name ||
|
||||||
|
'',
|
||||||
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||||
|
type: outbound?.value?.type || '',
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.proxy_config_type === 'outbound') {
|
||||||
|
const outbound = proxies.find(
|
||||||
|
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
withTagSelect: false,
|
||||||
|
code: outbound?.code || section['.name'],
|
||||||
|
displayName: section['.name'],
|
||||||
|
outbounds: [
|
||||||
|
{
|
||||||
|
code: outbound?.code || section['.name'],
|
||||||
|
displayName:
|
||||||
|
decodeURIComponent(JSON.parse(section.outbound_json)?.tag) ||
|
||||||
|
outbound?.value?.name ||
|
||||||
|
'',
|
||||||
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||||
|
type: outbound?.value?.type || '',
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.proxy_config_type === 'urltest') {
|
||||||
|
const selector = proxies.find(
|
||||||
|
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||||
|
);
|
||||||
|
const outbound = proxies.find(
|
||||||
|
(proxy) => proxy.code === `${section['.name']}-urltest-out`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const outbounds = (outbound?.value?.all ?? [])
|
||||||
|
.map((code) => proxies.find((item) => item.code === code))
|
||||||
|
.map((item, index) => ({
|
||||||
|
code: item?.code || '',
|
||||||
|
displayName:
|
||||||
|
getProxyUrlName(section.urltest_proxy_links?.[index]) ||
|
||||||
|
item?.value?.name ||
|
||||||
|
'',
|
||||||
|
latency: item?.value?.history?.[0]?.delay || 0,
|
||||||
|
type: item?.value?.type || '',
|
||||||
|
selected: selector?.value?.now === item?.code,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
withTagSelect: true,
|
||||||
|
code: selector?.code || section['.name'],
|
||||||
|
displayName: section['.name'],
|
||||||
|
outbounds: [
|
||||||
|
{
|
||||||
|
code: outbound?.code || '',
|
||||||
|
displayName: _('Fastest'),
|
||||||
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||||
|
type: outbound?.value?.type || '',
|
||||||
|
selected: selector?.value?.now === outbound?.code,
|
||||||
|
},
|
||||||
|
...outbounds,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.mode === 'vpn') {
|
||||||
|
const outbound = proxies.find(
|
||||||
|
(proxy) => proxy.code === `${section['.name']}-out`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
withTagSelect: false,
|
||||||
|
code: outbound?.code || section['.name'],
|
||||||
|
displayName: section['.name'],
|
||||||
|
outbounds: [
|
||||||
|
{
|
||||||
|
code: outbound?.code || section['.name'],
|
||||||
|
displayName: section.interface || outbound?.value?.name || '',
|
||||||
|
latency: outbound?.value?.history?.[0]?.delay || 0,
|
||||||
|
type: outbound?.value?.type || '',
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
withTagSelect: false,
|
||||||
|
code: section['.name'],
|
||||||
|
displayName: section['.name'],
|
||||||
|
outbounds: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
21
fe-app-podkop/src/podkop/methods/getPodkopStatus.ts
Normal file
21
fe-app-podkop/src/podkop/methods/getPodkopStatus.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { executeShellCommand } from '../../helpers';
|
||||||
|
|
||||||
|
export async function getPodkopStatus(): Promise<{
|
||||||
|
enabled: number;
|
||||||
|
status: string;
|
||||||
|
}> {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: '/usr/bin/podkop',
|
||||||
|
args: ['get_status'],
|
||||||
|
timeout: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.stdout) {
|
||||||
|
return JSON.parse(response.stdout.replace(/\n/g, '')) as {
|
||||||
|
enabled: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { enabled: 0, status: 'unknown' };
|
||||||
|
}
|
||||||
23
fe-app-podkop/src/podkop/methods/getSingboxStatus.ts
Normal file
23
fe-app-podkop/src/podkop/methods/getSingboxStatus.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { executeShellCommand } from '../../helpers';
|
||||||
|
|
||||||
|
export async function getSingboxStatus(): Promise<{
|
||||||
|
running: number;
|
||||||
|
enabled: number;
|
||||||
|
status: string;
|
||||||
|
}> {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: '/usr/bin/podkop',
|
||||||
|
args: ['get_sing_box_status'],
|
||||||
|
timeout: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.stdout) {
|
||||||
|
return JSON.parse(response.stdout.replace(/\n/g, '')) as {
|
||||||
|
running: number;
|
||||||
|
enabled: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { running: 0, enabled: 0, status: 'unknown' };
|
||||||
|
}
|
||||||
4
fe-app-podkop/src/podkop/methods/index.ts
Normal file
4
fe-app-podkop/src/podkop/methods/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './getConfigSections';
|
||||||
|
export * from './getDashboardSections';
|
||||||
|
export * from './getPodkopStatus';
|
||||||
|
export * from './getSingboxStatus';
|
||||||
13
fe-app-podkop/src/podkop/services/core.service.ts
Normal file
13
fe-app-podkop/src/podkop/services/core.service.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { TabServiceInstance } from './tab.service';
|
||||||
|
import { store } from '../../store';
|
||||||
|
|
||||||
|
export function coreService() {
|
||||||
|
TabServiceInstance.onChange((activeId, tabs) => {
|
||||||
|
store.set({
|
||||||
|
tabService: {
|
||||||
|
current: activeId || '',
|
||||||
|
all: tabs.map((tab) => tab.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
2
fe-app-podkop/src/podkop/services/index.ts
Normal file
2
fe-app-podkop/src/podkop/services/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './tab.service';
|
||||||
|
export * from './core.service';
|
||||||
92
fe-app-podkop/src/podkop/services/tab.service.ts
Normal file
92
fe-app-podkop/src/podkop/services/tab.service.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
type TabInfo = {
|
||||||
|
el: HTMLElement;
|
||||||
|
id: string;
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TabChangeCallback = (activeId: string | null, allTabs: TabInfo[]) => void;
|
||||||
|
|
||||||
|
export class TabService {
|
||||||
|
private static instance: TabService;
|
||||||
|
private observer: MutationObserver | null = null;
|
||||||
|
private callback?: TabChangeCallback;
|
||||||
|
private lastActiveId: string | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): TabService {
|
||||||
|
if (!TabService.instance) {
|
||||||
|
TabService.instance = new TabService();
|
||||||
|
}
|
||||||
|
return TabService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
this.observer = new MutationObserver(() => this.handleMutations());
|
||||||
|
this.observer.observe(document.body, {
|
||||||
|
subtree: true,
|
||||||
|
childList: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// initial check
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMutations() {
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTabsInfo(): TabInfo[] {
|
||||||
|
const tabs = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>('.cbi-tab, .cbi-tab-disabled'),
|
||||||
|
);
|
||||||
|
return tabs.map((el) => ({
|
||||||
|
el,
|
||||||
|
id: el.dataset.tab || '',
|
||||||
|
active:
|
||||||
|
el.classList.contains('cbi-tab') &&
|
||||||
|
!el.classList.contains('cbi-tab-disabled'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getActiveTabId(): string | null {
|
||||||
|
const active = document.querySelector<HTMLElement>(
|
||||||
|
'.cbi-tab:not(.cbi-tab-disabled)',
|
||||||
|
);
|
||||||
|
return active?.dataset.tab || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private notify() {
|
||||||
|
const tabs = this.getTabsInfo();
|
||||||
|
const activeId = this.getActiveTabId();
|
||||||
|
|
||||||
|
if (activeId !== this.lastActiveId) {
|
||||||
|
this.lastActiveId = activeId;
|
||||||
|
this.callback?.(activeId, tabs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onChange(callback: TabChangeCallback) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllTabs(): TabInfo[] {
|
||||||
|
return this.getTabsInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveTab(): string | null {
|
||||||
|
return this.getActiveTabId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnect() {
|
||||||
|
this.observer?.disconnect();
|
||||||
|
this.observer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabServiceInstance = TabService.getInstance();
|
||||||
2
fe-app-podkop/src/podkop/tabs/dashboard/index.ts
Normal file
2
fe-app-podkop/src/podkop/tabs/dashboard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './renderDashboard';
|
||||||
|
export * from './initDashboardController';
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
import {
|
||||||
|
getDashboardSections,
|
||||||
|
getPodkopStatus,
|
||||||
|
getSingboxStatus,
|
||||||
|
} from '../../methods';
|
||||||
|
import { getClashWsUrl, onMount } from '../../../helpers';
|
||||||
|
import {
|
||||||
|
triggerLatencyGroupTest,
|
||||||
|
triggerLatencyProxyTest,
|
||||||
|
triggerProxySelector,
|
||||||
|
} from '../../../clash';
|
||||||
|
import { store, StoreType } from '../../../store';
|
||||||
|
import { socket } from '../../../socket';
|
||||||
|
import { prettyBytes } from '../../../helpers/prettyBytes';
|
||||||
|
import { renderSections } from './renderSections';
|
||||||
|
import { renderWidget } from './renderWidget';
|
||||||
|
|
||||||
|
// Fetchers
|
||||||
|
|
||||||
|
async function fetchDashboardSections() {
|
||||||
|
const prev = store.get().sectionsWidget;
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
...prev,
|
||||||
|
failed: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, success } = await getDashboardSections();
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
sectionsWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: !success,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchServicesInfo() {
|
||||||
|
const [podkop, singbox] = await Promise.all([
|
||||||
|
getPodkopStatus(),
|
||||||
|
getSingboxStatus(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
data: { singbox: singbox.running, podkop: podkop.enabled },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectToClashSockets() {
|
||||||
|
socket.subscribe(
|
||||||
|
`${getClashWsUrl()}/traffic?token=`,
|
||||||
|
(msg) => {
|
||||||
|
const parsedMsg = JSON.parse(msg);
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
bandwidthWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
data: { up: parsedMsg.up, down: parsedMsg.down },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(_err) => {
|
||||||
|
store.set({
|
||||||
|
bandwidthWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: { up: 0, down: 0 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.subscribe(
|
||||||
|
`${getClashWsUrl()}/connections?token=`,
|
||||||
|
(msg) => {
|
||||||
|
const parsedMsg = JSON.parse(msg);
|
||||||
|
|
||||||
|
store.set({
|
||||||
|
trafficTotalWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
data: {
|
||||||
|
downloadTotal: parsedMsg.downloadTotal,
|
||||||
|
uploadTotal: parsedMsg.uploadTotal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
systemInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
data: {
|
||||||
|
connections: parsedMsg.connections?.length,
|
||||||
|
memory: parsedMsg.memory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(_err) => {
|
||||||
|
store.set({
|
||||||
|
trafficTotalWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: { downloadTotal: 0, uploadTotal: 0 },
|
||||||
|
},
|
||||||
|
systemInfoWidget: {
|
||||||
|
loading: false,
|
||||||
|
failed: true,
|
||||||
|
data: {
|
||||||
|
connections: 0,
|
||||||
|
memory: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
|
||||||
|
async function handleChooseOutbound(selector: string, tag: string) {
|
||||||
|
await triggerProxySelector(selector, tag);
|
||||||
|
await fetchDashboardSections();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTestGroupLatency(tag: string) {
|
||||||
|
await triggerLatencyGroupTest(tag);
|
||||||
|
await fetchDashboardSections();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTestProxyLatency(tag: string) {
|
||||||
|
await triggerLatencyProxyTest(tag);
|
||||||
|
await fetchDashboardSections();
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceTestLatencyButtonsWithSkeleton() {
|
||||||
|
document
|
||||||
|
.querySelectorAll('.dashboard-sections-grid-item-test-latency')
|
||||||
|
.forEach((el) => {
|
||||||
|
const newDiv = document.createElement('div');
|
||||||
|
newDiv.className = 'skeleton';
|
||||||
|
newDiv.style.width = '99px';
|
||||||
|
newDiv.style.height = '28px';
|
||||||
|
el.replaceWith(newDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderer
|
||||||
|
|
||||||
|
async function renderSectionsWidget() {
|
||||||
|
console.log('renderSectionsWidget');
|
||||||
|
const sectionsWidget = store.get().sectionsWidget;
|
||||||
|
const container = document.getElementById('dashboard-sections-grid');
|
||||||
|
|
||||||
|
if (sectionsWidget.loading || sectionsWidget.failed) {
|
||||||
|
const renderedWidget = renderSections({
|
||||||
|
loading: sectionsWidget.loading,
|
||||||
|
failed: sectionsWidget.failed,
|
||||||
|
section: {
|
||||||
|
code: '',
|
||||||
|
displayName: '',
|
||||||
|
outbounds: [],
|
||||||
|
withTagSelect: false,
|
||||||
|
},
|
||||||
|
onTestLatency: () => {},
|
||||||
|
onChooseOutbound: () => {},
|
||||||
|
});
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidgets = sectionsWidget.data.map((section) =>
|
||||||
|
renderSections({
|
||||||
|
loading: sectionsWidget.loading,
|
||||||
|
failed: sectionsWidget.failed,
|
||||||
|
section,
|
||||||
|
onTestLatency: (tag) => {
|
||||||
|
replaceTestLatencyButtonsWithSkeleton();
|
||||||
|
|
||||||
|
if (section.withTagSelect) {
|
||||||
|
return handleTestGroupLatency(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleTestProxyLatency(tag);
|
||||||
|
},
|
||||||
|
onChooseOutbound: (selector, tag) => {
|
||||||
|
handleChooseOutbound(selector, tag);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return container!.replaceChildren(...renderedWidgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderBandwidthWidget() {
|
||||||
|
console.log('renderBandwidthWidget');
|
||||||
|
const traffic = store.get().bandwidthWidget;
|
||||||
|
|
||||||
|
const container = document.getElementById('dashboard-widget-traffic');
|
||||||
|
|
||||||
|
if (traffic.loading || traffic.failed) {
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: traffic.loading,
|
||||||
|
failed: traffic.failed,
|
||||||
|
title: '',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: traffic.loading,
|
||||||
|
failed: traffic.failed,
|
||||||
|
title: _('Traffic'),
|
||||||
|
items: [
|
||||||
|
{ key: _('Uplink'), value: `${prettyBytes(traffic.data.up)}/s` },
|
||||||
|
{ key: _('Downlink'), value: `${prettyBytes(traffic.data.down)}/s` },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderTrafficTotalWidget() {
|
||||||
|
console.log('renderTrafficTotalWidget');
|
||||||
|
const trafficTotalWidget = store.get().trafficTotalWidget;
|
||||||
|
|
||||||
|
const container = document.getElementById('dashboard-widget-traffic-total');
|
||||||
|
|
||||||
|
if (trafficTotalWidget.loading || trafficTotalWidget.failed) {
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: trafficTotalWidget.loading,
|
||||||
|
failed: trafficTotalWidget.failed,
|
||||||
|
title: '',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: trafficTotalWidget.loading,
|
||||||
|
failed: trafficTotalWidget.failed,
|
||||||
|
title: _('Traffic Total'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: _('Uplink'),
|
||||||
|
value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: _('Downlink'),
|
||||||
|
value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderSystemInfoWidget() {
|
||||||
|
console.log('renderSystemInfoWidget');
|
||||||
|
const systemInfoWidget = store.get().systemInfoWidget;
|
||||||
|
|
||||||
|
const container = document.getElementById('dashboard-widget-system-info');
|
||||||
|
|
||||||
|
if (systemInfoWidget.loading || systemInfoWidget.failed) {
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: systemInfoWidget.loading,
|
||||||
|
failed: systemInfoWidget.failed,
|
||||||
|
title: '',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: systemInfoWidget.loading,
|
||||||
|
failed: systemInfoWidget.failed,
|
||||||
|
title: _('System info'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: _('Active Connections'),
|
||||||
|
value: String(systemInfoWidget.data.connections),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: _('Memory Usage'),
|
||||||
|
value: String(prettyBytes(systemInfoWidget.data.memory)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderServicesInfoWidget() {
|
||||||
|
console.log('renderServicesInfoWidget');
|
||||||
|
const servicesInfoWidget = store.get().servicesInfoWidget;
|
||||||
|
|
||||||
|
const container = document.getElementById('dashboard-widget-service-info');
|
||||||
|
|
||||||
|
if (servicesInfoWidget.loading || servicesInfoWidget.failed) {
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: servicesInfoWidget.loading,
|
||||||
|
failed: servicesInfoWidget.failed,
|
||||||
|
title: '',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedWidget = renderWidget({
|
||||||
|
loading: servicesInfoWidget.loading,
|
||||||
|
failed: servicesInfoWidget.failed,
|
||||||
|
title: _('Services info'),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: _('Podkop'),
|
||||||
|
value: servicesInfoWidget.data.podkop
|
||||||
|
? _('✔ Enabled')
|
||||||
|
: _('✘ Disabled'),
|
||||||
|
attributes: {
|
||||||
|
class: servicesInfoWidget.data.podkop
|
||||||
|
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
||||||
|
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: _('Sing-box'),
|
||||||
|
value: servicesInfoWidget.data.singbox
|
||||||
|
? _('✔ Running')
|
||||||
|
: _('✘ Stopped'),
|
||||||
|
attributes: {
|
||||||
|
class: servicesInfoWidget.data.singbox
|
||||||
|
? 'pdk_dashboard-page__widgets-section__item__row--success'
|
||||||
|
: 'pdk_dashboard-page__widgets-section__item__row--error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
container!.replaceChildren(renderedWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onStoreUpdate(
|
||||||
|
next: StoreType,
|
||||||
|
prev: StoreType,
|
||||||
|
diff: Partial<StoreType>,
|
||||||
|
) {
|
||||||
|
if (diff.sectionsWidget) {
|
||||||
|
renderSectionsWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff.bandwidthWidget) {
|
||||||
|
renderBandwidthWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff.trafficTotalWidget) {
|
||||||
|
renderTrafficTotalWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff.systemInfoWidget) {
|
||||||
|
renderSystemInfoWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff.servicesInfoWidget) {
|
||||||
|
renderServicesInfoWidget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initDashboardController(): Promise<void> {
|
||||||
|
onMount('dashboard-status').then(() => {
|
||||||
|
// Remove old listener
|
||||||
|
store.unsubscribe(onStoreUpdate);
|
||||||
|
// Clear store
|
||||||
|
store.reset();
|
||||||
|
|
||||||
|
// Add new listener
|
||||||
|
store.subscribe(onStoreUpdate);
|
||||||
|
|
||||||
|
// Initial sections fetch
|
||||||
|
fetchDashboardSections();
|
||||||
|
fetchServicesInfo();
|
||||||
|
connectToClashSockets();
|
||||||
|
});
|
||||||
|
}
|
||||||
54
fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts
Normal file
54
fe-app-podkop/src/podkop/tabs/dashboard/renderDashboard.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { renderSections } from './renderSections';
|
||||||
|
import { renderWidget } from './renderWidget';
|
||||||
|
|
||||||
|
export function renderDashboard() {
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
id: 'dashboard-status',
|
||||||
|
class: 'pdk_dashboard-page',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Widgets section
|
||||||
|
E('div', { class: 'pdk_dashboard-page__widgets-section' }, [
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{ id: 'dashboard-widget-traffic' },
|
||||||
|
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{ id: 'dashboard-widget-traffic-total' },
|
||||||
|
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{ id: 'dashboard-widget-system-info' },
|
||||||
|
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{ id: 'dashboard-widget-service-info' },
|
||||||
|
renderWidget({ loading: true, failed: false, title: '', items: [] }),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
// All outbounds
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{ id: 'dashboard-sections-grid' },
|
||||||
|
renderSections({
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
section: {
|
||||||
|
code: '',
|
||||||
|
displayName: '',
|
||||||
|
outbounds: [],
|
||||||
|
withTagSelect: false,
|
||||||
|
},
|
||||||
|
onTestLatency: () => {},
|
||||||
|
onChooseOutbound: () => {},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
125
fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts
Normal file
125
fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { Podkop } from '../../types';
|
||||||
|
|
||||||
|
interface IRenderSectionsProps {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
section: Podkop.OutboundGroup;
|
||||||
|
onTestLatency: (tag: string) => void;
|
||||||
|
onChooseOutbound: (selector: string, tag: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFailedState() {
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'pdk_dashboard-page__outbound-section centered',
|
||||||
|
style: 'height: 127px',
|
||||||
|
},
|
||||||
|
E('span', {}, _('Dashboard currently unavailable')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLoadingState() {
|
||||||
|
return E('div', {
|
||||||
|
id: 'dashboard-sections-grid-skeleton',
|
||||||
|
class: 'pdk_dashboard-page__outbound-section skeleton',
|
||||||
|
style: 'height: 127px',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderDefaultState({
|
||||||
|
section,
|
||||||
|
onChooseOutbound,
|
||||||
|
onTestLatency,
|
||||||
|
}: IRenderSectionsProps) {
|
||||||
|
function testLatency() {
|
||||||
|
if (section.withTagSelect) {
|
||||||
|
return onTestLatency(section.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.outbounds.length) {
|
||||||
|
return onTestLatency(section.outbounds[0].code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOutbound(outbound: Podkop.Outbound) {
|
||||||
|
function getLatencyClass() {
|
||||||
|
if (!outbound.latency) {
|
||||||
|
return 'pdk_dashboard-page__outbound-grid__item__latency--empty';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outbound.latency < 200) {
|
||||||
|
return 'pdk_dashboard-page__outbound-grid__item__latency--green';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outbound.latency < 400) {
|
||||||
|
return 'pdk_dashboard-page__outbound-grid__item__latency--yellow';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'pdk_dashboard-page__outbound-grid__item__latency--red';
|
||||||
|
}
|
||||||
|
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? 'pdk_dashboard-page__outbound-grid__item--active' : ''} ${section.withTagSelect ? 'pdk_dashboard-page__outbound-grid__item--selectable' : ''}`,
|
||||||
|
click: () =>
|
||||||
|
section.withTagSelect &&
|
||||||
|
onChooseOutbound(section.code, outbound.code),
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E('b', {}, outbound.displayName),
|
||||||
|
E('div', { class: 'pdk_dashboard-page__outbound-grid__item__footer' }, [
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{ class: 'pdk_dashboard-page__outbound-grid__item__type' },
|
||||||
|
outbound.type,
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{ class: getLatencyClass() },
|
||||||
|
outbound.latency ? `${outbound.latency}ms` : 'N/A',
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { class: 'pdk_dashboard-page__outbound-section' }, [
|
||||||
|
// Title with test latency
|
||||||
|
E('div', { class: 'pdk_dashboard-page__outbound-section__title-section' }, [
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'pdk_dashboard-page__outbound-section__title-section__title',
|
||||||
|
},
|
||||||
|
section.displayName,
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
class: 'btn dashboard-sections-grid-item-test-latency',
|
||||||
|
click: () => testLatency(),
|
||||||
|
},
|
||||||
|
'Test latency',
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{ class: 'pdk_dashboard-page__outbound-grid' },
|
||||||
|
section.outbounds.map((outbound) => renderOutbound(outbound)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSections(props: IRenderSectionsProps) {
|
||||||
|
if (props.failed) {
|
||||||
|
return renderFailedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.loading) {
|
||||||
|
return renderLoadingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderDefaultState(props);
|
||||||
|
}
|
||||||
78
fe-app-podkop/src/podkop/tabs/dashboard/renderWidget.ts
Normal file
78
fe-app-podkop/src/podkop/tabs/dashboard/renderWidget.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
interface IRenderWidgetProps {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
title: string;
|
||||||
|
items: Array<{
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
attributes?: {
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFailedState() {
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
style: 'height: 78px',
|
||||||
|
class: 'pdk_dashboard-page__widgets-section__item centered',
|
||||||
|
},
|
||||||
|
_('Currently unavailable'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLoadingState() {
|
||||||
|
return E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
style: 'height: 78px',
|
||||||
|
class: 'pdk_dashboard-page__widgets-section__item skeleton',
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDefaultState({ title, items }: IRenderWidgetProps) {
|
||||||
|
return E('div', { class: 'pdk_dashboard-page__widgets-section__item' }, [
|
||||||
|
E(
|
||||||
|
'b',
|
||||||
|
{ class: 'pdk_dashboard-page__widgets-section__item__title' },
|
||||||
|
title,
|
||||||
|
),
|
||||||
|
...items.map((item) =>
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ''}`,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E(
|
||||||
|
'span',
|
||||||
|
{ class: 'pdk_dashboard-page__widgets-section__item__row__key' },
|
||||||
|
`${item.key}: `,
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
'span',
|
||||||
|
{ class: 'pdk_dashboard-page__widgets-section__item__row__value' },
|
||||||
|
item.value,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderWidget(props: IRenderWidgetProps) {
|
||||||
|
if (props.loading) {
|
||||||
|
return renderLoadingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.failed) {
|
||||||
|
return renderFailedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderDefaultState(props);
|
||||||
|
}
|
||||||
1
fe-app-podkop/src/podkop/tabs/index.ts
Normal file
1
fe-app-podkop/src/podkop/tabs/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './dashboard';
|
||||||
56
fe-app-podkop/src/podkop/types.ts
Normal file
56
fe-app-podkop/src/podkop/types.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
export namespace Podkop {
|
||||||
|
export interface Outbound {
|
||||||
|
code: string;
|
||||||
|
displayName: string;
|
||||||
|
latency: number;
|
||||||
|
type: string;
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutboundGroup {
|
||||||
|
withTagSelect: boolean;
|
||||||
|
code: string;
|
||||||
|
displayName: string;
|
||||||
|
outbounds: Outbound[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigProxyUrlTestSection {
|
||||||
|
mode: 'proxy';
|
||||||
|
proxy_config_type: 'urltest';
|
||||||
|
urltest_proxy_links: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigProxyUrlSection {
|
||||||
|
mode: 'proxy';
|
||||||
|
proxy_config_type: 'url';
|
||||||
|
proxy_string: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigProxyOutboundSection {
|
||||||
|
mode: 'proxy';
|
||||||
|
proxy_config_type: 'outbound';
|
||||||
|
outbound_json: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigVpnSection {
|
||||||
|
mode: 'vpn';
|
||||||
|
interface: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigBlockSection {
|
||||||
|
mode: 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConfigBaseSection =
|
||||||
|
| ConfigProxyUrlTestSection
|
||||||
|
| ConfigProxyUrlSection
|
||||||
|
| ConfigProxyOutboundSection
|
||||||
|
| ConfigVpnSection
|
||||||
|
| ConfigBlockSection;
|
||||||
|
|
||||||
|
export type ConfigSection = ConfigBaseSection & {
|
||||||
|
'.name': string;
|
||||||
|
'.type': 'main' | 'extra';
|
||||||
|
};
|
||||||
|
}
|
||||||
121
fe-app-podkop/src/socket.ts
Normal file
121
fe-app-podkop/src/socket.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// eslint-disable-next-line
|
||||||
|
type Listener = (data: any) => void;
|
||||||
|
type ErrorListener = (error: Event | string) => void;
|
||||||
|
|
||||||
|
class SocketManager {
|
||||||
|
private static instance: SocketManager;
|
||||||
|
private sockets = new Map<string, WebSocket>();
|
||||||
|
private listeners = new Map<string, Set<Listener>>();
|
||||||
|
private connected = new Map<string, boolean>();
|
||||||
|
private errorListeners = new Map<string, Set<ErrorListener>>();
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): SocketManager {
|
||||||
|
if (!SocketManager.instance) {
|
||||||
|
SocketManager.instance = new SocketManager();
|
||||||
|
}
|
||||||
|
return SocketManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(url: string): void {
|
||||||
|
if (this.sockets.has(url)) return;
|
||||||
|
|
||||||
|
const ws = new WebSocket(url);
|
||||||
|
this.sockets.set(url, ws);
|
||||||
|
this.connected.set(url, false);
|
||||||
|
this.listeners.set(url, new Set());
|
||||||
|
this.errorListeners.set(url, new Set());
|
||||||
|
|
||||||
|
ws.addEventListener('open', () => {
|
||||||
|
this.connected.set(url, true);
|
||||||
|
console.info(`Connected: ${url}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('message', (event) => {
|
||||||
|
const handlers = this.listeners.get(url);
|
||||||
|
if (handlers) {
|
||||||
|
for (const handler of handlers) {
|
||||||
|
try {
|
||||||
|
handler(event.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Handler error for ${url}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('close', () => {
|
||||||
|
this.connected.set(url, false);
|
||||||
|
console.warn(`Disconnected: ${url}`);
|
||||||
|
this.triggerError(url, 'Connection closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('error', (err) => {
|
||||||
|
console.error(`Socket error for ${url}:`, err);
|
||||||
|
this.triggerError(url, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||||
|
if (!this.sockets.has(url)) {
|
||||||
|
this.connect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.get(url)?.add(listener);
|
||||||
|
|
||||||
|
if (onError) {
|
||||||
|
this.errorListeners.get(url)?.add(onError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void {
|
||||||
|
this.listeners.get(url)?.delete(listener);
|
||||||
|
if (onError) {
|
||||||
|
this.errorListeners.get(url)?.delete(onError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
send(url: string, data: any): void {
|
||||||
|
const ws = this.sockets.get(url);
|
||||||
|
if (ws && this.connected.get(url)) {
|
||||||
|
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
||||||
|
} else {
|
||||||
|
console.warn(`Cannot send: not connected to ${url}`);
|
||||||
|
this.triggerError(url, 'Not connected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(url: string): void {
|
||||||
|
const ws = this.sockets.get(url);
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
this.sockets.delete(url);
|
||||||
|
this.listeners.delete(url);
|
||||||
|
this.errorListeners.delete(url);
|
||||||
|
this.connected.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectAll(): void {
|
||||||
|
for (const url of this.sockets.keys()) {
|
||||||
|
this.disconnect(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private triggerError(url: string, err: Event | string): void {
|
||||||
|
const handlers = this.errorListeners.get(url);
|
||||||
|
if (handlers) {
|
||||||
|
for (const cb of handlers) {
|
||||||
|
try {
|
||||||
|
cb(err);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error handler threw for ${url}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const socket = SocketManager.getInstance();
|
||||||
179
fe-app-podkop/src/store.ts
Normal file
179
fe-app-podkop/src/store.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { Podkop } from './podkop/types';
|
||||||
|
|
||||||
|
function jsonStableStringify<T, V>(obj: T): string {
|
||||||
|
return JSON.stringify(obj, (_, value) => {
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
return Object.keys(value)
|
||||||
|
.sort()
|
||||||
|
.reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
acc[key] = value[key];
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, V>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonEqual<A, B>(a: A, b: B): boolean {
|
||||||
|
try {
|
||||||
|
return jsonStableStringify(a) === jsonStableStringify(b);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
class Store<T extends Record<string, any>> {
|
||||||
|
private value: T;
|
||||||
|
private readonly initial: T;
|
||||||
|
private listeners = new Set<Listener<T>>();
|
||||||
|
private lastHash = '';
|
||||||
|
|
||||||
|
constructor(initial: T) {
|
||||||
|
this.value = initial;
|
||||||
|
this.initial = structuredClone(initial);
|
||||||
|
this.lastHash = jsonStableStringify(initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): T {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(next: Partial<T>): void {
|
||||||
|
const prev = this.value;
|
||||||
|
const merged = { ...prev, ...next };
|
||||||
|
|
||||||
|
if (jsonEqual(prev, merged)) return;
|
||||||
|
|
||||||
|
this.value = merged;
|
||||||
|
this.lastHash = jsonStableStringify(merged);
|
||||||
|
|
||||||
|
const diff: Partial<T> = {};
|
||||||
|
for (const key in merged) {
|
||||||
|
if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
const prev = this.value;
|
||||||
|
const next = structuredClone(this.initial);
|
||||||
|
|
||||||
|
if (jsonEqual(prev, next)) return;
|
||||||
|
|
||||||
|
this.value = next;
|
||||||
|
this.lastHash = jsonStableStringify(next);
|
||||||
|
|
||||||
|
const diff: Partial<T> = {};
|
||||||
|
for (const key in next) {
|
||||||
|
if (!jsonEqual(next[key], prev[key])) diff[key] = next[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.forEach((cb) => cb(this.value, prev, diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(cb: Listener<T>): () => void {
|
||||||
|
this.listeners.add(cb);
|
||||||
|
cb(this.value, this.value, {});
|
||||||
|
return () => this.listeners.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(cb: Listener<T>): void {
|
||||||
|
this.listeners.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
patch<K extends keyof T>(key: K, value: T[K]): void {
|
||||||
|
this.set({ [key]: value } as unknown as Partial<T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
getKey<K extends keyof T>(key: K): T[K] {
|
||||||
|
return this.value[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeKey<K extends keyof T>(
|
||||||
|
key: K,
|
||||||
|
cb: (value: T[K]) => void,
|
||||||
|
): () => void {
|
||||||
|
let prev = this.value[key];
|
||||||
|
const wrapper: Listener<T> = (val) => {
|
||||||
|
if (!jsonEqual(val[key], prev)) {
|
||||||
|
prev = val[key];
|
||||||
|
cb(val[key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.listeners.add(wrapper);
|
||||||
|
return () => this.listeners.delete(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreType {
|
||||||
|
tabService: {
|
||||||
|
current: string;
|
||||||
|
all: string[];
|
||||||
|
};
|
||||||
|
bandwidthWidget: {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
data: { up: number; down: number };
|
||||||
|
};
|
||||||
|
trafficTotalWidget: {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
data: { downloadTotal: number; uploadTotal: number };
|
||||||
|
};
|
||||||
|
systemInfoWidget: {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
data: { connections: number; memory: number };
|
||||||
|
};
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
data: { singbox: number; podkop: number };
|
||||||
|
};
|
||||||
|
sectionsWidget: {
|
||||||
|
loading: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
data: Podkop.OutboundGroup[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialStore: StoreType = {
|
||||||
|
tabService: {
|
||||||
|
current: '',
|
||||||
|
all: [],
|
||||||
|
},
|
||||||
|
bandwidthWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: { up: 0, down: 0 },
|
||||||
|
},
|
||||||
|
trafficTotalWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: { downloadTotal: 0, uploadTotal: 0 },
|
||||||
|
},
|
||||||
|
systemInfoWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: { connections: 0, memory: 0 },
|
||||||
|
},
|
||||||
|
servicesInfoWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: { singbox: 0, podkop: 0 },
|
||||||
|
},
|
||||||
|
sectionsWidget: {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const store = new Store<StoreType>(initialStore);
|
||||||
@@ -23,4 +23,155 @@ export const GlobalStyles = `
|
|||||||
#cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra {
|
#cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cbi-podkop-main-_status > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard styles */
|
||||||
|
|
||||||
|
.pdk_dashboard-page {
|
||||||
|
width: 100%;
|
||||||
|
--dashboard-grid-columns: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.pdk_dashboard-page {
|
||||||
|
--dashboard-grid-columns: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item {
|
||||||
|
border: 2px var(--background-color-low, lightgray) solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__title {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value {
|
||||||
|
color: var(--success-color-medium, green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value {
|
||||||
|
color: var(--error-color-medium, red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row__key {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__widgets-section__item__row__value {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-section {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 2px var(--background-color-low, lightgray) solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-section__title-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-section__title-section__title {
|
||||||
|
color: var(--text-color-high);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item {
|
||||||
|
border: 2px var(--background-color-low, lightgray) solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
transition: border 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item--selectable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item--selectable:hover {
|
||||||
|
border-color: var(--primary-color-high, dodgerblue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item--active {
|
||||||
|
border-color: var(--success-color-medium, green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__type {}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__latency--empty {
|
||||||
|
color: var(--primary-color-low, lightgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__latency--green {
|
||||||
|
color: var(--success-color-medium, green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__latency--yellow {
|
||||||
|
color: var(--warn-color-medium, orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdk_dashboard-page__outbound-grid__item__latency--red {
|
||||||
|
color: var(--error-color-medium, red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skeleton styles*/
|
||||||
|
.skeleton {
|
||||||
|
background-color: var(--background-color-low, #e0e0e0);
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -150%;
|
||||||
|
width: 150%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.4),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
animation: skeleton-shimmer 1.6s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skeleton-shimmer {
|
||||||
|
100% {
|
||||||
|
left: 150%;
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -4,20 +4,21 @@ import { ValidationResult } from './types';
|
|||||||
|
|
||||||
export function validateDNS(value: string): ValidationResult {
|
export function validateDNS(value: string): ValidationResult {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return { valid: false, message: 'DNS server address cannot be empty' };
|
return { valid: false, message: _('DNS server address cannot be empty') };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validateIPV4(value).valid) {
|
if (validateIPV4(value).valid) {
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validateDomain(value).valid) {
|
if (validateDomain(value).valid) {
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH',
|
'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function validateDomain(domain: string): ValidationResult {
|
|||||||
/^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/;
|
/^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/;
|
||||||
|
|
||||||
if (!domainRegex.test(domain)) {
|
if (!domainRegex.test(domain)) {
|
||||||
return { valid: false, message: 'Invalid domain address' };
|
return { valid: false, message: _('Invalid domain address') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostname = domain.split('/')[0];
|
const hostname = domain.split('/')[0];
|
||||||
@@ -14,8 +14,8 @@ export function validateDomain(domain: string): ValidationResult {
|
|||||||
const atLeastOneInvalidPart = parts.some((part) => part.length > 63);
|
const atLeastOneInvalidPart = parts.some((part) => part.length > 63);
|
||||||
|
|
||||||
if (atLeastOneInvalidPart) {
|
if (atLeastOneInvalidPart) {
|
||||||
return { valid: false, message: 'Invalid domain address' };
|
return { valid: false, message: _('Invalid domain address') };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export function validateIPV4(ip: string): ValidationResult {
|
|||||||
/^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
|
/^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
|
||||||
|
|
||||||
if (ipRegex.test(ip)) {
|
if (ipRegex.test(ip)) {
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: false, message: 'Invalid IP address' };
|
return { valid: false, message: _('Invalid IP address') };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ export function validateOutboundJson(value: string): ValidationResult {
|
|||||||
if (!parsed.type || !parsed.server || !parsed.server_port) {
|
if (!parsed.type || !parsed.server || !parsed.server_port) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Outbound JSON must contain at least "type", "server" and "server_port" fields',
|
'Outbound JSON must contain at least "type", "server" and "server_port" fields',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
} catch {
|
} catch {
|
||||||
return { valid: false, message: 'Invalid JSON format' };
|
return { valid: false, message: _('Invalid JSON format') };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export function validatePath(value: string): ValidationResult {
|
|||||||
if (!value) {
|
if (!value) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Path cannot be empty',
|
message: _('Path cannot be empty'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +19,8 @@ export function validatePath(value: string): ValidationResult {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Invalid path format. Path must start with "/" and contain valid characters',
|
'Invalid path format. Path must start with "/" and contain valid characters',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export function validateProxyUrl(url: string): ValidationResult {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'URL must start with vless:// or ss:// or trojan://',
|
message: _('URL must start with vless:// or ss:// or trojan://'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
|||||||
if (!url.startsWith('ss://')) {
|
if (!url.startsWith('ss://')) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid Shadowsocks URL: must start with ss://',
|
message: _('Invalid Shadowsocks URL: must start with ss://'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
|||||||
if (!url || /\s/.test(url)) {
|
if (!url || /\s/.test(url)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid Shadowsocks URL: must not contain spaces',
|
message: _('Invalid Shadowsocks URL: must not contain spaces'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
|||||||
if (!encryptedPart) {
|
if (!encryptedPart) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid Shadowsocks URL: missing credentials',
|
message: _('Invalid Shadowsocks URL: missing credentials'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,16 +34,18 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
|||||||
if (!decoded.includes(':')) {
|
if (!decoded.includes(':')) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Invalid Shadowsocks URL: decoded credentials must contain method:password',
|
'Invalid Shadowsocks URL: decoded credentials must contain method:password',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) {
|
if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Invalid Shadowsocks URL: missing method and password separator ":"',
|
'Invalid Shadowsocks URL: missing method and password separator ":"',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +55,7 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
|||||||
if (!serverPart) {
|
if (!serverPart) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid Shadowsocks URL: missing server address',
|
message: _('Invalid Shadowsocks URL: missing server address'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,14 +64,17 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
|||||||
if (!server) {
|
if (!server) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid Shadowsocks URL: missing server',
|
message: _('Invalid Shadowsocks URL: missing server'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
|
const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
|
||||||
|
|
||||||
if (!port) {
|
if (!port) {
|
||||||
return { valid: false, message: 'Invalid Shadowsocks URL: missing port' };
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: _('Invalid Shadowsocks URL: missing port'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const portNum = parseInt(port, 10);
|
const portNum = parseInt(port, 10);
|
||||||
@@ -77,12 +82,15 @@ export function validateShadowsocksUrl(url: string): ValidationResult {
|
|||||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid port number. Must be between 1 and 65535',
|
message: _('Invalid port number. Must be between 1 and 65535'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return { valid: false, message: 'Invalid Shadowsocks URL: parsing failed' };
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: _('Invalid Shadowsocks URL: parsing failed'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ export function validateSubnet(value: string): ValidationResult {
|
|||||||
if (!subnetRegex.test(value)) {
|
if (!subnetRegex.test(value)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid format. Use X.X.X.X or X.X.X.X/Y',
|
message: _('Invalid format. Use X.X.X.X or X.X.X.X/Y'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [ip, cidr] = value.split('/');
|
const [ip, cidr] = value.split('/');
|
||||||
|
|
||||||
if (ip === '0.0.0.0') {
|
if (ip === '0.0.0.0') {
|
||||||
return { valid: false, message: 'IP address 0.0.0.0 is not allowed' };
|
return { valid: false, message: _('IP address 0.0.0.0 is not allowed') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ipCheck = validateIPV4(ip);
|
const ipCheck = validateIPV4(ip);
|
||||||
@@ -30,10 +30,10 @@ export function validateSubnet(value: string): ValidationResult {
|
|||||||
if (cidrNum < 0 || cidrNum > 32) {
|
if (cidrNum < 0 || cidrNum > 32) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'CIDR must be between 0 and 32',
|
message: _('CIDR must be between 0 and 32'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ export function validateTrojanUrl(url: string): ValidationResult {
|
|||||||
if (!url.startsWith('trojan://')) {
|
if (!url.startsWith('trojan://')) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid Trojan URL: must start with trojan://',
|
message: _('Invalid Trojan URL: must start with trojan://'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url || /\s/.test(url)) {
|
if (!url || /\s/.test(url)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid Trojan URL: must not contain spaces',
|
message: _('Invalid Trojan URL: must not contain spaces'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,12 +22,14 @@ export function validateTrojanUrl(url: string): ValidationResult {
|
|||||||
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
|
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid Trojan URL: must contain username, hostname and port',
|
message: _(
|
||||||
|
'Invalid Trojan URL: must contain username, hostname and port',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return { valid: false, message: 'Invalid Trojan URL: parsing failed' };
|
return { valid: false, message: _('Invalid Trojan URL: parsing failed') };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export function validateUrl(
|
|||||||
if (!protocols.includes(parsedUrl.protocol)) {
|
if (!protocols.includes(parsedUrl.protocol)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: `URL must use one of the following protocols: ${protocols.join(', ')}`,
|
message: `${_('URL must use one of the following protocols:')} ${protocols.join(', ')}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return { valid: false, message: 'Invalid URL format' };
|
return { valid: false, message: _('Invalid URL format') };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ValidationResult } from './types';
|
import { ValidationResult } from './types';
|
||||||
|
|
||||||
// TODO refactor current validation and add tests
|
|
||||||
export function validateVlessUrl(url: string): ValidationResult {
|
export function validateVlessUrl(url: string): ValidationResult {
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
@@ -8,27 +7,27 @@ export function validateVlessUrl(url: string): ValidationResult {
|
|||||||
if (!url || /\s/.test(url)) {
|
if (!url || /\s/.test(url)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid VLESS URL: must not contain spaces',
|
message: _('Invalid VLESS URL: must not contain spaces'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedUrl.protocol !== 'vless:') {
|
if (parsedUrl.protocol !== 'vless:') {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid VLESS URL: must start with vless://',
|
message: _('Invalid VLESS URL: must start with vless://'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsedUrl.username) {
|
if (!parsedUrl.username) {
|
||||||
return { valid: false, message: 'Invalid VLESS URL: missing UUID' };
|
return { valid: false, message: _('Invalid VLESS URL: missing UUID') };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsedUrl.hostname) {
|
if (!parsedUrl.hostname) {
|
||||||
return { valid: false, message: 'Invalid VLESS URL: missing server' };
|
return { valid: false, message: _('Invalid VLESS URL: missing server') };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsedUrl.port) {
|
if (!parsedUrl.port) {
|
||||||
return { valid: false, message: 'Invalid VLESS URL: missing port' };
|
return { valid: false, message: _('Invalid VLESS URL: missing port') };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -38,15 +37,16 @@ export function validateVlessUrl(url: string): ValidationResult {
|
|||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Invalid VLESS URL: invalid port number. Must be between 1 and 65535',
|
'Invalid VLESS URL: invalid port number. Must be between 1 and 65535',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsedUrl.search) {
|
if (!parsedUrl.search) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Invalid VLESS URL: missing query parameters',
|
message: _('Invalid VLESS URL: missing query parameters'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +68,9 @@ export function validateVlessUrl(url: string): ValidationResult {
|
|||||||
if (!type || !validTypes.includes(type)) {
|
if (!type || !validTypes.includes(type)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws',
|
'Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +80,9 @@ export function validateVlessUrl(url: string): ValidationResult {
|
|||||||
if (!security || !validSecurities.includes(security)) {
|
if (!security || !validSecurities.includes(security)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Invalid VLESS URL: security must be one of tls, reality, none',
|
'Invalid VLESS URL: security must be one of tls, reality, none',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,21 +90,23 @@ export function validateVlessUrl(url: string): ValidationResult {
|
|||||||
if (!params.get('pbk')) {
|
if (!params.get('pbk')) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Invalid VLESS URL: missing pbk parameter for reality security',
|
'Invalid VLESS URL: missing pbk parameter for reality security',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!params.get('fp')) {
|
if (!params.get('fp')) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message:
|
message: _(
|
||||||
'Invalid VLESS URL: missing fp parameter for reality security',
|
'Invalid VLESS URL: missing fp parameter for reality security',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, message: 'Valid' };
|
return { valid: true, message: _('Valid') };
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return { valid: false, message: 'Invalid VLESS URL: parsing failed' };
|
return { valid: false, message: _('Invalid VLESS URL: parsing failed') };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
fe-app-podkop/tests/setup/global-mocks.ts
Normal file
2
fe-app-podkop/tests/setup/global-mocks.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// tests/setup/global-mocks.ts
|
||||||
|
globalThis._ = (key: string) => key;
|
||||||
@@ -4,5 +4,6 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
|
setupFiles: ['./tests/setup/global-mocks.ts'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
82
fe-app-podkop/watch-upload.js
Normal file
82
fe-app-podkop/watch-upload.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import chokidar from 'chokidar';
|
||||||
|
import SFTPClient from 'ssh2-sftp-client';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { glob } from 'glob';
|
||||||
|
|
||||||
|
const sftp = new SFTPClient();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
host: process.env.SFTP_HOST,
|
||||||
|
port: Number(process.env.SFTP_PORT || 22),
|
||||||
|
username: process.env.SFTP_USER,
|
||||||
|
...(process.env.SFTP_PRIVATE_KEY
|
||||||
|
? { privateKey: fs.readFileSync(process.env.SFTP_PRIVATE_KEY) }
|
||||||
|
: { password: process.env.SFTP_PASS }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const localDir = path.resolve(process.env.LOCAL_DIR || './dist');
|
||||||
|
const remoteDir = process.env.REMOTE_DIR || '/www/luci-static/mypkg';
|
||||||
|
|
||||||
|
async function uploadFile(filePath) {
|
||||||
|
const relativePath = path.relative(localDir, filePath);
|
||||||
|
const remotePath = path.posix.join(remoteDir, relativePath);
|
||||||
|
|
||||||
|
console.log(`Uploading: ${relativePath} -> ${remotePath}`);
|
||||||
|
try {
|
||||||
|
await sftp.fastPut(filePath, remotePath);
|
||||||
|
console.log(`Uploaded: ${relativePath}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed: ${relativePath}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFile(filePath) {
|
||||||
|
const relativePath = path.relative(localDir, filePath);
|
||||||
|
const remotePath = path.posix.join(remoteDir, relativePath);
|
||||||
|
|
||||||
|
console.log(`Removing: ${relativePath}`);
|
||||||
|
try {
|
||||||
|
await sftp.delete(remotePath);
|
||||||
|
console.log(`Removed: ${relativePath}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Could not delete ${relativePath}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadAllFiles() {
|
||||||
|
console.log('Uploading all files from', localDir);
|
||||||
|
|
||||||
|
const files = await glob(`${localDir}/**/*`, { nodir: true });
|
||||||
|
for (const file of files) {
|
||||||
|
await uploadFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Initial upload complete!');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await sftp.connect(config);
|
||||||
|
console.log(`Connected to ${config.host}`);
|
||||||
|
|
||||||
|
await uploadAllFiles();
|
||||||
|
|
||||||
|
chokidar
|
||||||
|
.watch(localDir, { ignoreInitial: true })
|
||||||
|
.on('all', async (event, filePath) => {
|
||||||
|
if (event === 'add' || event === 'change') {
|
||||||
|
await uploadFile(filePath);
|
||||||
|
} else if (event === 'unlink') {
|
||||||
|
await deleteFile(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log('Disconnecting...');
|
||||||
|
await sftp.end();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
@@ -221,6 +221,18 @@
|
|||||||
resolved "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
|
resolved "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
|
||||||
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
|
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
|
||||||
|
|
||||||
|
"@isaacs/balanced-match@^4.0.1":
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29"
|
||||||
|
integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==
|
||||||
|
|
||||||
|
"@isaacs/brace-expansion@^5.0.0":
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3"
|
||||||
|
integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==
|
||||||
|
dependencies:
|
||||||
|
"@isaacs/balanced-match" "^4.0.1"
|
||||||
|
|
||||||
"@isaacs/cliui@^8.0.2":
|
"@isaacs/cliui@^8.0.2":
|
||||||
version "8.0.2"
|
version "8.0.2"
|
||||||
resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
|
resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
|
||||||
@@ -628,6 +640,13 @@ argparse@^2.0.1:
|
|||||||
resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||||
|
|
||||||
|
asn1@^0.2.6:
|
||||||
|
version "0.2.6"
|
||||||
|
resolved "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
|
||||||
|
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
|
||||||
|
dependencies:
|
||||||
|
safer-buffer "~2.1.0"
|
||||||
|
|
||||||
assertion-error@^2.0.1:
|
assertion-error@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
|
resolved "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
|
||||||
@@ -638,6 +657,13 @@ balanced-match@^1.0.0:
|
|||||||
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
|
bcrypt-pbkdf@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
||||||
|
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
|
||||||
|
dependencies:
|
||||||
|
tweetnacl "^0.14.3"
|
||||||
|
|
||||||
brace-expansion@^1.1.7:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.12"
|
version "1.1.12"
|
||||||
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
|
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
|
||||||
@@ -660,6 +686,16 @@ braces@^3.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.1.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
|
buffer-from@^1.0.0:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||||
|
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||||
|
|
||||||
|
buildcheck@~0.0.6:
|
||||||
|
version "0.0.6"
|
||||||
|
resolved "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238"
|
||||||
|
integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==
|
||||||
|
|
||||||
bundle-require@^5.1.0:
|
bundle-require@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee"
|
resolved "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee"
|
||||||
@@ -701,7 +737,7 @@ check-error@^2.1.1:
|
|||||||
resolved "https://registry.npmmirror.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
|
resolved "https://registry.npmmirror.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
|
||||||
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
|
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
|
||||||
|
|
||||||
chokidar@^4.0.3:
|
chokidar@4.0.3, chokidar@^4.0.3:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
|
resolved "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
|
||||||
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
|
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
|
||||||
@@ -730,6 +766,16 @@ concat-map@0.0.1:
|
|||||||
resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||||
|
|
||||||
|
concat-stream@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1"
|
||||||
|
integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==
|
||||||
|
dependencies:
|
||||||
|
buffer-from "^1.0.0"
|
||||||
|
inherits "^2.0.3"
|
||||||
|
readable-stream "^3.0.2"
|
||||||
|
typedarray "^0.0.6"
|
||||||
|
|
||||||
confbox@^0.1.8:
|
confbox@^0.1.8:
|
||||||
version "0.1.8"
|
version "0.1.8"
|
||||||
resolved "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06"
|
resolved "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06"
|
||||||
@@ -740,6 +786,14 @@ consola@^3.4.0:
|
|||||||
resolved "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7"
|
resolved "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7"
|
||||||
integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==
|
integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==
|
||||||
|
|
||||||
|
cpu-features@~0.0.10:
|
||||||
|
version "0.0.10"
|
||||||
|
resolved "https://registry.npmmirror.com/cpu-features/-/cpu-features-0.0.10.tgz#9aae536db2710c7254d7ed67cb3cbc7d29ad79c5"
|
||||||
|
integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==
|
||||||
|
dependencies:
|
||||||
|
buildcheck "~0.0.6"
|
||||||
|
nan "^2.19.0"
|
||||||
|
|
||||||
cross-spawn@^7.0.6:
|
cross-spawn@^7.0.6:
|
||||||
version "7.0.6"
|
version "7.0.6"
|
||||||
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||||
@@ -766,6 +820,11 @@ deep-is@^0.1.3:
|
|||||||
resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||||
|
|
||||||
|
dotenv@17.2.3:
|
||||||
|
version "17.2.3"
|
||||||
|
resolved "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2"
|
||||||
|
integrity sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==
|
||||||
|
|
||||||
eastasianwidth@^0.2.0:
|
eastasianwidth@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||||
@@ -1014,7 +1073,7 @@ flatted@^3.2.9:
|
|||||||
resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
|
resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
|
||||||
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
||||||
|
|
||||||
foreground-child@^3.1.0:
|
foreground-child@^3.1.0, foreground-child@^3.3.1:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
|
resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
|
||||||
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
|
integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
|
||||||
@@ -1041,6 +1100,18 @@ glob-parent@^6.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob "^4.0.3"
|
is-glob "^4.0.3"
|
||||||
|
|
||||||
|
glob@11.0.3:
|
||||||
|
version "11.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6"
|
||||||
|
integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==
|
||||||
|
dependencies:
|
||||||
|
foreground-child "^3.3.1"
|
||||||
|
jackspeak "^4.1.1"
|
||||||
|
minimatch "^10.0.3"
|
||||||
|
minipass "^7.1.2"
|
||||||
|
package-json-from-dist "^1.0.0"
|
||||||
|
path-scurry "^2.0.0"
|
||||||
|
|
||||||
glob@^10.3.10:
|
glob@^10.3.10:
|
||||||
version "10.4.5"
|
version "10.4.5"
|
||||||
resolved "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
|
resolved "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
|
||||||
@@ -1091,6 +1162,11 @@ imurmurhash@^0.1.4:
|
|||||||
resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||||
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
|
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
|
||||||
|
|
||||||
|
inherits@^2.0.3:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
is-extglob@^2.1.1:
|
is-extglob@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||||
@@ -1127,6 +1203,13 @@ jackspeak@^3.1.2:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@pkgjs/parseargs" "^0.11.0"
|
"@pkgjs/parseargs" "^0.11.0"
|
||||||
|
|
||||||
|
jackspeak@^4.1.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae"
|
||||||
|
integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==
|
||||||
|
dependencies:
|
||||||
|
"@isaacs/cliui" "^8.0.2"
|
||||||
|
|
||||||
joycon@^3.1.1:
|
joycon@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
resolved "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
||||||
@@ -1216,6 +1299,11 @@ lru-cache@^10.2.0:
|
|||||||
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||||
|
|
||||||
|
lru-cache@^11.0.0:
|
||||||
|
version "11.2.2"
|
||||||
|
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24"
|
||||||
|
integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==
|
||||||
|
|
||||||
magic-string@^0.30.17:
|
magic-string@^0.30.17:
|
||||||
version "0.30.19"
|
version "0.30.19"
|
||||||
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9"
|
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9"
|
||||||
@@ -1236,6 +1324,13 @@ micromatch@^4.0.8:
|
|||||||
braces "^3.0.3"
|
braces "^3.0.3"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
|
minimatch@^10.0.3:
|
||||||
|
version "10.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa"
|
||||||
|
integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==
|
||||||
|
dependencies:
|
||||||
|
"@isaacs/brace-expansion" "^5.0.0"
|
||||||
|
|
||||||
minimatch@^3.1.2:
|
minimatch@^3.1.2:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||||
@@ -1279,6 +1374,11 @@ mz@^2.7.0:
|
|||||||
object-assign "^4.0.1"
|
object-assign "^4.0.1"
|
||||||
thenify-all "^1.0.0"
|
thenify-all "^1.0.0"
|
||||||
|
|
||||||
|
nan@^2.19.0, nan@^2.23.0:
|
||||||
|
version "2.23.0"
|
||||||
|
resolved "https://registry.npmmirror.com/nan/-/nan-2.23.0.tgz#24aa4ddffcc37613a2d2935b97683c1ec96093c6"
|
||||||
|
integrity sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==
|
||||||
|
|
||||||
nanoid@^3.3.11:
|
nanoid@^3.3.11:
|
||||||
version "3.3.11"
|
version "3.3.11"
|
||||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||||
@@ -1350,6 +1450,14 @@ path-scurry@^1.11.1:
|
|||||||
lru-cache "^10.2.0"
|
lru-cache "^10.2.0"
|
||||||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||||
|
|
||||||
|
path-scurry@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580"
|
||||||
|
integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==
|
||||||
|
dependencies:
|
||||||
|
lru-cache "^11.0.0"
|
||||||
|
minipass "^7.1.2"
|
||||||
|
|
||||||
pathe@^2.0.1, pathe@^2.0.3:
|
pathe@^2.0.1, pathe@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
|
resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
|
||||||
@@ -1425,6 +1533,15 @@ queue-microtask@^1.2.2:
|
|||||||
resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||||
|
|
||||||
|
readable-stream@^3.0.2:
|
||||||
|
version "3.6.2"
|
||||||
|
resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
|
||||||
|
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
|
||||||
|
dependencies:
|
||||||
|
inherits "^2.0.3"
|
||||||
|
string_decoder "^1.1.1"
|
||||||
|
util-deprecate "^1.0.1"
|
||||||
|
|
||||||
readdirp@^4.0.1:
|
readdirp@^4.0.1:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
|
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
|
||||||
@@ -1483,6 +1600,16 @@ run-parallel@^1.1.9:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask "^1.2.2"
|
queue-microtask "^1.2.2"
|
||||||
|
|
||||||
|
safe-buffer@~5.2.0:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||||
|
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||||
|
|
||||||
|
safer-buffer@~2.1.0:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
semver@^7.6.0:
|
semver@^7.6.0:
|
||||||
version "7.7.2"
|
version "7.7.2"
|
||||||
resolved "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
resolved "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
||||||
@@ -1522,6 +1649,25 @@ source-map@0.8.0-beta.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url "^7.0.0"
|
whatwg-url "^7.0.0"
|
||||||
|
|
||||||
|
ssh2-sftp-client@12.0.1:
|
||||||
|
version "12.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/ssh2-sftp-client/-/ssh2-sftp-client-12.0.1.tgz#926764878954dbed85f6f9233ce7980bfc94fdd4"
|
||||||
|
integrity sha512-ICJ1L2PmBel2Q2ctbyxzTFZCPKSHYYD6s2TFZv7NXmZDrDNGk8lHBb/SK2WgXLMXNANH78qoumeJzxlWZqSqWg==
|
||||||
|
dependencies:
|
||||||
|
concat-stream "^2.0.0"
|
||||||
|
ssh2 "^1.16.0"
|
||||||
|
|
||||||
|
ssh2@^1.16.0:
|
||||||
|
version "1.17.0"
|
||||||
|
resolved "https://registry.npmmirror.com/ssh2/-/ssh2-1.17.0.tgz#dc686e8e3abdbd4ad95d46fa139615903c12258c"
|
||||||
|
integrity sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==
|
||||||
|
dependencies:
|
||||||
|
asn1 "^0.2.6"
|
||||||
|
bcrypt-pbkdf "^1.0.2"
|
||||||
|
optionalDependencies:
|
||||||
|
cpu-features "~0.0.10"
|
||||||
|
nan "^2.23.0"
|
||||||
|
|
||||||
stackback@0.0.2:
|
stackback@0.0.2:
|
||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
|
resolved "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
|
||||||
@@ -1559,6 +1705,13 @@ string-width@^5.0.1, string-width@^5.1.2:
|
|||||||
emoji-regex "^9.2.2"
|
emoji-regex "^9.2.2"
|
||||||
strip-ansi "^7.0.1"
|
strip-ansi "^7.0.1"
|
||||||
|
|
||||||
|
string_decoder@^1.1.1:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||||
|
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||||
|
dependencies:
|
||||||
|
safe-buffer "~5.2.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
@@ -1711,6 +1864,11 @@ tsup@8.5.0:
|
|||||||
tinyglobby "^0.2.11"
|
tinyglobby "^0.2.11"
|
||||||
tree-kill "^1.2.2"
|
tree-kill "^1.2.2"
|
||||||
|
|
||||||
|
tweetnacl@^0.14.3:
|
||||||
|
version "0.14.5"
|
||||||
|
resolved "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||||
|
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
|
||||||
|
|
||||||
type-check@^0.4.0, type-check@~0.4.0:
|
type-check@^0.4.0, type-check@~0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||||
@@ -1718,6 +1876,11 @@ type-check@^0.4.0, type-check@~0.4.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls "^1.2.1"
|
prelude-ls "^1.2.1"
|
||||||
|
|
||||||
|
typedarray@^0.0.6:
|
||||||
|
version "0.0.6"
|
||||||
|
resolved "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
|
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||||
|
|
||||||
typescript-eslint@8.45.0:
|
typescript-eslint@8.45.0:
|
||||||
version "8.45.0"
|
version "8.45.0"
|
||||||
resolved "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz#98ab164234dc04c112747ec0a4ae29a94efe123b"
|
resolved "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz#98ab164234dc04c112747ec0a4ae29a94efe123b"
|
||||||
@@ -1745,6 +1908,11 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
|
util-deprecate@^1.0.1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
|
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||||
|
|
||||||
vite-node@3.2.4:
|
vite-node@3.2.4:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07"
|
resolved "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07"
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ function createAdditionalSection(mainSection) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = mainSection.taboption(
|
o = mainSection.taboption(
|
||||||
@@ -113,7 +113,7 @@ function createAdditionalSection(mainSection) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = mainSection.taboption(
|
o = mainSection.taboption(
|
||||||
@@ -342,7 +342,7 @@ function createAdditionalSection(mainSection) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = mainSection.taboption(
|
o = mainSection.taboption(
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return `${_('Invalid URL format:')} ${e?.message}`;
|
return `${_('Invalid URL format:')} ${e?.message}`;
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.taboption(
|
o = s.taboption(
|
||||||
@@ -204,7 +204,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.taboption(
|
o = s.taboption(
|
||||||
@@ -315,7 +315,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.taboption(
|
o = s.taboption(
|
||||||
@@ -459,7 +459,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.taboption(
|
o = s.taboption(
|
||||||
@@ -538,7 +538,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.taboption(
|
o = s.taboption(
|
||||||
@@ -575,7 +575,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.taboption(
|
o = s.taboption(
|
||||||
@@ -612,7 +612,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.taboption(
|
o = s.taboption(
|
||||||
@@ -654,7 +654,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.taboption(
|
o = s.taboption(
|
||||||
@@ -733,7 +733,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
o = s.taboption(
|
o = s.taboption(
|
||||||
@@ -772,7 +772,7 @@ function createConfigSection(section) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _(validation.message);
|
return validation.message;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
'use strict';
|
||||||
|
'require baseclass';
|
||||||
|
'require form';
|
||||||
|
'require ui';
|
||||||
|
'require uci';
|
||||||
|
'require fs';
|
||||||
|
'require view.podkop.utils as utils';
|
||||||
|
'require view.podkop.main as main';
|
||||||
|
|
||||||
|
function createDashboardSection(mainSection) {
|
||||||
|
let o = mainSection.tab('dashboard', _('Dashboard'));
|
||||||
|
|
||||||
|
o = mainSection.taboption('dashboard', form.DummyValue, '_status');
|
||||||
|
o.rawhtml = true;
|
||||||
|
o.cfgvalue = () => {
|
||||||
|
main.initDashboardController();
|
||||||
|
|
||||||
|
return main.renderDashboard();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryPoint = {
|
||||||
|
createDashboardSection,
|
||||||
|
};
|
||||||
|
|
||||||
|
return baseclass.extend(EntryPoint);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
|||||||
'require view.podkop.configSection as configSection';
|
'require view.podkop.configSection as configSection';
|
||||||
'require view.podkop.diagnosticTab as diagnosticTab';
|
'require view.podkop.diagnosticTab as diagnosticTab';
|
||||||
'require view.podkop.additionalTab as additionalTab';
|
'require view.podkop.additionalTab as additionalTab';
|
||||||
|
'require view.podkop.dashboardTab as dashboardTab';
|
||||||
'require view.podkop.utils as utils';
|
'require view.podkop.utils as utils';
|
||||||
'require view.podkop.main as main';
|
'require view.podkop.main as main';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ const EntryNode = {
|
|||||||
// Main Section
|
// Main Section
|
||||||
const mainSection = podkopFormMap.section(form.TypedSection, 'main');
|
const mainSection = podkopFormMap.section(form.TypedSection, 'main');
|
||||||
mainSection.anonymous = true;
|
mainSection.anonymous = true;
|
||||||
|
|
||||||
configSection.createConfigSection(mainSection);
|
configSection.createConfigSection(mainSection);
|
||||||
|
|
||||||
// Additional Settings Tab (main section)
|
// Additional Settings Tab (main section)
|
||||||
@@ -24,7 +26,7 @@ const EntryNode = {
|
|||||||
|
|
||||||
// Diagnostics Tab (main section)
|
// Diagnostics Tab (main section)
|
||||||
diagnosticTab.createDiagnosticsSection(mainSection);
|
diagnosticTab.createDiagnosticsSection(mainSection);
|
||||||
const podkopFormMapPromise = podkopFormMap.render().then(node => {
|
const podkopFormMapPromise = podkopFormMap.render().then((node) => {
|
||||||
// Set up diagnostics event handlers
|
// Set up diagnostics event handlers
|
||||||
diagnosticTab.setupDiagnosticsEventHandlers(node);
|
diagnosticTab.setupDiagnosticsEventHandlers(node);
|
||||||
|
|
||||||
@@ -56,15 +58,25 @@ const EntryNode = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Extra Section
|
// Extra Section
|
||||||
const extraSection = podkopFormMap.section(form.TypedSection, 'extra', _('Extra configurations'));
|
const extraSection = podkopFormMap.section(
|
||||||
|
form.TypedSection,
|
||||||
|
'extra',
|
||||||
|
_('Extra configurations'),
|
||||||
|
);
|
||||||
extraSection.anonymous = false;
|
extraSection.anonymous = false;
|
||||||
extraSection.addremove = true;
|
extraSection.addremove = true;
|
||||||
extraSection.addbtntitle = _('Add Section');
|
extraSection.addbtntitle = _('Add Section');
|
||||||
extraSection.multiple = true;
|
extraSection.multiple = true;
|
||||||
configSection.createConfigSection(extraSection);
|
configSection.createConfigSection(extraSection);
|
||||||
|
|
||||||
|
// Initial dashboard render
|
||||||
|
dashboardTab.createDashboardSection(mainSection);
|
||||||
|
|
||||||
|
// Inject core service
|
||||||
|
main.coreService();
|
||||||
|
|
||||||
return podkopFormMapPromise;
|
return podkopFormMapPromise;
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
return view.extend(EntryNode);
|
return view.extend(EntryNode);
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PODKOP\n"
|
"Project-Id-Version: PODKOP\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-10-02 19:37+0500\n"
|
"POT-Creation-Date: 2025-10-07 16:55+0300\n"
|
||||||
"PO-Revision-Date: 2025-09-30 15:18+0500\n"
|
"PO-Revision-Date: 2025-10-07 23:45+0300\n"
|
||||||
"Last-Translator: Automatically generated\n"
|
"Last-Translator: Automatically generated\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
@@ -17,171 +17,6 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
|
|
||||||
msgid "Additional Settings"
|
|
||||||
msgstr "Дополнительные настройки"
|
|
||||||
|
|
||||||
msgid "Yacd enable"
|
|
||||||
msgstr "Включить Yacd"
|
|
||||||
|
|
||||||
msgid "Exclude NTP"
|
|
||||||
msgstr "Исключить NTP"
|
|
||||||
|
|
||||||
msgid "Allows you to exclude NTP protocol traffic from the tunnel"
|
|
||||||
msgstr "Позволяет исключить направление трафика NTP-протокола в туннель"
|
|
||||||
|
|
||||||
msgid "QUIC disable"
|
|
||||||
msgstr "Отключить QUIC"
|
|
||||||
|
|
||||||
msgid "For issues with the video stream"
|
|
||||||
msgstr "Для проблем с видеопотоком"
|
|
||||||
|
|
||||||
msgid "List Update Frequency"
|
|
||||||
msgstr "Частота обновления списков"
|
|
||||||
|
|
||||||
msgid "Select how often the lists will be updated"
|
|
||||||
msgstr "Выберите как часто будут обновляться списки"
|
|
||||||
|
|
||||||
msgid "DNS Protocol Type"
|
|
||||||
msgstr "Тип DNS протокола"
|
|
||||||
|
|
||||||
msgid "Select DNS protocol to use"
|
|
||||||
msgstr "Выберите протокол DNS"
|
|
||||||
|
|
||||||
msgid "DNS over HTTPS (DoH)"
|
|
||||||
msgstr "DNS через HTTPS (DoH)"
|
|
||||||
|
|
||||||
msgid "DNS over TLS (DoT)"
|
|
||||||
msgstr "DNS через TLS (DoT)"
|
|
||||||
|
|
||||||
msgid "UDP (Unprotected DNS)"
|
|
||||||
msgstr "UDP (Незащищённый DNS)"
|
|
||||||
|
|
||||||
msgid "DNS Server"
|
|
||||||
msgstr "DNS-сервер"
|
|
||||||
|
|
||||||
msgid "Select or enter DNS server address"
|
|
||||||
msgstr "Выберите или введите адрес DNS-сервера"
|
|
||||||
|
|
||||||
msgid "DNS server address cannot be empty"
|
|
||||||
msgstr "Адрес DNS-сервера не может быть пустым"
|
|
||||||
|
|
||||||
msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH"
|
|
||||||
msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8 или dns.example.com или dns.example.com/nicedns для DoH"
|
|
||||||
|
|
||||||
msgid "Bootstrap DNS server"
|
|
||||||
msgstr "Bootstrap DNS-сервер"
|
|
||||||
|
|
||||||
msgid "The DNS server used to look up the IP address of an upstream DNS server"
|
|
||||||
msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера"
|
|
||||||
|
|
||||||
msgid "Invalid DNS server format. Example: 8.8.8.8"
|
|
||||||
msgstr "Неверный формат DNS-сервера. Пример: 8.8.8.8"
|
|
||||||
|
|
||||||
msgid "DNS Rewrite TTL"
|
|
||||||
msgstr "Перезапись TTL для DNS"
|
|
||||||
|
|
||||||
msgid "Time in seconds for DNS record caching (default: 60)"
|
|
||||||
msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)"
|
|
||||||
|
|
||||||
msgid "TTL value cannot be empty"
|
|
||||||
msgstr "Значение TTL не может быть пустым"
|
|
||||||
|
|
||||||
msgid "TTL must be a positive number"
|
|
||||||
msgstr "TTL должно быть положительным числом"
|
|
||||||
|
|
||||||
msgid "Config File Path"
|
|
||||||
msgstr "Путь к файлу конфигурации"
|
|
||||||
|
|
||||||
msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing"
|
|
||||||
msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
|
|
||||||
|
|
||||||
msgid "Cache File Path"
|
|
||||||
msgstr "Путь к файлу кэша"
|
|
||||||
|
|
||||||
msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing"
|
|
||||||
msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
|
|
||||||
|
|
||||||
msgid "Cache file path cannot be empty"
|
|
||||||
msgstr "Путь к файлу кэша не может быть пустым"
|
|
||||||
|
|
||||||
msgid "Path must be absolute (start with /)"
|
|
||||||
msgstr "Путь должен быть абсолютным (начинаться с /)"
|
|
||||||
|
|
||||||
msgid "Path must end with cache.db"
|
|
||||||
msgstr "Путь должен заканчиваться на cache.db"
|
|
||||||
|
|
||||||
msgid "Path must contain at least one directory (like /tmp/cache.db)"
|
|
||||||
msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)"
|
|
||||||
|
|
||||||
msgid "Source Network Interface"
|
|
||||||
msgstr "Сетевой интерфейс источника"
|
|
||||||
|
|
||||||
msgid "Select the network interface from which the traffic will originate"
|
|
||||||
msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик"
|
|
||||||
|
|
||||||
msgid "Interface monitoring"
|
|
||||||
msgstr "Мониторинг интерфейсов"
|
|
||||||
|
|
||||||
msgid "Interface monitoring for bad WAN"
|
|
||||||
msgstr "Мониторинг интерфейсов для плохого WAN"
|
|
||||||
|
|
||||||
msgid "Interface for monitoring"
|
|
||||||
msgstr "Интерфейс для мониторинга"
|
|
||||||
|
|
||||||
msgid "Select the WAN interfaces to be monitored"
|
|
||||||
msgstr "Выберите WAN интерфейсы для мониторинга"
|
|
||||||
|
|
||||||
msgid "Interface Monitoring Delay"
|
|
||||||
msgstr "Задержка при мониторинге интерфейсов"
|
|
||||||
|
|
||||||
msgid "Delay in milliseconds before reloading podkop after interface UP"
|
|
||||||
msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса"
|
|
||||||
|
|
||||||
msgid "Delay value cannot be empty"
|
|
||||||
msgstr "Значение задержки не может быть пустым"
|
|
||||||
|
|
||||||
msgid "Dont touch my DHCP!"
|
|
||||||
msgstr "Не трогать мой DHCP!"
|
|
||||||
|
|
||||||
msgid "Podkop will not change the DHCP config"
|
|
||||||
msgstr "Podkop не будет изменять конфигурацию DHCP"
|
|
||||||
|
|
||||||
msgid "Proxy download of lists"
|
|
||||||
msgstr "Загрузка списков через прокси"
|
|
||||||
|
|
||||||
msgid "Downloading all lists via main Proxy/VPN"
|
|
||||||
msgstr "Загрузка всех списков через основной прокси/VPN"
|
|
||||||
|
|
||||||
msgid "IP for exclusion"
|
|
||||||
msgstr "IP для исключения"
|
|
||||||
|
|
||||||
msgid "Specify local IP addresses that will never use the configured route"
|
|
||||||
msgstr "Укажите локальные IP-адреса, которые никогда не будут использовать настроенный маршрут"
|
|
||||||
|
|
||||||
msgid "Local IPs"
|
|
||||||
msgstr "Локальные IP адреса"
|
|
||||||
|
|
||||||
msgid "Enter valid IPv4 addresses"
|
|
||||||
msgstr "Введите действительные IPv4-адреса"
|
|
||||||
|
|
||||||
msgid "Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)"
|
|
||||||
msgstr "Неверный формат IP. Используйте формат: X.X.X.X (например: 192.168.1.1)"
|
|
||||||
|
|
||||||
msgid "IP address parts must be between 0 and 255"
|
|
||||||
msgstr "Части IP-адреса должны быть между 0 и 255"
|
|
||||||
|
|
||||||
msgid "Mixed enable"
|
|
||||||
msgstr "Включить смешанный режим"
|
|
||||||
|
|
||||||
msgid "Browser port: 2080"
|
|
||||||
msgstr "Порт браузера: 2080"
|
|
||||||
|
|
||||||
msgid "URL must use one of the following protocols: "
|
|
||||||
msgstr "URL должен использовать один из следующих протоколов: "
|
|
||||||
|
|
||||||
msgid "Invalid URL format"
|
|
||||||
msgstr "Неверный формат URL"
|
|
||||||
|
|
||||||
msgid "Basic Settings"
|
msgid "Basic Settings"
|
||||||
msgstr "Основные настройки"
|
msgstr "Основные настройки"
|
||||||
|
|
||||||
@@ -216,68 +51,15 @@ msgid "Config without description"
|
|||||||
msgstr "Конфигурация без описания"
|
msgstr "Конфигурация без описания"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup "
|
"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs"
|
||||||
"configs"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для "
|
"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для резервных конфигураций"
|
||||||
"сохранения других конфигураций"
|
|
||||||
|
|
||||||
msgid "No active configuration found. At least one non-commented line is required."
|
msgid "No active configuration found. One configuration is required."
|
||||||
msgstr "Активная конфигурация не найдена. Требуется хотя бы одна незакомментированная строка."
|
msgstr "Активная конфигурация не найдена. Требуется хотя бы одна незакомментированная строка."
|
||||||
|
|
||||||
msgid "URL must start with vless:// or ss://"
|
msgid "Multiply active configurations found. Please leave one configuration."
|
||||||
msgstr "URL должен начинаться с vless:// или ss://"
|
msgstr "Найдено несколько активных конфигураций. Оставьте только одну."
|
||||||
|
|
||||||
msgid "Invalid Shadowsocks URL format: missing method and password separator \":\""
|
|
||||||
msgstr "Неверный формат URL Shadowsocks: отсутствует разделитель метода и пароля \":\""
|
|
||||||
|
|
||||||
msgid "Invalid Shadowsocks URL format"
|
|
||||||
msgstr "Неверный формат URL Shadowsocks"
|
|
||||||
|
|
||||||
msgid "Invalid Shadowsocks URL: missing server address"
|
|
||||||
msgstr "Неверный URL Shadowsocks: отсутствует адрес сервера"
|
|
||||||
|
|
||||||
msgid "Invalid Shadowsocks URL: missing server"
|
|
||||||
msgstr "Неверный URL Shadowsocks: отсутствует сервер"
|
|
||||||
|
|
||||||
msgid "Invalid Shadowsocks URL: missing port"
|
|
||||||
msgstr "Неверный URL Shadowsocks: отсутствует порт"
|
|
||||||
|
|
||||||
msgid "Invalid port number. Must be between 1 and 65535"
|
|
||||||
msgstr "Неверный номер порта. Должен быть между 1 и 65535"
|
|
||||||
|
|
||||||
msgid "Invalid Shadowsocks URL: missing or invalid server/port format"
|
|
||||||
msgstr "Неверный URL Shadowsocks: отсутствует или неверный формат сервера/порта"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: missing UUID"
|
|
||||||
msgstr "Неверный URL VLESS: отсутствует UUID"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: missing server address"
|
|
||||||
msgstr "Неверный URL VLESS: отсутствует адрес сервера"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: missing server"
|
|
||||||
msgstr "Неверный URL VLESS: отсутствует сервер"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: missing port"
|
|
||||||
msgstr "Неверный URL VLESS: отсутствует порт"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: missing or invalid server/port format"
|
|
||||||
msgstr "Неверный URL VLESS: отсутствует или неверный формат сервера/порта"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: missing query parameters"
|
|
||||||
msgstr "Неверный URL VLESS: отсутствуют параметры запроса"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws"
|
|
||||||
msgstr "Неверный URL VLESS: тип должен быть одним из tcp, raw, udp, grpc, http, ws"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: security must be one of tls, reality, none"
|
|
||||||
msgstr "Неверный URL VLESS: security должен быть одним из tls, reality, none"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: missing pbk parameter for reality security"
|
|
||||||
msgstr "Неверный URL VLESS: отсутствует параметр pbk для security reality"
|
|
||||||
|
|
||||||
msgid "Invalid VLESS URL: missing fp parameter for reality security"
|
|
||||||
msgstr "Неверный URL VLESS: отсутствует параметр fp для security reality"
|
|
||||||
|
|
||||||
msgid "Invalid URL format:"
|
msgid "Invalid URL format:"
|
||||||
msgstr "Неверный формат URL:"
|
msgstr "Неверный формат URL:"
|
||||||
@@ -288,12 +70,6 @@ msgstr "Конфигурация исходящего соединения"
|
|||||||
msgid "Enter complete outbound configuration in JSON format"
|
msgid "Enter complete outbound configuration in JSON format"
|
||||||
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON"
|
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON"
|
||||||
|
|
||||||
msgid "JSON must contain at least type, server and server_port fields"
|
|
||||||
msgstr "JSON должен содержать как минимум поля type, server и server_port"
|
|
||||||
|
|
||||||
msgid "Invalid JSON format"
|
|
||||||
msgstr "Неверный формат JSON"
|
|
||||||
|
|
||||||
msgid "URLTest Proxy Links"
|
msgid "URLTest Proxy Links"
|
||||||
msgstr "Ссылки прокси для URLTest"
|
msgstr "Ссылки прокси для URLTest"
|
||||||
|
|
||||||
@@ -315,8 +91,26 @@ msgstr "Резолвер доменов"
|
|||||||
msgid "Enable built-in DNS resolver for domains handled by this section"
|
msgid "Enable built-in DNS resolver for domains handled by this section"
|
||||||
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе"
|
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе"
|
||||||
|
|
||||||
|
msgid "DNS Protocol Type"
|
||||||
|
msgstr "Тип протокола DNS"
|
||||||
|
|
||||||
msgid "Select the DNS protocol type for the domain resolver"
|
msgid "Select the DNS protocol type for the domain resolver"
|
||||||
msgstr "Выберите протокол DNS для резолвера доменов"
|
msgstr "Выберите тип протокола DNS для резолвера доменов"
|
||||||
|
|
||||||
|
msgid "DNS over HTTPS (DoH)"
|
||||||
|
msgstr "DNS через HTTPS (DoH)"
|
||||||
|
|
||||||
|
msgid "DNS over TLS (DoT)"
|
||||||
|
msgstr "DNS через TLS (DoT)"
|
||||||
|
|
||||||
|
msgid "UDP (Unprotected DNS)"
|
||||||
|
msgstr "UDP (Незащищённый DNS)"
|
||||||
|
|
||||||
|
msgid "DNS Server"
|
||||||
|
msgstr "DNS-сервер"
|
||||||
|
|
||||||
|
msgid "Select or enter DNS server address"
|
||||||
|
msgstr "Выберите или введите адрес DNS-сервера"
|
||||||
|
|
||||||
msgid "Community Lists"
|
msgid "Community Lists"
|
||||||
msgstr "Списки сообщества"
|
msgstr "Списки сообщества"
|
||||||
@@ -328,21 +122,16 @@ msgid "Select predefined service for routing"
|
|||||||
msgstr "Выберите предустановленные сервисы для маршрутизации"
|
msgstr "Выберите предустановленные сервисы для маршрутизации"
|
||||||
|
|
||||||
msgid "Regional options cannot be used together"
|
msgid "Regional options cannot be used together"
|
||||||
msgstr "Нельзя использовать несколько региональных опций"
|
msgstr "Нельзя использовать несколько региональных опций одновременно"
|
||||||
|
|
||||||
#, javascript-format
|
|
||||||
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
|
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
|
||||||
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
|
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
|
||||||
|
|
||||||
msgid "Russia inside restrictions"
|
msgid "Russia inside restrictions"
|
||||||
msgstr "Ограничения Russia inside"
|
msgstr "Ограничения Russia inside"
|
||||||
|
|
||||||
#, javascript-format
|
msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
|
||||||
msgid ""
|
msgstr "Внимание: «Russia inside» может использоваться только с %s. %s уже находится в «Russia inside» и был удалён из выбора."
|
||||||
"Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
|
|
||||||
msgstr ""
|
|
||||||
"Внимание: \"Russia inside\" может использоваться только с %s. %s уже находится в \"Russia inside\" и был удален из "
|
|
||||||
"выбора."
|
|
||||||
|
|
||||||
msgid "User Domain List Type"
|
msgid "User Domain List Type"
|
||||||
msgstr "Тип пользовательского списка доменов"
|
msgstr "Тип пользовательского списка доменов"
|
||||||
@@ -363,25 +152,19 @@ msgid "User Domains"
|
|||||||
msgstr "Пользовательские домены"
|
msgstr "Пользовательские домены"
|
||||||
|
|
||||||
msgid "Enter domain names without protocols (example: sub.example.com or example.com)"
|
msgid "Enter domain names without protocols (example: sub.example.com or example.com)"
|
||||||
msgstr "Введите доменные имена без указания протоколов (например: sub.example.com или example.com)"
|
msgstr "Введите доменные имена без протоколов (например: sub.example.com или example.com)"
|
||||||
|
|
||||||
msgid "Invalid domain format. Enter domain without protocol (example: sub.example.com or ru)"
|
|
||||||
msgstr "Введите имена доменов без протоколов (пример: sub.example.com или example.com)"
|
|
||||||
|
|
||||||
msgid "User Domains List"
|
msgid "User Domains List"
|
||||||
msgstr "Список пользовательских доменов"
|
msgstr "Список пользовательских доменов"
|
||||||
|
|
||||||
msgid "Enter domain names separated by comma, space or newline. You can add comments after //"
|
msgid "Enter domain names separated by comma, space or newline. You can add comments after //"
|
||||||
msgstr ""
|
msgstr "Введите домены через запятую, пробел или с новой строки. Можно добавлять комментарии после //"
|
||||||
"Введите имена доменов, разделяя их запятой, пробелом или с новой строки. Вы можете добавлять комментарии после //"
|
|
||||||
|
|
||||||
#, javascript-format
|
|
||||||
msgid "Invalid domain format: %s. Enter domain without protocol"
|
|
||||||
msgstr "Неверный формат домена: %s. Введите домен без протокола"
|
|
||||||
|
|
||||||
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
|
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
|
||||||
msgstr ""
|
msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается."
|
||||||
"Должен быть указан хотя бы один действительный домен. Содержимое, состоящее только из комментариев, не допускается."
|
|
||||||
|
msgid "Validation errors:"
|
||||||
|
msgstr "Ошибки валидации:"
|
||||||
|
|
||||||
msgid "Local Domain Lists"
|
msgid "Local Domain Lists"
|
||||||
msgstr "Локальные списки доменов"
|
msgstr "Локальные списки доменов"
|
||||||
@@ -395,17 +178,14 @@ msgstr "Пути к локальным спискам доменов"
|
|||||||
msgid "Enter the list file path"
|
msgid "Enter the list file path"
|
||||||
msgstr "Введите путь к файлу списка"
|
msgstr "Введите путь к файлу списка"
|
||||||
|
|
||||||
msgid "Invalid path format. Path must start with \"/\" and contain valid characters"
|
|
||||||
msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы"
|
|
||||||
|
|
||||||
msgid "Remote Domain Lists"
|
msgid "Remote Domain Lists"
|
||||||
msgstr "Удаленные списки доменов"
|
msgstr "Удалённые списки доменов"
|
||||||
|
|
||||||
msgid "Download and use domain lists from remote URLs"
|
msgid "Download and use domain lists from remote URLs"
|
||||||
msgstr "Загрузка и использование списков доменов с удаленных URL"
|
msgstr "Загружать и использовать списки доменов с удалённых URL"
|
||||||
|
|
||||||
msgid "Remote Domain URLs"
|
msgid "Remote Domain URLs"
|
||||||
msgstr "URL удаленных доменов"
|
msgstr "URL удалённых доменов"
|
||||||
|
|
||||||
msgid "Enter full URLs starting with http:// or https://"
|
msgid "Enter full URLs starting with http:// or https://"
|
||||||
msgstr "Введите полные URL, начинающиеся с http:// или https://"
|
msgstr "Введите полные URL, начинающиеся с http:// или https://"
|
||||||
@@ -423,58 +203,31 @@ msgid "Select how to add your custom subnets"
|
|||||||
msgstr "Выберите способ добавления пользовательских подсетей"
|
msgstr "Выберите способ добавления пользовательских подсетей"
|
||||||
|
|
||||||
msgid "Text List (comma/space/newline separated)"
|
msgid "Text List (comma/space/newline separated)"
|
||||||
msgstr "Текстовый список (разделенный запятыми/пробелами/новыми строками)"
|
msgstr "Текстовый список (через запятую, пробел или новую строку)"
|
||||||
|
|
||||||
msgid "User Subnets"
|
msgid "User Subnets"
|
||||||
msgstr "Пользовательские подсети"
|
msgstr "Пользовательские подсети"
|
||||||
|
|
||||||
msgid "Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses"
|
msgid "Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses"
|
||||||
msgstr "Введите подсети в нотации CIDR (пример: 103.21.244.0/22) или отдельные IP-адреса"
|
msgstr "Введите подсети в нотации CIDR (например: 103.21.244.0/22) или отдельные IP-адреса"
|
||||||
|
|
||||||
msgid "Invalid format. Use format: X.X.X.X or X.X.X.X/Y"
|
|
||||||
msgstr "Неверный формат. Используйте формат: X.X.X.X или X.X.X.X/Y"
|
|
||||||
|
|
||||||
msgid "IP address 0.0.0.0 is not allowed"
|
|
||||||
msgstr "IP адрес не может быть 0.0.0.0"
|
|
||||||
|
|
||||||
msgid "CIDR must be between 0 and 32"
|
|
||||||
msgstr "CIDR должен быть между 0 и 32"
|
|
||||||
|
|
||||||
msgid "User Subnets List"
|
msgid "User Subnets List"
|
||||||
msgstr "Список пользовательских подсетей"
|
msgstr "Список пользовательских подсетей"
|
||||||
|
|
||||||
msgid ""
|
msgid "Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //"
|
||||||
"Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments "
|
msgstr "Введите подсети в нотации CIDR или IP-адреса через запятую, пробел или новую строку. Можно добавлять комментарии после //"
|
||||||
"after //"
|
|
||||||
msgstr ""
|
|
||||||
"Введите подсети в нотации CIDR или отдельные IP-адреса, разделенные запятой, пробелом или новой строкой. Вы можете "
|
|
||||||
"добавлять комментарии после //"
|
|
||||||
|
|
||||||
#, javascript-format
|
|
||||||
msgid "Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y"
|
|
||||||
msgstr "Неверный формат: %s. Используйте формат: X.X.X.X или X.X.X.X/Y"
|
|
||||||
|
|
||||||
#, javascript-format
|
|
||||||
msgid "IP parts must be between 0 and 255 in: %s"
|
|
||||||
msgstr "Части IP-адреса должны быть между 0 и 255 в: %s"
|
|
||||||
|
|
||||||
#, javascript-format
|
|
||||||
msgid "CIDR must be between 0 and 32 in: %s"
|
|
||||||
msgstr "CIDR должен быть между 0 и 32 в: %s"
|
|
||||||
|
|
||||||
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
|
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
|
||||||
msgstr ""
|
msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы."
|
||||||
"Должна быть указана хотя бы одна действительная подсеть или IP. Содержимое, состоящее только из комментариев, не "
|
|
||||||
"допускается."
|
|
||||||
|
|
||||||
msgid "Remote Subnet Lists"
|
msgid "Remote Subnet Lists"
|
||||||
msgstr "Удаленные списки подсетей"
|
msgstr "Удалённые списки подсетей"
|
||||||
|
|
||||||
msgid "Download and use subnet lists from remote URLs"
|
msgid "Download and use subnet lists from remote URLs"
|
||||||
msgstr "Загрузка и использование списков подсетей с удаленных URL"
|
msgstr "Загружать и использовать списки подсетей с удалённых URL"
|
||||||
|
|
||||||
msgid "Remote Subnet URLs"
|
msgid "Remote Subnet URLs"
|
||||||
msgstr "URL удаленных подсетей"
|
msgstr "URL удалённых подсетей"
|
||||||
|
|
||||||
msgid "IP for full redirection"
|
msgid "IP for full redirection"
|
||||||
msgstr "IP для полного перенаправления"
|
msgstr "IP для полного перенаправления"
|
||||||
@@ -482,21 +235,219 @@ msgstr "IP для полного перенаправления"
|
|||||||
msgid "Specify local IP addresses whose traffic will always use the configured route"
|
msgid "Specify local IP addresses whose traffic will always use the configured route"
|
||||||
msgstr "Укажите локальные IP-адреса, трафик которых всегда будет использовать настроенный маршрут"
|
msgstr "Укажите локальные IP-адреса, трафик которых всегда будет использовать настроенный маршрут"
|
||||||
|
|
||||||
|
msgid "Local IPs"
|
||||||
|
msgstr "Локальные IP-адреса"
|
||||||
|
|
||||||
|
msgid "Enter valid IPv4 addresses"
|
||||||
|
msgstr "Введите действительные IPv4-адреса"
|
||||||
|
|
||||||
|
msgid "Extra configurations"
|
||||||
|
msgstr "Дополнительные конфигурации"
|
||||||
|
|
||||||
|
msgid "Add Section"
|
||||||
|
msgstr "Добавить раздел"
|
||||||
|
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr "Дашборд"
|
||||||
|
|
||||||
|
msgid "Valid"
|
||||||
|
msgstr "Валидно"
|
||||||
|
|
||||||
|
msgid "Invalid IP address"
|
||||||
|
msgstr "Неверный IP-адрес"
|
||||||
|
|
||||||
|
msgid "Invalid domain address"
|
||||||
|
msgstr "Неверный домен"
|
||||||
|
|
||||||
|
msgid "DNS server address cannot be empty"
|
||||||
|
msgstr "Адрес DNS-сервера не может быть пустым"
|
||||||
|
|
||||||
|
msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH"
|
||||||
|
msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH"
|
||||||
|
|
||||||
|
msgid "URL must use one of the following protocols:"
|
||||||
|
msgstr "URL должен использовать один из следующих протоколов:"
|
||||||
|
|
||||||
|
msgid "Invalid URL format"
|
||||||
|
msgstr "Неверный формат URL"
|
||||||
|
|
||||||
|
msgid "Path cannot be empty"
|
||||||
|
msgstr "Путь не может быть пустым"
|
||||||
|
|
||||||
|
msgid "Invalid path format. Path must start with \"/\" and contain valid characters"
|
||||||
|
msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы"
|
||||||
|
|
||||||
|
msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y"
|
||||||
|
msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y"
|
||||||
|
|
||||||
|
msgid "IP address 0.0.0.0 is not allowed"
|
||||||
|
msgstr "IP-адрес 0.0.0.0 не допускается"
|
||||||
|
|
||||||
|
msgid "CIDR must be between 0 and 32"
|
||||||
|
msgstr "CIDR должен быть между 0 и 32"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: must start with ss://"
|
||||||
|
msgstr "Неверный URL Shadowsocks: должен начинаться с ss://"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: must not contain spaces"
|
||||||
|
msgstr "Неверный URL Shadowsocks: не должен содержать пробелов"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing credentials"
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password"
|
||||||
|
msgstr "Неверный URL Shadowsocks: декодированные данные должны содержать method:password"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing method and password separator \":\""
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\""
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing server address"
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствует адрес сервера"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing server"
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствует сервер"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: missing port"
|
||||||
|
msgstr "Неверный URL Shadowsocks: отсутствует порт"
|
||||||
|
|
||||||
|
msgid "Invalid port number. Must be between 1 and 65535"
|
||||||
|
msgstr "Неверный номер порта. Допустимо от 1 до 65535"
|
||||||
|
|
||||||
|
msgid "Invalid Shadowsocks URL: parsing failed"
|
||||||
|
msgstr "Неверный URL Shadowsocks: ошибка разбора"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: must not contain spaces"
|
||||||
|
msgstr "Неверный URL VLESS: не должен содержать пробелов"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: must start with vless://"
|
||||||
|
msgstr "Неверный URL VLESS: должен начинаться с vless://"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: missing UUID"
|
||||||
|
msgstr "Неверный URL VLESS: отсутствует UUID"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: missing server"
|
||||||
|
msgstr "Неверный URL VLESS: отсутствует сервер"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: missing port"
|
||||||
|
msgstr "Неверный URL VLESS: отсутствует порт"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: invalid port number. Must be between 1 and 65535"
|
||||||
|
msgstr "Неверный URL VLESS: недопустимый порт (1–65535)"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: missing query parameters"
|
||||||
|
msgstr "Неверный URL VLESS: отсутствуют параметры запроса"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws"
|
||||||
|
msgstr "Неверный URL VLESS: тип должен быть tcp, raw, udp, grpc, http или ws"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: security must be one of tls, reality, none"
|
||||||
|
msgstr "Неверный URL VLESS: параметр security должен быть tls, reality или none"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: missing pbk parameter for reality security"
|
||||||
|
msgstr "Неверный URL VLESS: отсутствует параметр pbk для security=reality"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: missing fp parameter for reality security"
|
||||||
|
msgstr "Неверный URL VLESS: отсутствует параметр fp для security=reality"
|
||||||
|
|
||||||
|
msgid "Invalid VLESS URL: parsing failed"
|
||||||
|
msgstr "Неверный URL VLESS: ошибка разбора"
|
||||||
|
|
||||||
|
msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields"
|
||||||
|
msgstr "JSON должен содержать поля \"type\", \"server\" и \"server_port\""
|
||||||
|
|
||||||
|
msgid "Invalid JSON format"
|
||||||
|
msgstr "Неверный формат JSON"
|
||||||
|
|
||||||
|
msgid "Invalid Trojan URL: must start with trojan://"
|
||||||
|
msgstr "Неверный URL Trojan: должен начинаться с trojan://"
|
||||||
|
|
||||||
|
msgid "Invalid Trojan URL: must not contain spaces"
|
||||||
|
msgstr "Неверный URL Trojan: не должен содержать пробелов"
|
||||||
|
|
||||||
|
msgid "Invalid Trojan URL: must contain username, hostname and port"
|
||||||
|
msgstr "Неверный URL Trojan: должен содержать имя пользователя, хост и порт"
|
||||||
|
|
||||||
|
msgid "Invalid Trojan URL: parsing failed"
|
||||||
|
msgstr "Неверный URL Trojan: ошибка разбора"
|
||||||
|
|
||||||
|
msgid "URL must start with vless:// or ss:// or trojan://"
|
||||||
|
msgstr "URL должен начинаться с vless://, ss:// или trojan://"
|
||||||
|
|
||||||
|
msgid "Operation timed out"
|
||||||
|
msgstr "Время ожидания истекло"
|
||||||
|
|
||||||
|
msgid "HTTP error"
|
||||||
|
msgstr "Ошибка HTTP"
|
||||||
|
|
||||||
|
msgid "Unknown error"
|
||||||
|
msgstr "Неизвестная ошибка"
|
||||||
|
|
||||||
|
msgid "Fastest"
|
||||||
|
msgstr "Самый быстрый"
|
||||||
|
|
||||||
|
msgid "Dashboard currently unavailable"
|
||||||
|
msgstr "Дашборд сейчас недоступен"
|
||||||
|
|
||||||
|
msgid "Currently unavailable"
|
||||||
|
msgstr "Временно недоступно"
|
||||||
|
|
||||||
|
msgid "Traffic"
|
||||||
|
msgstr "Трафик"
|
||||||
|
|
||||||
|
msgid "Uplink"
|
||||||
|
msgstr "Исходящий"
|
||||||
|
|
||||||
|
msgid "Downlink"
|
||||||
|
msgstr "Входящий"
|
||||||
|
|
||||||
|
msgid "Traffic Total"
|
||||||
|
msgstr "Всего трафика"
|
||||||
|
|
||||||
|
msgid "System info"
|
||||||
|
msgstr "Системная информация"
|
||||||
|
|
||||||
|
msgid "Active Connections"
|
||||||
|
msgstr "Активные соединения"
|
||||||
|
|
||||||
|
msgid "Memory Usage"
|
||||||
|
msgstr "Использование памяти"
|
||||||
|
|
||||||
|
msgid "Services info"
|
||||||
|
msgstr "Информация о сервисах"
|
||||||
|
|
||||||
|
msgid "Podkop"
|
||||||
|
msgstr "Podkop"
|
||||||
|
|
||||||
|
msgid "✔ Enabled"
|
||||||
|
msgstr "✔ Включено"
|
||||||
|
|
||||||
|
msgid "✘ Disabled"
|
||||||
|
msgstr "✘ Отключено"
|
||||||
|
|
||||||
|
msgid "Sing-box"
|
||||||
|
msgstr "Sing-box"
|
||||||
|
|
||||||
|
msgid "✔ Running"
|
||||||
|
msgstr "✔ Работает"
|
||||||
|
|
||||||
|
msgid "✘ Stopped"
|
||||||
|
msgstr "✘ Остановлен"
|
||||||
|
|
||||||
msgid "Copied!"
|
msgid "Copied!"
|
||||||
msgstr "Скопировано!"
|
msgstr "Скопировано!"
|
||||||
|
|
||||||
msgid "Failed to copy: "
|
msgid "Failed to copy: "
|
||||||
msgstr "Не удалось скопировать: "
|
msgstr "Не удалось скопировать: "
|
||||||
|
|
||||||
|
msgid "Loading..."
|
||||||
|
msgstr "Загрузка..."
|
||||||
|
|
||||||
msgid "Copy to Clipboard"
|
msgid "Copy to Clipboard"
|
||||||
msgstr "Копировать в буфер обмена"
|
msgstr "Копировать в буфер"
|
||||||
|
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "Закрыть"
|
msgstr "Закрыть"
|
||||||
|
|
||||||
msgid "Loading..."
|
|
||||||
msgstr "Загрузка..."
|
|
||||||
|
|
||||||
msgid "No output"
|
msgid "No output"
|
||||||
msgstr "Нет вывода"
|
msgstr "Нет вывода"
|
||||||
|
|
||||||
@@ -507,7 +458,7 @@ msgid "FakeIP is not working in browser"
|
|||||||
msgstr "FakeIP не работает в браузере"
|
msgstr "FakeIP не работает в браузере"
|
||||||
|
|
||||||
msgid "Check DNS server on current device (PC, phone)"
|
msgid "Check DNS server on current device (PC, phone)"
|
||||||
msgstr "Проверьте DNS сервер на текущем устройстве (ПК, телефон)"
|
msgstr "Проверьте DNS-сервер на текущем устройстве (ПК, телефон)"
|
||||||
|
|
||||||
msgid "Its must be router!"
|
msgid "Its must be router!"
|
||||||
msgstr "Это должен быть роутер!"
|
msgstr "Это должен быть роутер!"
|
||||||
@@ -522,7 +473,7 @@ msgid "Proxy IP: "
|
|||||||
msgstr "Прокси IP: "
|
msgstr "Прокси IP: "
|
||||||
|
|
||||||
msgid "Proxy is not working - same IP for both domains"
|
msgid "Proxy is not working - same IP for both domains"
|
||||||
msgstr "Прокси не работает - одинаковый IP для обоих доменов"
|
msgstr "Прокси не работает — одинаковый IP для обоих доменов"
|
||||||
|
|
||||||
msgid "IP: "
|
msgid "IP: "
|
||||||
msgstr "IP: "
|
msgstr "IP: "
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1123,15 +1123,16 @@ sing_box_configure_experimental() {
|
|||||||
config_get cache_file "main" "cache_path" "/tmp/sing-box/cache.db"
|
config_get cache_file "main" "cache_path" "/tmp/sing-box/cache.db"
|
||||||
config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true)
|
config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true)
|
||||||
|
|
||||||
local yacd_enabled
|
local yacd_enabled external_controller_ui
|
||||||
config_get_bool yacd_enabled "main" "yacd" 0
|
config_get_bool yacd_enabled "main" "yacd" 0
|
||||||
|
log "Configuring Clash API"
|
||||||
if [ "$yacd_enabled" -eq 1 ]; then
|
if [ "$yacd_enabled" -eq 1 ]; then
|
||||||
log "Configuring Clash API (yacd)"
|
log "YACD is enabled, enabling Clash API with downloadable YACD" "debug"
|
||||||
local external_controller="0.0.0.0:9090"
|
|
||||||
local external_controller_ui="ui"
|
local external_controller_ui="ui"
|
||||||
config=$(sing_box_cm_configure_clash_api "$config" "$external_controller" "$external_controller_ui")
|
config=$(sing_box_cm_configure_clash_api "$config" "$SB_CLASH_API_CONTROLLER" "$external_controller_ui")
|
||||||
else
|
else
|
||||||
log "Clash API (yacd) is disabled, skipping configuration."
|
log "YACD is disabled, enabling Clash API in online mode" "debug"
|
||||||
|
config=$(sing_box_cm_configure_clash_api "$config" "$SB_CLASH_API_CONTROLLER")
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ SB_DIRECT_OUTBOUND_TAG="direct-out"
|
|||||||
SB_MAIN_OUTBOUND_TAG="main-out"
|
SB_MAIN_OUTBOUND_TAG="main-out"
|
||||||
# Route
|
# Route
|
||||||
SB_REJECT_RULE_TAG="reject-rule-tag"
|
SB_REJECT_RULE_TAG="reject-rule-tag"
|
||||||
|
# Experimental
|
||||||
|
SB_CLASH_API_CONTROLLER="0.0.0.0:9090"
|
||||||
|
|
||||||
## Lists
|
## Lists
|
||||||
GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main"
|
GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main"
|
||||||
|
|||||||
@@ -1335,8 +1335,8 @@ sing_box_cm_configure_cache_file() {
|
|||||||
# Configure the experimental clash_api section of a sing-box JSON configuration.
|
# Configure the experimental clash_api section of a sing-box JSON configuration.
|
||||||
# Arguments:
|
# Arguments:
|
||||||
# config: JSON configuration (string)
|
# config: JSON configuration (string)
|
||||||
# external_controller: string, URL or path for the external controller
|
# external_controller: API listening address; Clash API will be disabled if empty
|
||||||
# external_ui: string, URL or path for the external UI
|
# external_ui: Optional path to static web resources to serve at http://{{external-controller}}/ui
|
||||||
# Outputs:
|
# Outputs:
|
||||||
# Writes updated JSON configuration to stdout
|
# Writes updated JSON configuration to stdout
|
||||||
# Example:
|
# Example:
|
||||||
@@ -1352,8 +1352,8 @@ sing_box_cm_configure_clash_api() {
|
|||||||
--arg external_ui "$external_ui" \
|
--arg external_ui "$external_ui" \
|
||||||
'.experimental.clash_api = {
|
'.experimental.clash_api = {
|
||||||
external_controller: $external_controller,
|
external_controller: $external_controller,
|
||||||
external_ui: $external_ui
|
}
|
||||||
}'
|
+ (if $external_ui != "" then { external_ui: $external_ui } else {} end)'
|
||||||
}
|
}
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
|
|||||||
Reference in New Issue
Block a user