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