diff --git a/fe-app-podkop/src/helpers/index.ts b/fe-app-podkop/src/helpers/index.ts index 242f2e7..43c8879 100644 --- a/fe-app-podkop/src/helpers/index.ts +++ b/fe-app-podkop/src/helpers/index.ts @@ -7,3 +7,4 @@ export * from './maskIP'; export * from './getProxyUrlName'; export * from './onMount'; export * from './getClashApiUrl'; +export * from './splitProxyString'; diff --git a/fe-app-podkop/src/helpers/splitProxyString.ts b/fe-app-podkop/src/helpers/splitProxyString.ts new file mode 100644 index 0000000..9426e8b --- /dev/null +++ b/fe-app-podkop/src/helpers/splitProxyString.ts @@ -0,0 +1,7 @@ +export function splitProxyString(str: string) { + return str + .split('\n') + .map((line) => line.trim()) + .filter((line) => !line.startsWith('//')) + .filter(Boolean); +} diff --git a/fe-app-podkop/src/podkop/methods/getDashboardSections.ts b/fe-app-podkop/src/podkop/methods/getDashboardSections.ts index c101926..6507775 100644 --- a/fe-app-podkop/src/podkop/methods/getDashboardSections.ts +++ b/fe-app-podkop/src/podkop/methods/getDashboardSections.ts @@ -1,7 +1,7 @@ import { Podkop } from '../types'; import { getConfigSections } from './getConfigSections'; import { getClashProxies } from '../../clash'; -import { getProxyUrlName } from '../../helpers'; +import { getProxyUrlName, splitProxyString } from '../../helpers'; interface IGetDashboardSectionsResponse { success: boolean; @@ -35,6 +35,11 @@ export async function getDashboardSections(): Promise proxy.code === `${section['.name']}-out`, ); + const activeConfigs = splitProxyString(section.proxy_string); + + const proxyDisplayName = + getProxyUrlName(activeConfigs?.[0]) || outbound?.value?.name || ''; + return { withTagSelect: false, code: outbound?.code || section['.name'], @@ -42,10 +47,7 @@ export async function getDashboardSections(): Promise testLatency(), }, - 'Test latency', + _('Test latency'), ), ]), E( diff --git a/fe-app-podkop/src/validators/tests/validateDomain.test.js b/fe-app-podkop/src/validators/tests/validateDomain.test.js index a2c312a..be98dba 100644 --- a/fe-app-podkop/src/validators/tests/validateDomain.test.js +++ b/fe-app-podkop/src/validators/tests/validateDomain.test.js @@ -29,6 +29,13 @@ export const invalidDomains = [ ['Too long domain (>253 chars)', Array(40).fill('abcdef').join('.') + '.com'], ]; +export const dotTLDTests = [ + ['Dot TLD allowed (.net)', '.net', true, true], + ['Dot TLD not allowed (.net)', '.net', false, false], + ['Invalid with double dot', '..net', true, false], + ['Invalid single word TLD (net)', 'net', true, false], +]; + describe('validateDomain', () => { describe.each(validDomains)('Valid domain: %s', (_desc, domain) => { it(`returns valid=true for "${domain}"`, () => { @@ -43,4 +50,14 @@ describe('validateDomain', () => { expect(res.valid).toBe(false); }); }); + + describe.each(dotTLDTests)( + 'Dot TLD toggle: %s', + (_desc, domain, allowDotTLD, expected) => { + it(`"${domain}" with allowDotTLD=${allowDotTLD} → valid=${expected}`, () => { + const res = validateDomain(domain, allowDotTLD); + expect(res.valid).toBe(expected); + }); + }, + ); }); diff --git a/fe-app-podkop/src/validators/validateDomain.ts b/fe-app-podkop/src/validators/validateDomain.ts index fdebefa..14e1f24 100644 --- a/fe-app-podkop/src/validators/validateDomain.ts +++ b/fe-app-podkop/src/validators/validateDomain.ts @@ -1,9 +1,19 @@ import { ValidationResult } from './types'; -export function validateDomain(domain: string): ValidationResult { +export function validateDomain( + domain: string, + allowDotTLD = false, +): ValidationResult { const domainRegex = /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; + if (allowDotTLD) { + const dotTLD = /^\.[a-zA-Z]{2,}$/; + if (dotTLD.test(domain)) { + return { valid: true, message: _('Valid') }; + } + } + if (!domainRegex.test(domain)) { return { valid: false, message: _('Invalid domain address') }; } diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js index cde7914..c44e389 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js @@ -130,11 +130,7 @@ function createConfigSection(section) { } try { - const activeConfigs = value - .split('\n') - .map((line) => line.trim()) - .filter((line) => !line.startsWith('//')) - .filter(Boolean); + const activeConfigs = main.splitProxyString(value); if (!activeConfigs.length) { return _( @@ -455,7 +451,7 @@ function createConfigSection(section) { return true; } - const validation = main.validateDomain(value); + const validation = main.validateDomain(value, true); if (validation.valid) { return true; @@ -493,7 +489,7 @@ function createConfigSection(section) { ); } - const { valid, results } = main.bulkValidate(domains, main.validateDomain); + const { valid, results } = main.bulkValidate(domains, row => main.validateDomain(row, true)); if (!valid) { const errors = results diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index dae6acf..7d09f99 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -14,8 +14,14 @@ function validateIPV4(ip) { } // src/validators/validateDomain.ts -function validateDomain(domain) { +function validateDomain(domain, allowDotTLD = false) { const domainRegex = /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; + if (allowDotTLD) { + const dotTLD = /^\.[a-zA-Z]{2,}$/; + if (dotTLD.test(domain)) { + return { valid: true, message: _("Valid") }; + } + } if (!domainRegex.test(domain)) { return { valid: false, message: _("Invalid domain address") }; } @@ -765,6 +771,11 @@ function getClashWsUrl() { return `ws://${hostname}:9090`; } +// src/helpers/splitProxyString.ts +function splitProxyString(str) { + return str.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("//")).filter(Boolean); +} + // src/clash/methods/createBaseApiRequest.ts async function createBaseApiRequest(fetchFn) { try { @@ -893,6 +904,8 @@ async function getDashboardSections() { const outbound = proxies.find( (proxy) => proxy.code === `${section[".name"]}-out` ); + const activeConfigs = splitProxyString(section.proxy_string); + const proxyDisplayName = getProxyUrlName(activeConfigs?.[0]) || outbound?.value?.name || ""; return { withTagSelect: false, code: outbound?.code || section[".name"], @@ -900,7 +913,7 @@ async function getDashboardSections() { outbounds: [ { code: outbound?.code || section[".name"], - displayName: getProxyUrlName(section.proxy_string) || outbound?.value?.name || "", + displayName: proxyDisplayName, latency: outbound?.value?.history?.[0]?.delay || 0, type: outbound?.value?.type || "", selected: true @@ -1294,7 +1307,7 @@ function renderDefaultState({ class: "btn dashboard-sections-grid-item-test-latency", click: () => testLatency() }, - "Test latency" + _("Test latency") ) ]), E( @@ -1891,6 +1904,7 @@ return baseclass.extend({ onMount, parseValueList, renderDashboard, + splitProxyString, triggerLatencyGroupTest, triggerLatencyProxyTest, triggerProxySelector, diff --git a/luci-app-podkop/msgmerge.sh b/luci-app-podkop/msgmerge.sh index fd1e7e6..06e5706 100644 --- a/luci-app-podkop/msgmerge.sh +++ b/luci-app-podkop/msgmerge.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail PODIR="po" diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index ef1d859..7cfb6bc 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -51,9 +51,11 @@ msgid "Config without description" msgstr "Конфигурация без описания" msgid "" -"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs" +"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup " +"configs" msgstr "" -"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для резервных конфигураций" +"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для " +"резервных конфигураций" msgid "No active configuration found. One configuration is required." msgstr "Активная конфигурация не найдена. Требуется хотя бы одна незакомментированная строка." @@ -124,14 +126,18 @@ msgstr "Выберите предустановленные сервисы дл msgid "Regional options cannot be used together" msgstr "Нельзя использовать несколько региональных опций одновременно" +#, javascript-format msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены." msgid "Russia inside restrictions" msgstr "Ограничения Russia inside" -msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." -msgstr "Внимание: «Russia inside» может использоваться только с %s. %s уже находится в «Russia inside» и был удалён из выбора." +#, javascript-format +msgid "" +"Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." +msgstr "" +"Внимание: «Russia inside» может использоваться только с %s. %s уже находится в «Russia inside» и был удалён из выбора." msgid "User Domain List Type" msgstr "Тип пользовательского списка доменов" @@ -214,8 +220,12 @@ msgstr "Введите подсети в нотации CIDR (например: msgid "User Subnets List" msgstr "Список пользовательских подсетей" -msgid "Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //" -msgstr "Введите подсети в нотации CIDR или IP-адреса через запятую, пробел или новую строку. Можно добавлять комментарии после //" +msgid "" +"Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments " +"after //" +msgstr "" +"Введите подсети в нотации CIDR или IP-адреса через запятую, пробел или новую строку. Можно добавлять комментарии " +"после //" msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы." @@ -568,11 +578,122 @@ msgstr "Конфигурация: " msgid "Diagnostics" msgstr "Диагностика" -msgid "Podkop" -msgstr "Podkop" +msgid "Additional Settings" +msgstr "Дополнительные настройки" -msgid "Extra configurations" -msgstr "Дополнительные конфигурации" +msgid "Yacd enable" +msgstr "Включить YACD" -msgid "Add Section" -msgstr "Добавить раздел" +msgid "Exclude NTP" +msgstr "Исключить NTP" + +msgid "Allows you to exclude NTP protocol traffic from the tunnel" +msgstr "Позволяет исключить направление трафика NTP-протокола в туннель" + +msgid "QUIC disable" +msgstr "Отключить QUIC" + +msgid "For issues with the video stream" +msgstr "Для проблем с видеопотоком" + +msgid "List Update Frequency" +msgstr "Частота обновления списков" + +msgid "Select how often the lists will be updated" +msgstr "Выберите как часто будут обновляться списки" + +msgid "Select DNS protocol to use" +msgstr "Выберите протокол DNS" + +msgid "Bootstrap DNS server" +msgstr "Bootstrap DNS-сервер" + +msgid "The DNS server used to look up the IP address of an upstream DNS server" +msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера" + +msgid "DNS Rewrite TTL" +msgstr "Перезапись TTL для DNS" + +msgid "Time in seconds for DNS record caching (default: 60)" +msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)" + +msgid "TTL value cannot be empty" +msgstr "Значение TTL не может быть пустым" + +msgid "TTL must be a positive number" +msgstr "TTL должно быть положительным числом" + +msgid "Config File Path" +msgstr "Путь к файлу конфигурации" + +msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" +msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" + +msgid "Cache File Path" +msgstr "Путь к файлу кэша" + +msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" +msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" + +msgid "Cache file path cannot be empty" +msgstr "Путь к файлу кэша не может быть пустым" + +msgid "Path must be absolute (start with /)" +msgstr "Путь должен быть абсолютным (начинаться с /)" + +msgid "Path must end with cache.db" +msgstr "Путь должен заканчиваться на cache.db" + +msgid "Path must contain at least one directory (like /tmp/cache.db)" +msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)" + +msgid "Source Network Interface" +msgstr "Сетевой интерфейс источника" + +msgid "Select the network interface from which the traffic will originate" +msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик" + +msgid "Interface monitoring" +msgstr "Мониторинг интерфейсов" + +msgid "Interface monitoring for bad WAN" +msgstr "Мониторинг интерфейсов для плохого WAN" + +msgid "Interface for monitoring" +msgstr "Интерфейс для мониторинга" + +msgid "Select the WAN interfaces to be monitored" +msgstr "Выберите WAN интерфейсы для мониторинга" + +msgid "Interface Monitoring Delay" +msgstr "Задержка при мониторинге интерфейсов" + +msgid "Delay in milliseconds before reloading podkop after interface UP" +msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса" + +msgid "Delay value cannot be empty" +msgstr "Значение задержки не может быть пустым" + +msgid "Dont touch my DHCP!" +msgstr "Не трогать мой DHCP!" + +msgid "Podkop will not change the DHCP config" +msgstr "Podkop не будет изменять конфигурацию DHCP" + +msgid "Proxy download of lists" +msgstr "Загрузка списков через прокси" + +msgid "Downloading all lists via main Proxy/VPN" +msgstr "Загрузка всех списков через основной прокси/VPN" + +msgid "IP for exclusion" +msgstr "IP для исключения" + +msgid "Specify local IP addresses that will never use the configured route" +msgstr "Укажите локальные IP-адреса, которые никогда не будут использовать настроенный маршрут" + +msgid "Mixed enable" +msgstr "Включить смешанный режим" + +msgid "Browser port: 2080" +msgstr "Порт браузера: 2080" diff --git a/luci-app-podkop/xgettext.sh b/luci-app-podkop/xgettext.sh index 6826874..0db78fb 100644 --- a/luci-app-podkop/xgettext.sh +++ b/luci-app-podkop/xgettext.sh @@ -1,4 +1,5 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail SRC_DIR="htdocs/luci-static/resources/view/podkop" OUT_POT="po/templates/podkop.pot" @@ -11,6 +12,7 @@ if [ ${#FILES[@]} -eq 0 ]; then exit 1 fi +mapfile -t FILES < <(printf '%s\n' "${FILES[@]}" | sort) mkdir -p "$(dirname "$OUT_POT")" echo "Generating POT template from JS files in $SRC_DIR"