Update diagnostics and domain UI
This commit is contained in:
@@ -60,12 +60,9 @@ sh <(wget -O - https://raw.githubusercontent.com/ushan0v/podkop-plus/main/instal
|
||||
|
||||
Podkop Plus расширяет набор условий, которые можно использовать в правилах:
|
||||
|
||||
- Домены (`domain_suffix`)
|
||||
- Домены (`domain_suffix`): обычные значения работают как суффиксы доменов. Для других типов используйте теги `full:example.com`, `keyword:example` и `regex:^example\.`.
|
||||
- IP-адреса (`ip_cidr`)
|
||||
- Точный полный домен (`domain`)
|
||||
- Ключевое слово домена (`domain_keyword`)
|
||||
- Регулярное выражение домена (`domain_regex`)
|
||||
- Исходные IP-адреса (`source_cidr`)
|
||||
- Исходные IP-адреса (`source_ip_cidr`)
|
||||
- Порт (`port` / `port_range`)
|
||||
- Полностью маршрутизированные IP-адреса
|
||||
- Встроенные наборы правил
|
||||
@@ -160,4 +157,4 @@ ByeDPI доступен как действие отдельной секции.
|
||||
- 💳 **Карты РФ / СБП / Tinkoff Pay:** [Донат на CloudTips](https://pay.cloudtips.ru/p/385e5af2)
|
||||
- 💎 **TON / USDT (сеть TON):** `UQBuYHpmHorHquBItuuUsNJJs5lWoP6Bi0cgY-gM5RCQvYgC`
|
||||
- 🟢 **USDT (сеть TRC-20):** `TR4SXiL9sV2GwNz8mDbJkN8SmK9jQ3GuZF`
|
||||
- 🪙 **USDT (сеть ERC-20 / BEP-20):** `0x21F86eF81d33A449037d6E750D2504BF225EAA9d`
|
||||
- 🪙 **USDT (сеть ERC-20 / BEP-20):** `0x21F86eF81d33A449037d6E750D2504BF225EAA9d`
|
||||
|
||||
+284
-283
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-10 22:19+0300\n"
|
||||
"PO-Revision-Date: 2026-06-10 22:19+0300\n"
|
||||
"POT-Creation-Date: 2026-06-18 06:38+0300\n"
|
||||
"PO-Revision-Date: 2026-06-18 06:38+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -77,6 +77,9 @@ msgstr "Все, кроме выбранных"
|
||||
msgid "All servers"
|
||||
msgstr "Все серверы"
|
||||
|
||||
msgid "Allowed domain prefixes are full:, keyword:, and regex:"
|
||||
msgstr "Разрешённые префиксы доменов: full:, keyword: и regex:"
|
||||
|
||||
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
|
||||
msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт."
|
||||
|
||||
@@ -182,9 +185,6 @@ msgstr "Не удалось сохранить сервер"
|
||||
msgid "Cascade connection"
|
||||
msgstr "Каскадное подключение"
|
||||
|
||||
msgid "Use another section as an intermediate hop for connecting to this one"
|
||||
msgstr "Использовать другую секцию как промежуточный узел для подключения к текущей"
|
||||
|
||||
msgid "Cascade connection cannot use the current section"
|
||||
msgstr "Каскадное подключение не может использовать текущую секцию"
|
||||
|
||||
@@ -368,12 +368,6 @@ msgstr "Не резолвится"
|
||||
msgid "Domain and IP Lists"
|
||||
msgstr "Списки доменов и IPs"
|
||||
|
||||
msgid "Domain keyword"
|
||||
msgstr "Ключевое слово домена"
|
||||
|
||||
msgid "Domain regex"
|
||||
msgstr "Регулярное выражение домена"
|
||||
|
||||
msgid "Domain Resolver"
|
||||
msgstr "Резолвер доменов"
|
||||
|
||||
@@ -446,9 +440,6 @@ msgstr "Введите полный объект outbound sing-box"
|
||||
msgid "Enter the subscription URL"
|
||||
msgstr "Введите URL подписки"
|
||||
|
||||
msgid "Exact full domain"
|
||||
msgstr "Точный полный домен"
|
||||
|
||||
msgid "Exclude country from URLTest"
|
||||
msgstr "Исключить страну из URLTest"
|
||||
|
||||
@@ -551,6 +542,9 @@ msgstr "Таймаут рукопожатия"
|
||||
msgid "Hide filtered servers"
|
||||
msgstr "Скрывать отфильтрованные серверы"
|
||||
|
||||
msgid "Hide values"
|
||||
msgstr "Скрывать значения"
|
||||
|
||||
msgid "Host"
|
||||
msgstr "Хост"
|
||||
|
||||
@@ -794,8 +788,8 @@ msgstr "JSON outbound"
|
||||
msgid "Key path"
|
||||
msgstr "Путь к ключу"
|
||||
|
||||
msgid "Keyword must not contain spaces"
|
||||
msgstr "Ключевое слово не должно содержать пробелы"
|
||||
msgid "Keyword must not contain spaces or commas"
|
||||
msgstr "Ключевое слово не должно содержать пробелы или запятые"
|
||||
|
||||
msgid "Latency test failed"
|
||||
msgstr ""
|
||||
@@ -845,18 +839,6 @@ msgstr "Совпадение по IP-адресам или подсетям на
|
||||
msgid "Match destination ports. Use a single port or a range"
|
||||
msgstr "Совпадение по портам назначения. Используйте одиночный порт или диапазон"
|
||||
|
||||
msgid "Match domains containing a substring"
|
||||
msgstr "Совпадение по доменам, содержащим подстроку"
|
||||
|
||||
msgid "Match domains including all subdomains"
|
||||
msgstr "Совпадение по доменам, включая все их поддомены"
|
||||
|
||||
msgid "Match domains using a regular expression"
|
||||
msgstr "Совпадение по доменам с использованием регулярного выражения"
|
||||
|
||||
msgid "Match only one exact full domain name"
|
||||
msgstr "Совпадение только по одному точному полному доменному имени"
|
||||
|
||||
msgid "Match source IPs or subnets"
|
||||
msgstr "Совпадение по исходным IP-адресам или подсетям"
|
||||
|
||||
@@ -1034,6 +1016,9 @@ msgstr "Приостановить обновление"
|
||||
msgid "Pending"
|
||||
msgstr "Ожидает запуска"
|
||||
|
||||
msgid "Plain values match domain suffixes. Use full:, keyword:, or regex: for exact domains, keywords, and regular expressions."
|
||||
msgstr "Обычные значения работают как суффиксы доменов. Используйте full:, keyword: или regex: для точных доменов, ключевых слов и регулярных выражений."
|
||||
|
||||
msgid "Podkop Plus Error"
|
||||
msgstr "Ошибка Podkop Plus"
|
||||
|
||||
@@ -1148,6 +1133,9 @@ msgstr "Сброс"
|
||||
msgid "Regional options cannot be used together"
|
||||
msgstr "Нельзя использовать несколько региональных опций одновременно"
|
||||
|
||||
msgid "Regular expression must not contain spaces or commas"
|
||||
msgstr "Регулярное выражение не должно содержать пробелы или запятые"
|
||||
|
||||
msgid "Remove"
|
||||
msgstr "Удалить"
|
||||
|
||||
@@ -1256,6 +1244,9 @@ msgstr "Выберите предустановленный набор прав
|
||||
msgid "Select a specific section for downloading external resources"
|
||||
msgstr "Выберите конкретную секцию для скачки внешних ресурсов"
|
||||
|
||||
msgid "Select a transit section"
|
||||
msgstr "Выберите секцию-транзит"
|
||||
|
||||
msgid "Select an enabled proxy, JSON outbound, or VPN section"
|
||||
msgstr "Выберите включенную секцию proxy, JSON outbound или VPN"
|
||||
|
||||
@@ -1670,6 +1661,9 @@ msgstr "Укажите IPv4-адрес"
|
||||
msgid "Use an IPv4 listen address"
|
||||
msgstr "Укажите IPv4-адрес прослушивания"
|
||||
|
||||
msgid "Use another section as an intermediate hop for connecting to this one"
|
||||
msgstr "Использовать другую секцию как промежуточный узел для подключения к текущей"
|
||||
|
||||
msgid "Use CIDR prefixes like 192.168.1.0/24"
|
||||
msgstr "Используйте CIDR-префиксы вроде 192.168.1.0/24"
|
||||
|
||||
@@ -1685,9 +1679,6 @@ msgstr "Используйте буквы, цифры, точки, подчёр
|
||||
msgid "Use sing-box duration format like 1d, 12h or 30m"
|
||||
msgstr "Используйте формат длительности sing-box, например 1d, 12h или 30m"
|
||||
|
||||
msgid "Select a transit section"
|
||||
msgstr "Выберите секцию-транзит"
|
||||
|
||||
msgid "Used by"
|
||||
msgstr "Используется"
|
||||
|
||||
@@ -1703,6 +1694,9 @@ msgstr "Валидно"
|
||||
msgid "Validation errors:"
|
||||
msgstr "Ошибки валидации:"
|
||||
|
||||
msgid "Value cannot be empty"
|
||||
msgstr "Значение не может быть пустым"
|
||||
|
||||
msgid "Value must not contain control characters"
|
||||
msgstr "Значение не должно содержать управляющие символы"
|
||||
|
||||
|
||||
@@ -2,13 +2,20 @@ import { renderButton } from '../button/renderButton';
|
||||
import { copyToClipboard } from '../../helpers/copyToClipboard';
|
||||
import { downloadAsTxt } from '../../helpers/downloadAsTxt';
|
||||
|
||||
interface ModalTextContext {
|
||||
maskValues: boolean;
|
||||
}
|
||||
|
||||
interface RenderModalOptions {
|
||||
getText?: () => string | Promise<string>;
|
||||
getText?: (context: ModalTextContext) => string | Promise<string>;
|
||||
refreshMs?: number;
|
||||
initialAutoRefresh?: boolean;
|
||||
showAutoRefreshToggle?: boolean;
|
||||
showMaskValuesToggle?: boolean;
|
||||
initialMaskValues?: boolean;
|
||||
startAtEnd?: boolean;
|
||||
autoRefreshLabel?: string;
|
||||
maskValuesLabel?: string;
|
||||
}
|
||||
|
||||
export function renderModal(
|
||||
@@ -19,13 +26,16 @@ export function renderModal(
|
||||
let currentText = text ?? '';
|
||||
let refreshInFlight = false;
|
||||
let pendingRefresh = false;
|
||||
let pendingForcedRefresh = false;
|
||||
let refreshSessionId = 0;
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
let observer: MutationObserver | undefined;
|
||||
let autoRefreshEnabled =
|
||||
options?.initialAutoRefresh ?? Boolean(options?.getText);
|
||||
let maskValuesEnabled = options?.initialMaskValues ?? true;
|
||||
let shouldScrollToBottomOnMount = Boolean(options?.startAtEnd);
|
||||
let autoRefreshInput: HTMLInputElement | undefined;
|
||||
let maskValuesInput: HTMLInputElement | undefined;
|
||||
|
||||
const codeEl = E('code', {}, currentText) as HTMLElement;
|
||||
const contentEl = E(
|
||||
@@ -44,6 +54,7 @@ export function renderModal(
|
||||
const destroyLiveRefresh = () => {
|
||||
refreshSessionId += 1;
|
||||
pendingRefresh = false;
|
||||
pendingForcedRefresh = false;
|
||||
stopRefreshTimer();
|
||||
|
||||
observer?.disconnect();
|
||||
@@ -94,8 +105,12 @@ export function renderModal(
|
||||
}
|
||||
};
|
||||
|
||||
const refreshText = async () => {
|
||||
if (!options?.getText || !autoRefreshEnabled || refreshInFlight) {
|
||||
const refreshText = async (force = false) => {
|
||||
if (
|
||||
!options?.getText ||
|
||||
(!force && !autoRefreshEnabled) ||
|
||||
refreshInFlight
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,9 +122,11 @@ export function renderModal(
|
||||
const sessionId = refreshSessionId;
|
||||
|
||||
try {
|
||||
const nextText = await options.getText();
|
||||
const nextText = await options.getText({
|
||||
maskValues: maskValuesEnabled,
|
||||
});
|
||||
|
||||
if (!body.isConnected || !autoRefreshEnabled) {
|
||||
if (!body.isConnected || (!force && !autoRefreshEnabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -124,10 +141,12 @@ export function renderModal(
|
||||
refreshInFlight = false;
|
||||
|
||||
if (pendingRefresh) {
|
||||
const shouldForceRefresh = pendingForcedRefresh;
|
||||
pendingRefresh = false;
|
||||
pendingForcedRefresh = false;
|
||||
|
||||
if (autoRefreshEnabled && body.isConnected) {
|
||||
void refreshText();
|
||||
if ((shouldForceRefresh || autoRefreshEnabled) && body.isConnected) {
|
||||
void refreshText(shouldForceRefresh);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +165,20 @@ export function renderModal(
|
||||
void refreshText();
|
||||
};
|
||||
|
||||
const requestForcedRefresh = () => {
|
||||
if (!options?.getText) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (refreshInFlight) {
|
||||
pendingRefresh = true;
|
||||
pendingForcedRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
void refreshText(true);
|
||||
};
|
||||
|
||||
const startRefreshTimer = () => {
|
||||
if (
|
||||
!options?.getText ||
|
||||
@@ -165,6 +198,7 @@ export function renderModal(
|
||||
autoRefreshEnabled = nextValue;
|
||||
refreshSessionId += 1;
|
||||
pendingRefresh = false;
|
||||
pendingForcedRefresh = false;
|
||||
|
||||
if (autoRefreshInput) {
|
||||
autoRefreshInput.checked = nextValue;
|
||||
@@ -179,6 +213,19 @@ export function renderModal(
|
||||
stopRefreshTimer();
|
||||
};
|
||||
|
||||
const setMaskValuesEnabled = (nextValue: boolean) => {
|
||||
maskValuesEnabled = nextValue;
|
||||
refreshSessionId += 1;
|
||||
pendingRefresh = false;
|
||||
pendingForcedRefresh = false;
|
||||
|
||||
if (maskValuesInput) {
|
||||
maskValuesInput.checked = nextValue;
|
||||
}
|
||||
|
||||
requestForcedRefresh();
|
||||
};
|
||||
|
||||
const footerChildren: HTMLElement[] = [
|
||||
renderButton({
|
||||
classNames: ['cbi-button-apply'],
|
||||
@@ -221,6 +268,27 @@ export function renderModal(
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.getText && options?.showMaskValuesToggle) {
|
||||
maskValuesInput = document.createElement('input');
|
||||
maskValuesInput.type = 'checkbox';
|
||||
maskValuesInput.className = 'cbi-input-checkbox';
|
||||
maskValuesInput.checked = maskValuesEnabled;
|
||||
maskValuesInput.addEventListener('change', () => {
|
||||
setMaskValuesEnabled(maskValuesInput!.checked);
|
||||
});
|
||||
|
||||
footerChildren.unshift(
|
||||
E('label', { class: 'pdk-partial-modal__checkbox' }, [
|
||||
maskValuesInput,
|
||||
E(
|
||||
'span',
|
||||
{ class: 'pdk-partial-modal__checkbox-text' },
|
||||
options.maskValuesLabel ?? _('Hide values'),
|
||||
),
|
||||
]) as HTMLElement,
|
||||
);
|
||||
}
|
||||
|
||||
const body = E('div', { class: 'pdk-partial-modal__body' }, [
|
||||
E('div', {}, [
|
||||
contentEl,
|
||||
|
||||
@@ -334,10 +334,15 @@ export const PodkopShellMethods = {
|
||||
[],
|
||||
'/etc/init.d/podkop-plus',
|
||||
),
|
||||
globalCheck: async () =>
|
||||
callBaseMethod<unknown>(Podkop.AvailableMethods.GLOBAL_CHECK),
|
||||
showSingBoxConfig: async () =>
|
||||
callBaseMethod<unknown>(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG),
|
||||
globalCheck: async (masked = true) =>
|
||||
callBaseMethod<unknown>(
|
||||
Podkop.AvailableMethods.GLOBAL_CHECK,
|
||||
[masked ? 'masked' : 'raw'],
|
||||
),
|
||||
showSingBoxConfig: async (masked = true) =>
|
||||
callBaseMethod<unknown>(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG, [
|
||||
masked ? 'masked' : 'raw',
|
||||
]),
|
||||
checkLogs: async () =>
|
||||
callBaseMethod<unknown>(Podkop.AvailableMethods.CHECK_LOGS),
|
||||
checkSingBoxLogs: async () =>
|
||||
|
||||
@@ -670,9 +670,24 @@ async function handleShowGlobalCheck() {
|
||||
const globalCheck = await PodkopShellMethods.globalCheck();
|
||||
|
||||
if (globalCheck.success) {
|
||||
const getGlobalCheckText = async ({ maskValues = true } = {}) => {
|
||||
const latestGlobalCheck =
|
||||
await PodkopShellMethods.globalCheck(maskValues);
|
||||
|
||||
if (!latestGlobalCheck.success) {
|
||||
throw latestGlobalCheck;
|
||||
}
|
||||
|
||||
return (latestGlobalCheck.data as string) ?? '';
|
||||
};
|
||||
|
||||
ui.showModal(
|
||||
_('Global check'),
|
||||
renderModal(globalCheck.data as string, 'global_check'),
|
||||
renderModal(globalCheck.data as string, 'global_check', {
|
||||
getText: getGlobalCheckText,
|
||||
initialAutoRefresh: false,
|
||||
showMaskValuesToggle: true,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
logger.error('[DIAGNOSTIC]', 'handleShowGlobalCheck - e', globalCheck);
|
||||
@@ -728,11 +743,27 @@ async function handleShowSingBoxConfig() {
|
||||
const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig();
|
||||
|
||||
if (showSingBoxConfig.success) {
|
||||
const getSingBoxConfigText = async ({ maskValues = true } = {}) => {
|
||||
const latestSingBoxConfig =
|
||||
await PodkopShellMethods.showSingBoxConfig(maskValues);
|
||||
|
||||
if (!latestSingBoxConfig.success) {
|
||||
throw latestSingBoxConfig;
|
||||
}
|
||||
|
||||
return JSON.stringify(latestSingBoxConfig.data, null, 2);
|
||||
};
|
||||
|
||||
ui.showModal(
|
||||
_('Show sing-box config'),
|
||||
renderModal(
|
||||
JSON.stringify(showSingBoxConfig.data, null, 2),
|
||||
'show_sing_box_config',
|
||||
{
|
||||
getText: getSingBoxConfigText,
|
||||
initialAutoRefresh: false,
|
||||
showMaskValuesToggle: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -2585,8 +2585,13 @@ var PodkopShellMethods = {
|
||||
[],
|
||||
"/etc/init.d/podkop-plus"
|
||||
),
|
||||
globalCheck: async () => callBaseMethod(Podkop.AvailableMethods.GLOBAL_CHECK),
|
||||
showSingBoxConfig: async () => callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG),
|
||||
globalCheck: async (masked = true) => callBaseMethod(
|
||||
Podkop.AvailableMethods.GLOBAL_CHECK,
|
||||
[masked ? "masked" : "raw"]
|
||||
),
|
||||
showSingBoxConfig: async (masked = true) => callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG, [
|
||||
masked ? "masked" : "raw"
|
||||
]),
|
||||
checkLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS),
|
||||
checkSingBoxLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_SING_BOX_LOGS),
|
||||
getSystemInfo: async () => callBaseMethod(
|
||||
@@ -6869,12 +6874,15 @@ function renderModal(text, name, options) {
|
||||
let currentText = text ?? "";
|
||||
let refreshInFlight = false;
|
||||
let pendingRefresh = false;
|
||||
let pendingForcedRefresh = false;
|
||||
let refreshSessionId = 0;
|
||||
let timer;
|
||||
let observer;
|
||||
let autoRefreshEnabled = options?.initialAutoRefresh ?? Boolean(options?.getText);
|
||||
let maskValuesEnabled = options?.initialMaskValues ?? true;
|
||||
let shouldScrollToBottomOnMount = Boolean(options?.startAtEnd);
|
||||
let autoRefreshInput;
|
||||
let maskValuesInput;
|
||||
const codeEl = E("code", {}, currentText);
|
||||
const contentEl = E(
|
||||
"pre",
|
||||
@@ -6890,6 +6898,7 @@ function renderModal(text, name, options) {
|
||||
const destroyLiveRefresh = () => {
|
||||
refreshSessionId += 1;
|
||||
pendingRefresh = false;
|
||||
pendingForcedRefresh = false;
|
||||
stopRefreshTimer();
|
||||
observer?.disconnect();
|
||||
observer = void 0;
|
||||
@@ -6925,8 +6934,8 @@ function renderModal(text, name, options) {
|
||||
});
|
||||
}
|
||||
};
|
||||
const refreshText = async () => {
|
||||
if (!options?.getText || !autoRefreshEnabled || refreshInFlight) {
|
||||
const refreshText = async (force = false) => {
|
||||
if (!options?.getText || !force && !autoRefreshEnabled || refreshInFlight) {
|
||||
return;
|
||||
}
|
||||
if (!body.isConnected) {
|
||||
@@ -6935,8 +6944,10 @@ function renderModal(text, name, options) {
|
||||
refreshInFlight = true;
|
||||
const sessionId = refreshSessionId;
|
||||
try {
|
||||
const nextText = await options.getText();
|
||||
if (!body.isConnected || !autoRefreshEnabled) {
|
||||
const nextText = await options.getText({
|
||||
maskValues: maskValuesEnabled
|
||||
});
|
||||
if (!body.isConnected || !force && !autoRefreshEnabled) {
|
||||
return;
|
||||
}
|
||||
if (sessionId !== refreshSessionId) {
|
||||
@@ -6948,9 +6959,11 @@ function renderModal(text, name, options) {
|
||||
} finally {
|
||||
refreshInFlight = false;
|
||||
if (pendingRefresh) {
|
||||
const shouldForceRefresh = pendingForcedRefresh;
|
||||
pendingRefresh = false;
|
||||
if (autoRefreshEnabled && body.isConnected) {
|
||||
void refreshText();
|
||||
pendingForcedRefresh = false;
|
||||
if ((shouldForceRefresh || autoRefreshEnabled) && body.isConnected) {
|
||||
void refreshText(shouldForceRefresh);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6965,6 +6978,17 @@ function renderModal(text, name, options) {
|
||||
}
|
||||
void refreshText();
|
||||
};
|
||||
const requestForcedRefresh = () => {
|
||||
if (!options?.getText) {
|
||||
return;
|
||||
}
|
||||
if (refreshInFlight) {
|
||||
pendingRefresh = true;
|
||||
pendingForcedRefresh = true;
|
||||
return;
|
||||
}
|
||||
void refreshText(true);
|
||||
};
|
||||
const startRefreshTimer = () => {
|
||||
if (!options?.getText || !autoRefreshEnabled || timer || typeof document === "undefined") {
|
||||
return;
|
||||
@@ -6977,6 +7001,7 @@ function renderModal(text, name, options) {
|
||||
autoRefreshEnabled = nextValue;
|
||||
refreshSessionId += 1;
|
||||
pendingRefresh = false;
|
||||
pendingForcedRefresh = false;
|
||||
if (autoRefreshInput) {
|
||||
autoRefreshInput.checked = nextValue;
|
||||
}
|
||||
@@ -6987,6 +7012,16 @@ function renderModal(text, name, options) {
|
||||
}
|
||||
stopRefreshTimer();
|
||||
};
|
||||
const setMaskValuesEnabled = (nextValue) => {
|
||||
maskValuesEnabled = nextValue;
|
||||
refreshSessionId += 1;
|
||||
pendingRefresh = false;
|
||||
pendingForcedRefresh = false;
|
||||
if (maskValuesInput) {
|
||||
maskValuesInput.checked = nextValue;
|
||||
}
|
||||
requestForcedRefresh();
|
||||
};
|
||||
const footerChildren = [
|
||||
renderButton({
|
||||
classNames: ["cbi-button-apply"],
|
||||
@@ -7028,6 +7063,25 @@ ${currentText}
|
||||
])
|
||||
);
|
||||
}
|
||||
if (options?.getText && options?.showMaskValuesToggle) {
|
||||
maskValuesInput = document.createElement("input");
|
||||
maskValuesInput.type = "checkbox";
|
||||
maskValuesInput.className = "cbi-input-checkbox";
|
||||
maskValuesInput.checked = maskValuesEnabled;
|
||||
maskValuesInput.addEventListener("change", () => {
|
||||
setMaskValuesEnabled(maskValuesInput.checked);
|
||||
});
|
||||
footerChildren.unshift(
|
||||
E("label", { class: "pdk-partial-modal__checkbox" }, [
|
||||
maskValuesInput,
|
||||
E(
|
||||
"span",
|
||||
{ class: "pdk-partial-modal__checkbox-text" },
|
||||
options.maskValuesLabel ?? _("Hide values")
|
||||
)
|
||||
])
|
||||
);
|
||||
}
|
||||
const body = E("div", { class: "pdk-partial-modal__body" }, [
|
||||
E("div", {}, [
|
||||
contentEl,
|
||||
@@ -8149,9 +8203,20 @@ async function handleShowGlobalCheck() {
|
||||
try {
|
||||
const globalCheck = await PodkopShellMethods.globalCheck();
|
||||
if (globalCheck.success) {
|
||||
const getGlobalCheckText = async ({ maskValues = true } = {}) => {
|
||||
const latestGlobalCheck = await PodkopShellMethods.globalCheck(maskValues);
|
||||
if (!latestGlobalCheck.success) {
|
||||
throw latestGlobalCheck;
|
||||
}
|
||||
return latestGlobalCheck.data ?? "";
|
||||
};
|
||||
ui.showModal(
|
||||
_("Global check"),
|
||||
renderModal(globalCheck.data, "global_check")
|
||||
renderModal(globalCheck.data, "global_check", {
|
||||
getText: getGlobalCheckText,
|
||||
initialAutoRefresh: false,
|
||||
showMaskValuesToggle: true
|
||||
})
|
||||
);
|
||||
} else {
|
||||
logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", globalCheck);
|
||||
@@ -8198,11 +8263,23 @@ async function handleShowSingBoxConfig() {
|
||||
try {
|
||||
const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig();
|
||||
if (showSingBoxConfig.success) {
|
||||
const getSingBoxConfigText = async ({ maskValues = true } = {}) => {
|
||||
const latestSingBoxConfig = await PodkopShellMethods.showSingBoxConfig(maskValues);
|
||||
if (!latestSingBoxConfig.success) {
|
||||
throw latestSingBoxConfig;
|
||||
}
|
||||
return JSON.stringify(latestSingBoxConfig.data, null, 2);
|
||||
};
|
||||
ui.showModal(
|
||||
_("Show sing-box config"),
|
||||
renderModal(
|
||||
JSON.stringify(showSingBoxConfig.data, null, 2),
|
||||
"show_sing_box_config"
|
||||
"show_sing_box_config",
|
||||
{
|
||||
getText: getSingBoxConfigText,
|
||||
initialAutoRefresh: false,
|
||||
showMaskValuesToggle: true
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -781,8 +781,8 @@ function validateKeyword(_section_id, value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/\s/.test(value)) {
|
||||
return _("Keyword must not contain spaces");
|
||||
if (/[,\s]/.test(value)) {
|
||||
return _("Keyword must not contain spaces or commas");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -1514,9 +1514,60 @@ function analyzeTextListValue(value, validateItem, emptyMessage, options = {}) {
|
||||
}
|
||||
|
||||
function analyzeDomainSuffixText(value) {
|
||||
const validateDomainCondition = (domain) => {
|
||||
const normalized = `${domain || ""}`.trim();
|
||||
if (normalized.includes("/")) {
|
||||
return { valid: false, message: _("Invalid domain address") };
|
||||
}
|
||||
|
||||
return main.validateDomain(normalized, true);
|
||||
};
|
||||
|
||||
return analyzeTextListValue(
|
||||
value,
|
||||
(item) => main.validateDomain(item, true),
|
||||
(item) => {
|
||||
const colonIndex = item.indexOf(":");
|
||||
const prefix = colonIndex > 0 ? item.slice(0, colonIndex) : "";
|
||||
const body = colonIndex > 0 ? item.slice(colonIndex + 1) : item;
|
||||
|
||||
if (!prefix) {
|
||||
return validateDomainCondition(body);
|
||||
}
|
||||
|
||||
if (!["full", "keyword", "regex"].includes(prefix)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Allowed domain prefixes are full:, keyword:, and regex:"),
|
||||
};
|
||||
}
|
||||
|
||||
if (!body.length) {
|
||||
return { valid: false, message: _("Value cannot be empty") };
|
||||
}
|
||||
|
||||
if (prefix === "full") {
|
||||
return validateDomainCondition(body);
|
||||
}
|
||||
|
||||
if (prefix === "keyword") {
|
||||
const validation = validateKeyword(null, body);
|
||||
return validation === true
|
||||
? { valid: true, message: _("Valid") }
|
||||
: { valid: false, message: validation };
|
||||
}
|
||||
|
||||
if (/[,\s]/.test(body)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _("Regular expression must not contain spaces or commas"),
|
||||
};
|
||||
}
|
||||
|
||||
const validation = validateRegex(null, body);
|
||||
return validation === true
|
||||
? { valid: true, message: _("Valid") }
|
||||
: { valid: false, message: validation };
|
||||
},
|
||||
_("At least one valid domain must be specified."),
|
||||
{
|
||||
normalizeDuplicateValue: (item) => `${item}`.toLowerCase(),
|
||||
@@ -1524,6 +1575,54 @@ function analyzeDomainSuffixText(value) {
|
||||
);
|
||||
}
|
||||
|
||||
function domainValuesWithPrefix(section_id, key, prefix) {
|
||||
return getConfigListValues(section_id, key).map((value) =>
|
||||
prefix ? `${prefix}:${value}` : value,
|
||||
);
|
||||
}
|
||||
|
||||
function domainTextValuesWithPrefix(section_id, key, prefix) {
|
||||
const legacyText = uci.get(UCI_PACKAGE, section_id, `${key}_text`);
|
||||
if (!legacyText) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return main.parseValueList(legacyText).map((value) =>
|
||||
prefix ? `${prefix}:${value}` : value,
|
||||
);
|
||||
}
|
||||
|
||||
function uniqueDomainTextValues(values) {
|
||||
const seen = new Set();
|
||||
|
||||
return values.filter((value) => {
|
||||
const key = `${value}`.toLowerCase();
|
||||
if (!key || seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function loadCombinedDomainText(section_id) {
|
||||
const textValue = uci.get(UCI_PACKAGE, section_id, "domain_suffix_text");
|
||||
const values = uniqueDomainTextValues([
|
||||
...(textValue ? main.parseValueList(textValue) : []),
|
||||
...domainValuesWithPrefix(section_id, "domain_suffix", ""),
|
||||
...domainValuesWithPrefix(section_id, "domain", "full"),
|
||||
...domainValuesWithPrefix(section_id, "domain_keyword", "keyword"),
|
||||
...domainValuesWithPrefix(section_id, "domain_regex", "regex"),
|
||||
...domainTextValuesWithPrefix(section_id, "domain_suffix", ""),
|
||||
...domainTextValuesWithPrefix(section_id, "domain", "full"),
|
||||
...domainTextValuesWithPrefix(section_id, "domain_keyword", "keyword"),
|
||||
...domainTextValuesWithPrefix(section_id, "domain_regex", "regex"),
|
||||
]);
|
||||
|
||||
return valuesToText(values);
|
||||
}
|
||||
|
||||
function analyzeIpCidrText(value) {
|
||||
return analyzeTextListValue(
|
||||
value,
|
||||
@@ -3109,6 +3208,10 @@ function addTextConditionField(section, config) {
|
||||
configureTextareaOption(o, config.textAnalyze);
|
||||
|
||||
o.load = function (section_id) {
|
||||
if (typeof config.loadText === "function") {
|
||||
return config.loadText(section_id);
|
||||
}
|
||||
|
||||
const textValue = uci.get(UCI_PACKAGE, section_id, `${config.key}_text`);
|
||||
if (textValue) {
|
||||
return textValue;
|
||||
@@ -3128,6 +3231,10 @@ function addTextConditionField(section, config) {
|
||||
|
||||
uci.unset(UCI_PACKAGE, section_id, config.key);
|
||||
uci.unset(UCI_PACKAGE, section_id, `${config.key}_text_mode`);
|
||||
|
||||
if (typeof config.afterWrite === "function") {
|
||||
config.afterWrite(section_id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4234,8 +4341,26 @@ function createSectionContent(section) {
|
||||
addTextConditionField(section, {
|
||||
key: "domain_suffix",
|
||||
label: _("Domains"),
|
||||
description: _("Match domains including all subdomains"),
|
||||
description: _(
|
||||
"Plain values match domain suffixes. Use full:, keyword:, or regex: for exact domains, keywords, and regular expressions.",
|
||||
),
|
||||
textAnalyze: analyzeDomainSuffixText,
|
||||
loadText: loadCombinedDomainText,
|
||||
afterWrite: function (section_id) {
|
||||
[
|
||||
"domain",
|
||||
"domain_keyword",
|
||||
"domain_regex",
|
||||
"domain_text",
|
||||
"domain_keyword_text",
|
||||
"domain_regex_text",
|
||||
"domain_text_mode",
|
||||
"domain_keyword_text_mode",
|
||||
"domain_regex_text_mode",
|
||||
].forEach((key) => {
|
||||
uci.unset(UCI_PACKAGE, section_id, key);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
addTextConditionField(section, {
|
||||
@@ -4245,34 +4370,6 @@ function createSectionContent(section) {
|
||||
textAnalyze: analyzeIpCidrText,
|
||||
});
|
||||
|
||||
addDynamicConditionField(section, {
|
||||
key: "domain",
|
||||
label: _("Exact full domain"),
|
||||
description: _("Match only one exact full domain name"),
|
||||
dynamicValidate: function (_section_id, value) {
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const validation = main.validateDomain(value);
|
||||
return validation.valid ? true : validation.message;
|
||||
},
|
||||
});
|
||||
|
||||
addDynamicConditionField(section, {
|
||||
key: "domain_keyword",
|
||||
label: _("Domain keyword"),
|
||||
description: _("Match domains containing a substring"),
|
||||
dynamicValidate: validateKeyword,
|
||||
});
|
||||
|
||||
addDynamicConditionField(section, {
|
||||
key: "domain_regex",
|
||||
label: _("Domain regex"),
|
||||
description: _("Match domains using a regular expression"),
|
||||
dynamicValidate: validateRegex,
|
||||
});
|
||||
|
||||
addLocalDeviceSubnetDynamicField(section, {
|
||||
key: "source_ip_cidr",
|
||||
label: _("Source IPs"),
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-10 22:19+0300\n"
|
||||
"PO-Revision-Date: 2026-06-10 22:19+0300\n"
|
||||
"POT-Creation-Date: 2026-06-18 06:38+0300\n"
|
||||
"PO-Revision-Date: 2026-06-18 06:38+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -77,6 +77,9 @@ msgstr "Все, кроме выбранных"
|
||||
msgid "All servers"
|
||||
msgstr "Все серверы"
|
||||
|
||||
msgid "Allowed domain prefixes are full:, keyword:, and regex:"
|
||||
msgstr "Разрешённые префиксы доменов: full:, keyword: и regex:"
|
||||
|
||||
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
|
||||
msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт."
|
||||
|
||||
@@ -182,9 +185,6 @@ msgstr "Не удалось сохранить сервер"
|
||||
msgid "Cascade connection"
|
||||
msgstr "Каскадное подключение"
|
||||
|
||||
msgid "Use another section as an intermediate hop for connecting to this one"
|
||||
msgstr "Использовать другую секцию как промежуточный узел для подключения к текущей"
|
||||
|
||||
msgid "Cascade connection cannot use the current section"
|
||||
msgstr "Каскадное подключение не может использовать текущую секцию"
|
||||
|
||||
@@ -368,12 +368,6 @@ msgstr "Не резолвится"
|
||||
msgid "Domain and IP Lists"
|
||||
msgstr "Списки доменов и IPs"
|
||||
|
||||
msgid "Domain keyword"
|
||||
msgstr "Ключевое слово домена"
|
||||
|
||||
msgid "Domain regex"
|
||||
msgstr "Регулярное выражение домена"
|
||||
|
||||
msgid "Domain Resolver"
|
||||
msgstr "Резолвер доменов"
|
||||
|
||||
@@ -446,9 +440,6 @@ msgstr "Введите полный объект outbound sing-box"
|
||||
msgid "Enter the subscription URL"
|
||||
msgstr "Введите URL подписки"
|
||||
|
||||
msgid "Exact full domain"
|
||||
msgstr "Точный полный домен"
|
||||
|
||||
msgid "Exclude country from URLTest"
|
||||
msgstr "Исключить страну из URLTest"
|
||||
|
||||
@@ -551,6 +542,9 @@ msgstr "Таймаут рукопожатия"
|
||||
msgid "Hide filtered servers"
|
||||
msgstr "Скрывать отфильтрованные серверы"
|
||||
|
||||
msgid "Hide values"
|
||||
msgstr "Скрывать значения"
|
||||
|
||||
msgid "Host"
|
||||
msgstr "Хост"
|
||||
|
||||
@@ -794,8 +788,8 @@ msgstr "JSON outbound"
|
||||
msgid "Key path"
|
||||
msgstr "Путь к ключу"
|
||||
|
||||
msgid "Keyword must not contain spaces"
|
||||
msgstr "Ключевое слово не должно содержать пробелы"
|
||||
msgid "Keyword must not contain spaces or commas"
|
||||
msgstr "Ключевое слово не должно содержать пробелы или запятые"
|
||||
|
||||
msgid "Latency test failed"
|
||||
msgstr ""
|
||||
@@ -845,18 +839,6 @@ msgstr "Совпадение по IP-адресам или подсетям на
|
||||
msgid "Match destination ports. Use a single port or a range"
|
||||
msgstr "Совпадение по портам назначения. Используйте одиночный порт или диапазон"
|
||||
|
||||
msgid "Match domains containing a substring"
|
||||
msgstr "Совпадение по доменам, содержащим подстроку"
|
||||
|
||||
msgid "Match domains including all subdomains"
|
||||
msgstr "Совпадение по доменам, включая все их поддомены"
|
||||
|
||||
msgid "Match domains using a regular expression"
|
||||
msgstr "Совпадение по доменам с использованием регулярного выражения"
|
||||
|
||||
msgid "Match only one exact full domain name"
|
||||
msgstr "Совпадение только по одному точному полному доменному имени"
|
||||
|
||||
msgid "Match source IPs or subnets"
|
||||
msgstr "Совпадение по исходным IP-адресам или подсетям"
|
||||
|
||||
@@ -1034,6 +1016,9 @@ msgstr "Приостановить обновление"
|
||||
msgid "Pending"
|
||||
msgstr "Ожидает запуска"
|
||||
|
||||
msgid "Plain values match domain suffixes. Use full:, keyword:, or regex: for exact domains, keywords, and regular expressions."
|
||||
msgstr "Обычные значения работают как суффиксы доменов. Используйте full:, keyword: или regex: для точных доменов, ключевых слов и регулярных выражений."
|
||||
|
||||
msgid "Podkop Plus Error"
|
||||
msgstr "Ошибка Podkop Plus"
|
||||
|
||||
@@ -1148,6 +1133,9 @@ msgstr "Сброс"
|
||||
msgid "Regional options cannot be used together"
|
||||
msgstr "Нельзя использовать несколько региональных опций одновременно"
|
||||
|
||||
msgid "Regular expression must not contain spaces or commas"
|
||||
msgstr "Регулярное выражение не должно содержать пробелы или запятые"
|
||||
|
||||
msgid "Remove"
|
||||
msgstr "Удалить"
|
||||
|
||||
@@ -1256,6 +1244,9 @@ msgstr "Выберите предустановленный набор прав
|
||||
msgid "Select a specific section for downloading external resources"
|
||||
msgstr "Выберите конкретную секцию для скачки внешних ресурсов"
|
||||
|
||||
msgid "Select a transit section"
|
||||
msgstr "Выберите секцию-транзит"
|
||||
|
||||
msgid "Select an enabled proxy, JSON outbound, or VPN section"
|
||||
msgstr "Выберите включенную секцию proxy, JSON outbound или VPN"
|
||||
|
||||
@@ -1670,6 +1661,9 @@ msgstr "Укажите IPv4-адрес"
|
||||
msgid "Use an IPv4 listen address"
|
||||
msgstr "Укажите IPv4-адрес прослушивания"
|
||||
|
||||
msgid "Use another section as an intermediate hop for connecting to this one"
|
||||
msgstr "Использовать другую секцию как промежуточный узел для подключения к текущей"
|
||||
|
||||
msgid "Use CIDR prefixes like 192.168.1.0/24"
|
||||
msgstr "Используйте CIDR-префиксы вроде 192.168.1.0/24"
|
||||
|
||||
@@ -1685,9 +1679,6 @@ msgstr "Используйте буквы, цифры, точки, подчёр
|
||||
msgid "Use sing-box duration format like 1d, 12h or 30m"
|
||||
msgstr "Используйте формат длительности sing-box, например 1d, 12h или 30m"
|
||||
|
||||
msgid "Select a transit section"
|
||||
msgstr "Выберите секцию-транзит"
|
||||
|
||||
msgid "Used by"
|
||||
msgstr "Используется"
|
||||
|
||||
@@ -1703,6 +1694,9 @@ msgstr "Валидно"
|
||||
msgid "Validation errors:"
|
||||
msgstr "Ошибки валидации:"
|
||||
|
||||
msgid "Value cannot be empty"
|
||||
msgstr "Значение не может быть пустым"
|
||||
|
||||
msgid "Value must not contain control characters"
|
||||
msgstr "Значение не должно содержать управляющие символы"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user