feat: component version picker — list last 3 releases, install specific version (1.3.20)
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -51,6 +51,33 @@ function parseJsonObjectOutput<T>(output: string): T | null {
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonArrayOutput<T>(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<Tachyon.ComponentActionResult>(output);
|
||||
}
|
||||
@@ -675,6 +702,64 @@ export const TachyonShellMethods = {
|
||||
callBaseMethod<Tachyon.ComponentUpdateCheckCache>(
|
||||
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<Tachyon.ComponentRelease>(
|
||||
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<Tachyon.ComponentRelease[]>;
|
||||
},
|
||||
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<Tachyon.ComponentActionResult>(
|
||||
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<Tachyon.ComponentActionResult>;
|
||||
},
|
||||
waitComponentActionJob: async (
|
||||
jobId: string,
|
||||
component: Tachyon.ComponentName,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
{
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "пре-релиз"
|
||||
|
||||
@@ -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 <component> [count]\n");
|
||||
print(" List last N releases for a component (default 3)\n");
|
||||
print(" component_install_version <component> <tag>\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 ],
|
||||
|
||||
@@ -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 <component-action|latest-tachyon-version|tachyon-release-metadata> ...\n");
|
||||
warn("Usage: components/action.uc <component-action|list-component-releases|install-component-version|latest-tachyon-version|tachyon-release-metadata> ...\n");
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user