diff --git a/fe-app-tachyon/src/constants.ts b/fe-app-tachyon/src/constants.ts index 3f9ea053..ff8c88f8 100644 --- a/fe-app-tachyon/src/constants.ts +++ b/fe-app-tachyon/src/constants.ts @@ -16,8 +16,8 @@ export const DOMAIN_LIST_OPTIONS = { russia_inside: 'Russia inside', russia_outside: 'Russia outside', ukraine_inside: 'Ukraine', - geoblock: 'Geo Block (снаружи РФ)', - block: 'Block (РКН / внутри РФ)', + geoblock: 'Geo Block', + block: 'Block (RKN)', porn: 'Porn', news: 'News', anime: 'Anime', diff --git a/fe-app-tachyon/src/tachyon/methods/shell/index.ts b/fe-app-tachyon/src/tachyon/methods/shell/index.ts index c77901cb..2f987b6a 100644 --- a/fe-app-tachyon/src/tachyon/methods/shell/index.ts +++ b/fe-app-tachyon/src/tachyon/methods/shell/index.ts @@ -51,6 +51,33 @@ function parseJsonObjectOutput(output: string): T | null { } } +function parseJsonArrayOutput(output: string): T[] | null { + if (!output) { + return null; + } + + try { + const parsed = JSON.parse(output); + if (Array.isArray(parsed)) { + return parsed as T[]; + } + return null; + } catch (_error) { + const jsonMatch = output.match(/(\[[\s\S]*\])\s*$/); + + if (!jsonMatch) { + return null; + } + + try { + const parsed = JSON.parse(jsonMatch[1]); + return Array.isArray(parsed) ? (parsed as T[]) : null; + } catch (_jsonError) { + return null; + } + } +} + function parseComponentActionOutput(output: string) { return parseJsonObjectOutput(output); } @@ -675,6 +702,64 @@ export const TachyonShellMethods = { callBaseMethod( Tachyon.AvailableMethods.COMPONENT_UPDATE_CHECK_CACHE, ), + componentListReleases: async ( + component: Tachyon.ComponentName, + count = 3, + ) => { + const response = await executeShellCommand({ + command: '/usr/bin/tachyon', + args: [ + Tachyon.AvailableMethods.COMPONENT_LIST_RELEASES, + component, + String(count), + ], + timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS, + }); + const parsed = parseJsonArrayOutput( + response.stdout, + ); + + if ((response.code ?? 0) !== 0 || !parsed) { + return { + success: false, + error: response.stderr || _('Failed to fetch releases'), + } as Tachyon.MethodFailureResponse; + } + + return { + success: true, + data: parsed, + } as Tachyon.MethodSuccessResponse; + }, + componentInstallVersion: async ( + component: Tachyon.ComponentName, + tag: string, + ) => { + const response = await executeShellCommand({ + command: '/usr/bin/tachyon', + args: [ + Tachyon.AvailableMethods.COMPONENT_INSTALL_VERSION, + component, + tag, + ], + timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS, + }); + const parsed = parseJsonObjectOutput( + response.stdout, + ); + + if ((response.code ?? 0) !== 0 || !parsed) { + return { + success: false, + error: response.stderr || _('Failed to install version'), + } as Tachyon.MethodFailureResponse; + } + + return { + success: true, + data: parsed, + } as Tachyon.MethodSuccessResponse; + }, waitComponentActionJob: async ( jobId: string, component: Tachyon.ComponentName, diff --git a/fe-app-tachyon/src/tachyon/services/logger.service.ts b/fe-app-tachyon/src/tachyon/services/logger.service.ts index 22c5909e..83be38fe 100644 --- a/fe-app-tachyon/src/tachyon/services/logger.service.ts +++ b/fe-app-tachyon/src/tachyon/services/logger.service.ts @@ -65,7 +65,7 @@ class Logger { download(filename = 'logs.txt'): void { if (typeof document === 'undefined') { - console.warn('Logger.download() доступен только в браузере'); + console.warn('Logger.download() available only in browser'); return; } downloadAsTxt(this.getLogs(), filename); diff --git a/fe-app-tachyon/src/tachyon/tabs/updates/initController.ts b/fe-app-tachyon/src/tachyon/tabs/updates/initController.ts index c8ac7ed5..2f50daa3 100644 --- a/fe-app-tachyon/src/tachyon/tabs/updates/initController.ts +++ b/fe-app-tachyon/src/tachyon/tabs/updates/initController.ts @@ -117,6 +117,7 @@ interface ComponentCard { repoUrl?: string; actions: ComponentActionButton[]; badgeNode?: Node | null; + supportsVersions?: boolean; } let updatesLifecycleRegistered = false; @@ -1265,6 +1266,7 @@ function getComponentCards(): ComponentCard[] { releaseUrl: getGitHubReleaseUrl('tachyon'), repoUrl: COMPONENT_REPO_URLS.tachyon, actions: tachyonActions, + supportsVersions: true, }, { component: 'sing_box', @@ -1286,6 +1288,7 @@ function getComponentCards(): ComponentCard[] { ? 'https://github.com/shtorm-7/sing-box-extended' : COMPONENT_REPO_URLS.sing_box), actions: singBoxActions, + supportsVersions: true, }, { component: 'zapret', @@ -1300,6 +1303,7 @@ function getComponentCards(): ComponentCard[] { releaseUrl: getGitHubReleaseUrl('zapret'), repoUrl: COMPONENT_REPO_URLS.zapret, actions: zapretActions, + supportsVersions: true, }, { component: 'zapret2', @@ -1314,6 +1318,7 @@ function getComponentCards(): ComponentCard[] { releaseUrl: getGitHubReleaseUrl('zapret2'), repoUrl: COMPONENT_REPO_URLS.zapret2, actions: zapret2Actions, + supportsVersions: true, }, { component: 'byedpi', @@ -1346,6 +1351,156 @@ function getComponentCards(): ComponentCard[] { ]; } +async function showVersionPicker(component: Tachyon.ComponentName) { + const container = document.getElementById('tachyon_updates-components'); + if (!container) return; + + let dropdown = container.querySelector( + '.tachyon-version-picker', + ) as HTMLElement | null; + if (dropdown) { + dropdown.remove(); + return; + } + + dropdown = E('div', { class: 'tachyon-version-picker' }, [ + E( + 'div', + { class: 'tachyon-version-picker__loading' }, + _('Loading versions...'), + ), + ]); + + const componentCards = container.querySelectorAll( + `.tachyon_updates-page__component`, + ); + const card = Array.from(componentCards).find((el) => { + const title = el.querySelector('.tachyon_updates-page__component__title'); + return title?.textContent === getComponentCardTitle(component); + }); + + if (card) { + const actionsEl = card.querySelector( + '.tachyon_updates-page__component__actions', + ); + actionsEl?.appendChild(dropdown); + } + + try { + const response = await TachyonShellMethods.componentListReleases( + component, + 3, + ); + + if (!response.success) { + dropdown.innerHTML = ''; + dropdown.appendChild( + E( + 'div', + { class: 'tachyon-version-picker__error' }, + response.error || _('No versions found'), + ), + ); + return; + } + + if (!response.data || response.data.length === 0) { + dropdown.innerHTML = ''; + dropdown.appendChild( + E( + 'div', + { class: 'tachyon-version-picker__error' }, + _('No versions found'), + ), + ); + return; + } + + dropdown.innerHTML = ''; + const list = E('div', { class: 'tachyon-version-picker__list' }); + + for (const release of response.data) { + const children: Node[] = [ + E('span', { class: 'tachyon-version-picker__tag' }, release.tag), + E('span', { class: 'tachyon-version-picker__date' }, release.published), + ]; + if (release.prerelease) { + children.push( + E( + 'span', + { class: 'tachyon-version-picker__prerelease' }, + _('pre-release'), + ), + ); + } + children.push( + renderButton({ + text: _('Install'), + loading: false, + disabled: false, + onClick: () => void installVersion(component, release.tag, dropdown!), + }), + ); + const item = E( + 'div', + { class: 'tachyon-version-picker__item' }, + children, + ); + list.appendChild(item); + } + + dropdown.appendChild(list); + dropdown.appendChild( + E( + 'button', + { + class: 'cbi-button tachyon-version-picker__close', + click: () => dropdown?.remove(), + }, + _('Close'), + ), + ); + } catch (_error) { + dropdown.innerHTML = ''; + dropdown.appendChild( + E( + 'div', + { class: 'tachyon-version-picker__error' }, + _('Failed to load versions'), + ), + ); + } +} + +async function installVersion( + component: Tachyon.ComponentName, + tag: string, + dropdown: HTMLElement, +) { + const loadingEl = dropdown.querySelector( + '.tachyon-version-picker__loading', + ) as HTMLElement | null; + if (loadingEl) { + loadingEl.textContent = _('Installing ') + tag + '...'; + } + + try { + const response = await TachyonShellMethods.componentInstallVersion( + component, + tag, + ); + if (response?.success) { + showToast(_('Installed ') + tag, 'success'); + dropdown.remove(); + window.location.reload(); + } else { + showToast(_('Installation failed: ') + (response?.error || ''), 'error'); + } + } catch (_error) { + showToast(_('Installation failed'), 'error'); + } +} + function renderComponentCard(card: ComponentCard) { const updatesActions = store.get().updatesActions; const anyActionLoading = isAnyActionLoading(); @@ -1576,6 +1731,20 @@ function renderComponentCard(card: ComponentCard) { ); } + if (card.supportsVersions) { + const versionsButton = renderButton({ + text: _('Versions'), + loading: false, + disabled: systemInfoLoading || anyActionLoading, + onClick: () => void showVersionPicker(card.component), + }); + actionElements.push( + E('div', { class: 'tachyon_updates-page__component__versions' }, [ + versionsButton, + ]), + ); + } + const actionsContainer = E( 'div', { diff --git a/fe-app-tachyon/src/tachyon/tabs/updates/styles.ts b/fe-app-tachyon/src/tachyon/tabs/updates/styles.ts index e1338619..8fe6450d 100644 --- a/fe-app-tachyon/src/tachyon/tabs/updates/styles.ts +++ b/fe-app-tachyon/src/tachyon/tabs/updates/styles.ts @@ -314,4 +314,65 @@ export const styles = ` flex: 1 1 auto; min-height: 0; } + +.tachyon-version-picker { + margin-top: 8px; + padding: 8px; + border: 1px solid var(--border-color, rgba(255,255,255,0.12)); + border-radius: 4px; + background: rgba(0, 0, 0, 0.2); +} + +.tachyon-version-picker__loading, +.tachyon-version-picker__error { + padding: 6px 0; + font-size: 12px; + color: var(--text-color, #aaa); +} + +.tachyon-version-picker__error { + color: var(--error-color, #e74c3c); +} + +.tachyon-version-picker__list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.tachyon-version-picker__item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06)); +} + +.tachyon-version-picker__item:last-child { + border-bottom: none; +} + +.tachyon-version-picker__tag { + font-weight: bold; + font-size: 13px; + color: var(--text-color, #ddd); +} + +.tachyon-version-picker__date { + font-size: 11px; + color: var(--text-color, #aaa); +} + +.tachyon-version-picker__prerelease { + font-size: 10px; + color: var(--warn-color, #f39c12); + border: 1px solid var(--warn-color, #f39c12); + border-radius: 3px; + padding: 1px 4px; +} + +.tachyon-version-picker__close { + margin-top: 8px; + width: 100%; +} `; diff --git a/fe-app-tachyon/src/tachyon/types.ts b/fe-app-tachyon/src/tachyon/types.ts index 6ffeb69f..96ca98db 100644 --- a/fe-app-tachyon/src/tachyon/types.ts +++ b/fe-app-tachyon/src/tachyon/types.ts @@ -95,6 +95,8 @@ export namespace Tachyon { COMPONENT_ACTION_STATUS = 'component_action_status', COMPONENT_ACTION_LOG = 'component_action_log', COMPONENT_UPDATE_CHECK_CACHE = 'component_update_check_cache', + COMPONENT_LIST_RELEASES = 'component_list_releases', + COMPONENT_INSTALL_VERSION = 'component_install_version', SUBSCRIPTION_UPDATE_ASYNC = 'subscription_update_async', SUBSCRIPTION_UPDATE_STATUS = 'subscription_update_status', SERVICE_HEALTH_CHECK = 'service_health_check', @@ -723,6 +725,14 @@ export namespace Tachyon { offset: number; } + export interface ComponentRelease { + tag: string; + name: string; + published: string; + prerelease: boolean; + release_url: string; + } + export type ComponentActionStartResult = UiActionStartResult; export type SubscriptionUpdateStartResult = UiActionStartResult; diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index 4a31fc45..721663ff 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -1054,7 +1054,7 @@ var Logger = class { } download(filename = "logs.txt") { if (typeof document === "undefined") { - console.warn("Logger.download() \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435"); + console.warn("Logger.download() available only in browser"); return; } downloadAsTxt(this.getLogs(), filename); @@ -1095,8 +1095,8 @@ var DOMAIN_LIST_OPTIONS = { russia_inside: "Russia inside", russia_outside: "Russia outside", ukraine_inside: "Ukraine", - geoblock: "Geo Block (\u0441\u043D\u0430\u0440\u0443\u0436\u0438 \u0420\u0424)", - block: "Block (\u0420\u041A\u041D / \u0432\u043D\u0443\u0442\u0440\u0438 \u0420\u0424)", + geoblock: "Geo Block", + block: "Block (RKN)", porn: "Porn", news: "News", anime: "Anime", @@ -3182,6 +3182,8 @@ var Tachyon; AvailableMethods2["COMPONENT_ACTION_STATUS"] = "component_action_status"; AvailableMethods2["COMPONENT_ACTION_LOG"] = "component_action_log"; AvailableMethods2["COMPONENT_UPDATE_CHECK_CACHE"] = "component_update_check_cache"; + AvailableMethods2["COMPONENT_LIST_RELEASES"] = "component_list_releases"; + AvailableMethods2["COMPONENT_INSTALL_VERSION"] = "component_install_version"; AvailableMethods2["SUBSCRIPTION_UPDATE_ASYNC"] = "subscription_update_async"; AvailableMethods2["SUBSCRIPTION_UPDATE_STATUS"] = "subscription_update_status"; AvailableMethods2["SERVICE_HEALTH_CHECK"] = "service_health_check"; @@ -3285,6 +3287,29 @@ function parseJsonObjectOutput(output) { } } } +function parseJsonArrayOutput(output) { + if (!output) { + return null; + } + try { + const parsed = JSON.parse(output); + if (Array.isArray(parsed)) { + return parsed; + } + return null; + } catch (_error) { + const jsonMatch = output.match(/(\[[\s\S]*\])\s*$/); + if (!jsonMatch) { + return null; + } + try { + const parsed = JSON.parse(jsonMatch[1]); + return Array.isArray(parsed) ? parsed : null; + } catch (_jsonError) { + return null; + } + } +} function parseComponentActionOutput(output) { return parseJsonObjectOutput(output); } @@ -3745,6 +3770,54 @@ var TachyonShellMethods = { componentUpdateCheckCache: async () => callBaseMethod( Tachyon.AvailableMethods.COMPONENT_UPDATE_CHECK_CACHE ), + componentListReleases: async (component, count = 3) => { + const response = await executeShellCommand({ + command: "/usr/bin/tachyon", + args: [ + Tachyon.AvailableMethods.COMPONENT_LIST_RELEASES, + component, + String(count) + ], + timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS + }); + const parsed = parseJsonArrayOutput( + response.stdout + ); + if ((response.code ?? 0) !== 0 || !parsed) { + return { + success: false, + error: response.stderr || _("Failed to fetch releases") + }; + } + return { + success: true, + data: parsed + }; + }, + componentInstallVersion: async (component, tag) => { + const response = await executeShellCommand({ + command: "/usr/bin/tachyon", + args: [ + Tachyon.AvailableMethods.COMPONENT_INSTALL_VERSION, + component, + tag + ], + timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS + }); + const parsed = parseJsonObjectOutput( + response.stdout + ); + if ((response.code ?? 0) !== 0 || !parsed) { + return { + success: false, + error: response.stderr || _("Failed to install version") + }; + } + return { + success: true, + data: parsed + }; + }, waitComponentActionJob: async (jobId, component, action, expectedLatestVersion) => { const jobStartedAt = Date.now(); const isSelfUpdate = component === "tachyon" && (action === "install" || action === "reinstall"); @@ -20211,7 +20284,8 @@ function getComponentCards() { latestVersion: getLatestVersion("tachyon"), releaseUrl: getGitHubReleaseUrl("tachyon"), repoUrl: COMPONENT_REPO_URLS.tachyon, - actions: tachyonActions + actions: tachyonActions, + supportsVersions: true }, { component: "sing_box", @@ -20222,7 +20296,8 @@ function getComponentCards() { latestVersion: getLatestVersion("sing_box"), releaseUrl: getGitHubReleaseUrl("sing_box"), repoUrl: systemInfo.sing_box_repo_url || (singBoxLx ? "https://github.com/Leadaxe/sing-box-lx" : singBoxExtended || singBoxExtendedCompressed ? "https://github.com/shtorm-7/sing-box-extended" : COMPONENT_REPO_URLS.sing_box), - actions: singBoxActions + actions: singBoxActions, + supportsVersions: true }, { component: "zapret", @@ -20232,7 +20307,8 @@ function getComponentCards() { latestVersion: getLatestVersion("zapret"), releaseUrl: getGitHubReleaseUrl("zapret"), repoUrl: COMPONENT_REPO_URLS.zapret, - actions: zapretActions + actions: zapretActions, + supportsVersions: true }, { component: "zapret2", @@ -20242,7 +20318,8 @@ function getComponentCards() { latestVersion: getLatestVersion("zapret2"), releaseUrl: getGitHubReleaseUrl("zapret2"), repoUrl: COMPONENT_REPO_URLS.zapret2, - actions: zapret2Actions + actions: zapret2Actions, + supportsVersions: true }, { component: "byedpi", @@ -20266,6 +20343,139 @@ function getComponentCards() { } ]; } +async function showVersionPicker(component) { + const container = document.getElementById("tachyon_updates-components"); + if (!container) return; + let dropdown = container.querySelector( + ".tachyon-version-picker" + ); + if (dropdown) { + dropdown.remove(); + return; + } + dropdown = E("div", { class: "tachyon-version-picker" }, [ + E( + "div", + { class: "tachyon-version-picker__loading" }, + _("Loading versions...") + ) + ]); + const componentCards = container.querySelectorAll( + `.tachyon_updates-page__component` + ); + const card = Array.from(componentCards).find((el) => { + const title = el.querySelector(".tachyon_updates-page__component__title"); + return title?.textContent === getComponentCardTitle(component); + }); + if (card) { + const actionsEl = card.querySelector( + ".tachyon_updates-page__component__actions" + ); + actionsEl?.appendChild(dropdown); + } + try { + const response = await TachyonShellMethods.componentListReleases( + component, + 3 + ); + if (!response.success) { + dropdown.innerHTML = ""; + dropdown.appendChild( + E( + "div", + { class: "tachyon-version-picker__error" }, + response.error || _("No versions found") + ) + ); + return; + } + if (!response.data || response.data.length === 0) { + dropdown.innerHTML = ""; + dropdown.appendChild( + E( + "div", + { class: "tachyon-version-picker__error" }, + _("No versions found") + ) + ); + return; + } + dropdown.innerHTML = ""; + const list = E("div", { class: "tachyon-version-picker__list" }); + for (const release of response.data) { + const children = [ + E("span", { class: "tachyon-version-picker__tag" }, release.tag), + E("span", { class: "tachyon-version-picker__date" }, release.published) + ]; + if (release.prerelease) { + children.push( + E( + "span", + { class: "tachyon-version-picker__prerelease" }, + _("pre-release") + ) + ); + } + children.push( + renderButton({ + text: _("Install"), + loading: false, + disabled: false, + onClick: () => void installVersion(component, release.tag, dropdown) + }) + ); + const item = E( + "div", + { class: "tachyon-version-picker__item" }, + children + ); + list.appendChild(item); + } + dropdown.appendChild(list); + dropdown.appendChild( + E( + "button", + { + class: "cbi-button tachyon-version-picker__close", + click: () => dropdown?.remove() + }, + _("Close") + ) + ); + } catch (_error) { + dropdown.innerHTML = ""; + dropdown.appendChild( + E( + "div", + { class: "tachyon-version-picker__error" }, + _("Failed to load versions") + ) + ); + } +} +async function installVersion(component, tag, dropdown) { + const loadingEl = dropdown.querySelector( + ".tachyon-version-picker__loading" + ); + if (loadingEl) { + loadingEl.textContent = _("Installing ") + tag + "..."; + } + try { + const response = await TachyonShellMethods.componentInstallVersion( + component, + tag + ); + if (response?.success) { + showToast(_("Installed ") + tag, "success"); + dropdown.remove(); + window.location.reload(); + } else { + showToast(_("Installation failed: ") + (response?.error || ""), "error"); + } + } catch (_error) { + showToast(_("Installation failed"), "error"); + } +} function renderComponentCard(card) { const updatesActions = store.get().updatesActions; const anyActionLoading = isAnyActionLoading(); @@ -20468,6 +20678,19 @@ function renderComponentCard(card) { ]) ); } + if (card.supportsVersions) { + const versionsButton = renderButton({ + text: _("Versions"), + loading: false, + disabled: systemInfoLoading || anyActionLoading, + onClick: () => void showVersionPicker(card.component) + }); + actionElements.push( + E("div", { class: "tachyon_updates-page__component__versions" }, [ + versionsButton + ]) + ); + } const actionsContainer = E( "div", { @@ -20917,6 +21140,67 @@ var styles6 = ` flex: 1 1 auto; min-height: 0; } + +.tachyon-version-picker { + margin-top: 8px; + padding: 8px; + border: 1px solid var(--border-color, rgba(255,255,255,0.12)); + border-radius: 4px; + background: rgba(0, 0, 0, 0.2); +} + +.tachyon-version-picker__loading, +.tachyon-version-picker__error { + padding: 6px 0; + font-size: 12px; + color: var(--text-color, #aaa); +} + +.tachyon-version-picker__error { + color: var(--error-color, #e74c3c); +} + +.tachyon-version-picker__list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.tachyon-version-picker__item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06)); +} + +.tachyon-version-picker__item:last-child { + border-bottom: none; +} + +.tachyon-version-picker__tag { + font-weight: bold; + font-size: 13px; + color: var(--text-color, #ddd); +} + +.tachyon-version-picker__date { + font-size: 11px; + color: var(--text-color, #aaa); +} + +.tachyon-version-picker__prerelease { + font-size: 10px; + color: var(--warn-color, #f39c12); + border: 1px solid var(--warn-color, #f39c12); + border-radius: 3px; + padding: 1px 4px; +} + +.tachyon-version-picker__close { + margin-top: 8px; + width: 100%; +} `; // src/tachyon/tabs/updates/index.ts diff --git a/luci-app-tachyon/po/ru/tachyon.po b/luci-app-tachyon/po/ru/tachyon.po index 89fe7bc5..52495de3 100644 --- a/luci-app-tachyon/po/ru/tachyon.po +++ b/luci-app-tachyon/po/ru/tachyon.po @@ -5725,3 +5725,30 @@ msgstr "CleanBrowsing (Семейный)" msgid "Yandex Safe" msgstr "Yandex (Безопасный)" + +msgid "Versions" +msgstr "Версии" + +msgid "Loading versions..." +msgstr "Загрузка версий..." + +msgid "No versions found" +msgstr "Версии не найдены" + +msgid "Failed to load versions" +msgstr "Не удалось загрузить версии" + +msgid "Installing " +msgstr "Установка " + +msgid "Installation failed" +msgstr "Ошибка установки" + +msgid "Installation failed: " +msgstr "Ошибка установки: " + +msgid "Installed " +msgstr "Установлено " + +msgid "pre-release" +msgstr "пре-релиз" diff --git a/tachyon/files/usr/bin/tachyon b/tachyon/files/usr/bin/tachyon index 8095f8f1..62e424fa 100755 --- a/tachyon/files/usr/bin/tachyon +++ b/tachyon/files/usr/bin/tachyon @@ -127,6 +127,10 @@ function show_help() { print(" component_action_status Get background component action status\n"); print(" component_action_log [job_id] [offset]\n"); print(" Get background component action log from offset\n"); + print(" component_list_releases [count]\n"); + print(" List last N releases for a component (default 3)\n"); + print(" component_install_version \n"); + print(" Install a specific component release version\n"); print(" component_updates_if_due Check installed components for updates when due\n"); print(" component_update_check_cache Get cached component update check results\n"); print(" hosts_list_update [url] Update custom hosts lists (all or specific URL)\n"); @@ -231,6 +235,8 @@ function command_spec(command) { component_action_async: [ "components/updates.uc", "component-action-async", 2 ], component_action_status: [ "components/updates.uc", "component-action-status", 1 ], component_action_log: [ "components/updates.uc", "component-action-log", 2 ], + component_list_releases: [ "components/action.uc", "list-component-releases", 2 ], + component_install_version: [ "components/action.uc", "install-component-version", 2 ], component_updates_if_due: [ "components/updates.uc", "component-updates-if-due", 0 ], component_auto_update_apply: [ "components/updates.uc", "component-auto-update-apply", 0 ], component_update_check_cache: [ "components/updates.uc", "component-update-check-cache", 0 ], diff --git a/tachyon/files/usr/lib/components/action.uc b/tachyon/files/usr/lib/components/action.uc index a3369a7d..7447971f 100644 --- a/tachyon/files/usr/lib/components/action.uc +++ b/tachyon/files/usr/lib/components/action.uc @@ -2840,10 +2840,201 @@ function component_action(component, action) { action_fail(component != "" ? component : "unknown", action != "" ? action : "unknown", "Unknown component action"); } +function list_component_releases(component, count) { + component = normalize_component_name(component); + count = int(count || 3); + if (count < 1) count = 1; + if (count > 10) count = 10; + + let owner = ""; + let repo = ""; + let per_page = as_string(count); + + if (component == "tachyon") { + let parts = split(TACHYON_RELEASE_REPO, "/"); + if (length(parts) != 2) { print("[]"); return; } + owner = parts[0]; repo = parts[1]; + } else if (component == "sing_box") { + let variant = sing_box_runtime_output("variant", []); + if (variant == "lx") { owner = "Leadaxe"; repo = "sing-box-lx"; } + else if (variant == "extended" || variant == "extended-compressed") { owner = "shtorm-7"; repo = "sing-box-extended"; } + else { owner = "SagerNet"; repo = "sing-box"; } + } else if (component == "zapret") { + owner = "remittor"; repo = "zapret-openwrt"; + } else if (component == "zapret2") { + owner = "Dushnilin"; repo = "zapret2-openwrt"; + } else if (component == "byedpi") { + owner = "DPITrickster"; repo = "ByeDPI-OpenWrt"; + } else { + print("[]"); return; + } + + let releases_json = fetch_github_releases_json(owner, repo, per_page); + if (releases_json == "") { print("[]"); return; } + + let releases = []; + try { + let parsed = json(releases_json); + if (type(parsed) == "array") { + for (let r in parsed) { + let tag = trim(as_string(r.tag_name || "")); + let name = trim(as_string(r.name || tag)); + let published = trim(as_string(r.published_at || r.created_at || "")); + let html_url = trim(as_string(r.html_url || "")); + if (tag == "") continue; + let prerelease = !!r.prerelease; + push(releases, { tag, name, published, prerelease, release_url: html_url }); + } + } + } catch (e) {} + + write_json(releases); +} + +function resolve_component_release(component, tag) { + component = normalize_component_name(component); + tag = trim(as_string(tag)); + if (tag == "") return null; + + if (component == "tachyon") { + let parts = split(TACHYON_RELEASE_REPO, "/"); + if (length(parts) != 2) return null; + let base_dl = "https://github.com/" + parts[0] + "/" + parts[1] + "/releases/download/" + tag + "/"; + let ext = is_apk() ? "apk" : "ipk"; + let tag_clean = replace(tag, /^v/, ""); + return { + tag, + packages: [ + base_dl + "tachyon_" + tag_clean + "." + ext, + base_dl + "luci-app-tachyon_" + tag_clean + "." + ext, + base_dl + "luci-i18n-tachyon-ru_" + tag_clean + "." + ext + ] + }; + } + + if (component == "sing_box") { + let variant = sing_box_runtime_output("variant", []); + let tag_clean = replace(tag, /^v/, ""); + let base_dl = ""; + + if (variant == "lx") { + let arch_suffix = resolve_sing_box_extended_arch_suffix(); + if (arch_suffix == "") return null; + base_dl = "https://github.com/Leadaxe/sing-box-lx/releases/download/" + tag + "/"; + let asset_name = "sing-box-lx_" + tag_clean + "_linux-" + arch_suffix + ".tar.gz"; + return { tag, packages: [ base_dl + asset_name ] }; + } else if (variant == "extended" || variant == "extended-compressed") { + let compressed = (variant == "extended-compressed"); + base_dl = "https://github.com/shtorm-7/sing-box-extended/releases/download/" + tag + "/"; + if (compressed) { + let arch_suffix = resolve_sing_box_extended_arch_suffix(); + if (arch_suffix == "") return null; + return { tag, packages: [ base_dl + "sing-box-extended_" + tag_clean + "_linux-" + arch_suffix + "-compressed.tar.gz" ] }; + } else { + let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH"); + if (distrib_arch == "") return null; + let asset_ext = is_apk() ? "apk" : "ipk"; + return { tag, packages: [ base_dl + "sing-box-extended_" + tag_clean + "_openwrt_" + distrib_arch + "." + asset_ext ] }; + } + } else { + let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH"); + if (distrib_arch == "") return null; + let asset_ext = is_apk() ? "apk" : "ipk"; + base_dl = "https://github.com/SagerNet/sing-box/releases/download/" + tag + "/"; + return { tag, packages: [ base_dl + "sing-box_" + tag_clean + "_openwrt_" + distrib_arch + "." + asset_ext ] }; + } + } + + if (component == "zapret") { + let arch = resolve_arch_candidates(); + if (arch == null) return null; + let base_dl = "https://github.com/remittor/zapret-openwrt/releases/download/" + tag + "/"; + let tag_clean = replace(tag, /^v/, ""); + return { tag, packages: [ base_dl + "zapret_" + tag_clean + "_openwrt_" + arch.candidates + ".zip" ] }; + } + + if (component == "zapret2") { + let arch = resolve_arch_candidates(); + if (arch == null) return null; + let base_dl = "https://github.com/Dushnilin/zapret2-openwrt/releases/download/" + tag + "/"; + let tag_clean = replace(tag, /^v/, ""); + let ext = is_apk() ? "apk" : "ipk"; + return { tag, packages: [ base_dl + "zapret2_" + tag_clean + "_openwrt_" + arch.candidates + "." + ext ] }; + } + + if (component == "byedpi") { + let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH"); + if (distrib_arch == "") return null; + let tag_clean = replace(tag, /^v/, ""); + let base_dl = "https://github.com/DPITrickster/ByeDPI-OpenWrt/releases/download/" + tag + "/"; + let ext = is_apk() ? "apk" : "ipk"; + return { tag, packages: [ base_dl + "byedpi_" + tag_clean + "_openwrt_" + distrib_arch + "." + ext ] }; + } + + return null; +} + +function install_component_version(component, tag) { + component = normalize_component_name(component); + tag = trim(as_string(tag)); + if (tag == "" || component == "") { + updates_response(false, component, "install_version", "Invalid component or tag", "", "", 0, "", "", null); + cleanup_action(); + return; + } + + let plan = resolve_component_release(component, tag); + if (plan == null || plan.packages == null || length(plan.packages) == 0) { + updates_response(false, component, "install_version", "Failed to resolve release assets for " + component + " " + tag, "", "", 0, "", "", null); + cleanup_action(); + return; + } + + let installed = 0; + let failed = 0; + for (let url in plan.packages) { + let fname = path_basename(url); + let args = [ "curl", "-sL", "--connect-timeout", "10", "-m", "120", "-o", fname ]; + let proxy_addr = service_proxy_address(); + if (proxy_addr != "") { + push(args, "-x"); + push(args, "http://" + proxy_addr); + } + push(args, url); + let dl_ok = run_logged("Downloading " + fname, command_from_args(args)); + if (!dl_ok || !file_exists(fname)) { + failed++; + continue; + } + let inst_ok = run_logged("Installing " + fname, pkg_install_files_command([ fname ], false)); + if (inst_ok) installed++; + else failed++; + remove_file(fname); + } + + if (component == "sing_box") { + write_file("/etc/tachyon/sing-box-version", tag + "\n"); + let variant = sing_box_runtime_output("variant", []); + write_file("/etc/tachyon/sing-box-variant", variant + "\n"); + } + + let success = installed > 0 && failed == 0; + let msg = installed + " packages installed"; + if (failed > 0) msg += ", " + failed + " failed"; + + updates_response(success, component, "install_version", msg, "", tag, success ? 1 : 0, success ? "latest" : "", "", null); + if (success) restart_tachyon_after_successful_change(); + cleanup_action(); +} + let mode = ARGV[0] || ""; if (mode == "component-action") component_action(ARGV[1], ARGV[2]); +else if (mode == "list-component-releases") + list_component_releases(ARGV[1], ARGV[2]); +else if (mode == "install-component-version") + install_component_version(ARGV[1], ARGV[2]); else if (mode == "latest-tachyon-release-json") print(latest_tachyon_release_json()); else if (mode == "latest-tachyon-version") @@ -2853,6 +3044,6 @@ else if (mode == "tachyon-release-metadata") else if (mode == "pkg-install-files-command") print(pkg_install_files_command(slice(ARGV, 2), ARGV[1] == "1"), "\n"); else { - warn("Usage: components/action.uc ...\n"); + warn("Usage: components/action.uc ...\n"); exit(1); } \ No newline at end of file