Update diagnostics and domain UI

This commit is contained in:
ushan0v
2026-06-18 08:45:51 +03:00
parent 59a13cc772
commit e7e9188962
11 changed files with 1171 additions and 921 deletions
+3 -6
View File
@@ -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`
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+25 -31
View File
@@ -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"),
+25 -31
View File
@@ -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