mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-09-14 20:34:11 +08:00
1911 lines
64 KiB
HTML
1911 lines
64 KiB
HTML
<!--
|
||
SPDX-License-Identifier: GPL-3.0-only
|
||
Copyright (C) 2026 smallprogram
|
||
Repository: https://github.com/smallprogram/luci-app-ghfu
|
||
-->
|
||
|
||
<%+header%>
|
||
|
||
<style>
|
||
/* ── CSS变量:亮色主题 ─────────────────────────────── */
|
||
:root {
|
||
--ghfu-text: #1e293b;
|
||
--ghfu-text-muted: #475569;
|
||
--ghfu-border: #d1d5db;
|
||
--ghfu-bg-card: transparent;
|
||
--ghfu-bg-status: #eff6ff;
|
||
--ghfu-border-status: #93c5fd;
|
||
--ghfu-text-status: #1e3a8a;
|
||
--ghfu-bg-assets: transparent;
|
||
--ghfu-border-assets: #d1d5db;
|
||
--ghfu-divider: #e5e7eb;
|
||
--ghfu-log-bg: #0f172a;
|
||
--ghfu-log-text: #dbeafe;
|
||
--ghfu-log-border: #334155;
|
||
--ghfu-ok: #16a34a;
|
||
--ghfu-new: #dc2626;
|
||
--ghfu-warn: #d97706;
|
||
--ghfu-modal-bg: #ffffff;
|
||
--ghfu-modal-text: #1e293b;
|
||
--ghfu-modal-muted: #475569;
|
||
--ghfu-spinner-track: #e2e8f0;
|
||
--ghfu-spinner-head: #2563eb;
|
||
--ghfu-overlay: rgba(15, 23, 42, 0.70);
|
||
--ghfu-accent: #2563eb;
|
||
--ghfu-danger: #ef4444;
|
||
--ghfu-card-shadow: 0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.04);
|
||
--ghfu-row-hover: rgba(148, 163, 184, 0.13);
|
||
--ghfu-row-selected: rgba(37, 99, 235, 0.09);
|
||
--ghfu-input-ro-bg: rgba(148, 163, 184, 0.07);
|
||
}
|
||
|
||
/* ── 暗色主题变量覆盖(JS检测后设置 data-ghfu-dark 属性触发) ── */
|
||
#ghfu-root[data-ghfu-dark] {
|
||
--ghfu-text: #e2e8f0;
|
||
--ghfu-text-muted: #94a3b8;
|
||
--ghfu-border: #334155;
|
||
--ghfu-bg-status: #1e2d40;
|
||
--ghfu-border-status: #3b82f6;
|
||
--ghfu-text-status: #93c5fd;
|
||
--ghfu-border-assets: #334155;
|
||
--ghfu-divider: #1e293b;
|
||
--ghfu-log-bg: #0a0f1e;
|
||
--ghfu-log-text: #bfdbfe;
|
||
--ghfu-log-border: #1e3a5f;
|
||
--ghfu-ok: #4ade80;
|
||
--ghfu-new: #f87171;
|
||
--ghfu-warn: #fbbf24;
|
||
--ghfu-modal-bg: #1e293b;
|
||
--ghfu-modal-text: #e2e8f0;
|
||
--ghfu-modal-muted: #94a3b8;
|
||
--ghfu-spinner-track: #334155;
|
||
--ghfu-spinner-head: #60a5fa;
|
||
--ghfu-overlay: rgba(0, 0, 0, 0.80);
|
||
--ghfu-accent: #60a5fa;
|
||
--ghfu-danger: #fb7185;
|
||
--ghfu-card-shadow: 0 2px 8px rgba(0,0,0,0.35);
|
||
--ghfu-row-hover: rgba(148, 163, 184, 0.10);
|
||
--ghfu-row-selected: rgba(96, 165, 250, 0.14);
|
||
--ghfu-input-ro-bg: rgba(148, 163, 184, 0.05);
|
||
}
|
||
|
||
/* ── 组件样式(全部使用CSS变量)─────────────────────── */
|
||
#ghfu-root {
|
||
max-width: none;
|
||
margin: 0;
|
||
padding: 14px 16px 12px;
|
||
}
|
||
|
||
#ghfu-root.ghfu-input-locked {
|
||
pointer-events: none;
|
||
user-select: none;
|
||
}
|
||
|
||
#ghfu-root h2 {
|
||
margin: 0 0 8px;
|
||
color: var(--ghfu-text);
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
#ghfu-root .ghfu-subtitle {
|
||
color: var(--ghfu-text-muted);
|
||
margin: 0 0 14px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
#ghfu-root .cbi-value-title {
|
||
color: var(--ghfu-text) !important;
|
||
font-weight: 600;
|
||
}
|
||
|
||
#ghfu-root .ghfu-wide {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.ghfu-layout {
|
||
display: grid;
|
||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||
grid-auto-rows: minmax(0, auto);
|
||
gap: 16px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.ghfu-card {
|
||
border: 1px solid var(--ghfu-border);
|
||
border-radius: 12px;
|
||
background: var(--ghfu-bg-card);
|
||
padding: 16px;
|
||
box-shadow: var(--ghfu-card-shadow), 0 6px 18px rgba(15, 23, 42, 0.08);
|
||
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
|
||
}
|
||
|
||
.ghfu-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: var(--ghfu-card-shadow), 0 20px 38px rgba(15, 23, 42, 0.22);
|
||
border-color: color-mix(in srgb, var(--ghfu-border) 65%, var(--ghfu-accent) 35%);
|
||
}
|
||
|
||
.ghfu-card-title {
|
||
margin: 0 0 14px;
|
||
padding: 2px 0 10px 12px;
|
||
border-left: 3px solid var(--ghfu-accent);
|
||
border-bottom: 1px solid var(--ghfu-divider);
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.02em;
|
||
color: var(--ghfu-text);
|
||
}
|
||
|
||
.ghfu-card .cbi-value {
|
||
margin: 0 0 10px;
|
||
}
|
||
|
||
.ghfu-card .cbi-value:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.ghfu-card-basic .cbi-value {
|
||
display: grid;
|
||
grid-template-columns: 150px minmax(0, 1fr);
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.ghfu-card-basic .cbi-value-title {
|
||
margin: 0;
|
||
text-align: left;
|
||
}
|
||
|
||
.ghfu-card-basic .cbi-value-field,
|
||
.ghfu-card-basic .cbi-input-text {
|
||
width: 100%;
|
||
min-width: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.ghfu-card-status { grid-column: 1 / span 5; grid-row: 1; }
|
||
.ghfu-card-basic { grid-column: 6 / span 5; grid-row: 1; }
|
||
.ghfu-card-log { grid-column: 1 / span 6; grid-row: 2; }
|
||
.ghfu-card-assets { grid-column: 7 / span 4; grid-row: 2; }
|
||
|
||
.ghfu-card-log,
|
||
.ghfu-card-assets {
|
||
min-height: 600px;
|
||
height: 600px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.ghfu-card-log > .cbi-value-field,
|
||
.ghfu-card-assets > .cbi-value-field {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
display: flex;
|
||
}
|
||
|
||
.ghfu-card-log > .cbi-value-field {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.ghfu-card-assets > .cbi-value-field {
|
||
flex-direction: column;
|
||
}
|
||
|
||
#ghfu_status {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
box-sizing: border-box;
|
||
border: 1px solid var(--ghfu-border-status);
|
||
border-radius: 8px;
|
||
background: linear-gradient(145deg, var(--ghfu-bg-status) 0%, rgba(148, 163, 184, 0.08) 100%);
|
||
padding: 10px 14px;
|
||
color: var(--ghfu-text-status);
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.ghfu-status-hero {
|
||
border-radius: 8px;
|
||
padding: 8px 10px;
|
||
margin-bottom: 10px;
|
||
font-weight: 700;
|
||
border: 1px solid transparent;
|
||
background: rgba(37, 99, 235, 0.08);
|
||
color: var(--ghfu-text-status);
|
||
}
|
||
|
||
.ghfu-status-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.ghfu-status-item {
|
||
border: 1px dashed var(--ghfu-border-status);
|
||
border-radius: 8px;
|
||
padding: 6px 8px;
|
||
background: rgba(255, 255, 255, 0.20);
|
||
color: var(--ghfu-text-status);
|
||
min-width: 0;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.ghfu-status-grid .ghfu-status-item:last-child {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
#ghfu_assets {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
max-height: 280px;
|
||
overflow: auto;
|
||
border: 1px solid var(--ghfu-border-assets);
|
||
border-radius: 8px;
|
||
padding: 4px 6px;
|
||
background: var(--ghfu-bg-assets);
|
||
}
|
||
|
||
#ghfu_assets .ghfu-asset-row {
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
gap: 10px;
|
||
padding: 8px 10px;
|
||
margin: 2px 0;
|
||
border: 1px solid transparent;
|
||
border-radius: 7px;
|
||
cursor: pointer;
|
||
transition: background 0.14s, border-color 0.14s;
|
||
color: var(--ghfu-text);
|
||
text-decoration: none;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
#ghfu_assets .ghfu-asset-row:hover {
|
||
background: var(--ghfu-row-hover);
|
||
}
|
||
|
||
#ghfu_assets .ghfu-asset-row.ghfu-row-selected {
|
||
background: var(--ghfu-row-selected);
|
||
border-color: var(--ghfu-accent);
|
||
}
|
||
|
||
#ghfu_assets .ghfu-asset-row input[type="radio"] {
|
||
flex: 0 0 16px;
|
||
width: 16px;
|
||
height: 16px;
|
||
margin: 0;
|
||
padding: 0;
|
||
cursor: pointer;
|
||
accent-color: var(--ghfu-accent);
|
||
pointer-events: none;
|
||
}
|
||
|
||
#ghfu_assets .ghfu-asset-name {
|
||
color: var(--ghfu-text);
|
||
flex: 1;
|
||
min-width: 0;
|
||
word-break: break-word;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
#ghfu_log {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
min-height: 260px;
|
||
height: 260px;
|
||
display: block;
|
||
box-sizing: border-box;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--ghfu-log-border);
|
||
background: var(--ghfu-log-bg);
|
||
color: var(--ghfu-log-text);
|
||
font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.ghfu-status-line { margin: 3px 0; }
|
||
|
||
#ghfu_release {
|
||
background-color: var(--ghfu-input-ro-bg) !important;
|
||
color: var(--ghfu-text-muted) !important;
|
||
}
|
||
.ghfu-conclusion-ok { color: var(--ghfu-ok); font-weight: 700; }
|
||
.ghfu-conclusion-new { color: var(--ghfu-new); font-weight: 700; }
|
||
.ghfu-conclusion-warn { color: var(--ghfu-warn); font-weight: 700; }
|
||
|
||
.ghfu-status-hero.ghfu-conclusion-ok {
|
||
border-color: rgba(34, 197, 94, 0.35);
|
||
background: rgba(34, 197, 94, 0.10);
|
||
color: var(--ghfu-ok);
|
||
}
|
||
|
||
.ghfu-status-hero.ghfu-conclusion-new {
|
||
border-color: rgba(239, 68, 68, 0.35);
|
||
background: rgba(239, 68, 68, 0.10);
|
||
color: var(--ghfu-new);
|
||
}
|
||
|
||
.ghfu-status-hero.ghfu-conclusion-warn {
|
||
border-color: rgba(245, 158, 11, 0.35);
|
||
background: rgba(245, 158, 11, 0.10);
|
||
color: var(--ghfu-warn);
|
||
}
|
||
|
||
#ghfu-root .cbi-page-actions {
|
||
margin-top: 14px;
|
||
text-align: right;
|
||
}
|
||
|
||
.ghfu-card-actions {
|
||
margin-top: 14px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid var(--ghfu-divider);
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
}
|
||
|
||
.ghfu-card-actions .cbi-button {
|
||
margin: 0;
|
||
}
|
||
|
||
.ghfu-btn-accent {
|
||
background: #834be2 !important;
|
||
border-color: #834be2 !important;
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.ghfu-card-assets .cbi-value {
|
||
display: grid;
|
||
grid-template-columns: 140px minmax(0, 1fr);
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.ghfu-card-assets .cbi-value-title {
|
||
margin: 0;
|
||
text-align: left;
|
||
}
|
||
|
||
.ghfu-card-assets .cbi-value-field,
|
||
.ghfu-card-log .cbi-value-field {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.ghfu-card-assets > .cbi-value-field,
|
||
.ghfu-card-log > .cbi-value-field {
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
width: 100% !important;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.ghfu-card-assets #ghfu_assets,
|
||
.ghfu-card-log #ghfu_log {
|
||
width: 100% !important;
|
||
max-width: 100% !important;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.ghfu-card-log #ghfu_log {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
height: 100%;
|
||
}
|
||
|
||
.ghfu-card-assets #ghfu_assets {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
max-height: none;
|
||
height: 100%;
|
||
}
|
||
|
||
.ghfu-basic-toggle-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
flex-wrap: wrap;
|
||
line-height: 1;
|
||
}
|
||
|
||
.ghfu-basic-toggle-row div {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
cursor: pointer;
|
||
font-weight: normal;
|
||
color: var(--ghfu-text);
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ghfu-basic-toggle-row input[type="checkbox"] {
|
||
margin: 0;
|
||
width: 16px;
|
||
height: 16px;
|
||
flex: 0 0 16px;
|
||
display: block;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.ghfu-basic-toggle-row span {
|
||
line-height: 1;
|
||
}
|
||
|
||
#ghfu-root .cbi-button {
|
||
border-radius: 10px;
|
||
padding: 8px 16px;
|
||
transition: opacity 0.15s, filter 0.15s;
|
||
}
|
||
|
||
#ghfu-root .cbi-button:not(:disabled):hover {
|
||
filter: brightness(1.08);
|
||
}
|
||
|
||
#ghfu-root .cbi-button:disabled {
|
||
opacity: 0.42;
|
||
cursor: not-allowed;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
#ghfu_upgrade {
|
||
background: linear-gradient(135deg, var(--ghfu-danger) 0%, #f97316 100%) !important;
|
||
border-color: var(--ghfu-danger) !important;
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
#ghfu_upgrade:not(:disabled):hover {
|
||
filter: brightness(1.12);
|
||
}
|
||
|
||
@media (max-width: 980px) {
|
||
.ghfu-layout {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.ghfu-card-status,
|
||
.ghfu-card-basic,
|
||
.ghfu-card-log,
|
||
.ghfu-card-assets {
|
||
grid-column: 1;
|
||
grid-row: auto;
|
||
}
|
||
|
||
.ghfu-card-log,
|
||
.ghfu-card-assets {
|
||
height: auto;
|
||
min-height: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
#ghfu-root { padding: 12px; }
|
||
|
||
.ghfu-status-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.ghfu-card-actions {
|
||
justify-content: stretch;
|
||
}
|
||
|
||
.ghfu-card-actions .cbi-button {
|
||
width: 100%;
|
||
}
|
||
|
||
.ghfu-card-basic .cbi-value,
|
||
.ghfu-card-assets .cbi-value {
|
||
grid-template-columns: 1fr;
|
||
align-items: stretch;
|
||
}
|
||
}
|
||
|
||
#ghfu-upgrade-modal {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: var(--ghfu-overlay);
|
||
z-index: 2147483647;
|
||
align-items: center;
|
||
justify-content: center;
|
||
pointer-events: auto;
|
||
touch-action: none;
|
||
}
|
||
|
||
body.ghfu-upgrade-lock,
|
||
html.ghfu-upgrade-lock {
|
||
overflow: hidden !important;
|
||
}
|
||
|
||
body.ghfu-upgrade-lock * {
|
||
pointer-events: none !important;
|
||
}
|
||
|
||
body.ghfu-upgrade-lock #ghfu-upgrade-modal,
|
||
body.ghfu-upgrade-lock #ghfu-upgrade-modal * {
|
||
pointer-events: auto !important;
|
||
}
|
||
|
||
.ghfu-modal-box {
|
||
background: var(--ghfu-modal-bg);
|
||
border-radius: 14px;
|
||
padding: 36px 48px;
|
||
text-align: center;
|
||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.45);
|
||
max-width: 420px;
|
||
width: 90%;
|
||
}
|
||
|
||
.ghfu-modal-spinner {
|
||
width: 48px; height: 48px;
|
||
border: 5px solid var(--ghfu-spinner-track);
|
||
border-top-color: var(--ghfu-spinner-head);
|
||
border-radius: 50%;
|
||
animation: ghfu-spin 0.8s linear infinite;
|
||
margin: 0 auto 20px;
|
||
}
|
||
|
||
#ghfu-upgrade-modal.ghfu-modal-countdown .ghfu-modal-spinner {
|
||
display: none;
|
||
}
|
||
|
||
@keyframes ghfu-spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.ghfu-modal-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: var(--ghfu-modal-text);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.ghfu-modal-msg {
|
||
font-size: 13px;
|
||
color: var(--ghfu-modal-muted);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
#ghfu-ext-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0; left: 0;
|
||
width: 100%; height: 100%;
|
||
background: var(--ghfu-overlay);
|
||
z-index: 9999;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
#ghfu-ext-modal.ghfu-modal-open {
|
||
display: flex;
|
||
}
|
||
|
||
#ghfu-ext-modal .ghfu-modal-box {
|
||
max-width: 440px;
|
||
text-align: left;
|
||
}
|
||
|
||
#ghfu_ext_textarea {
|
||
width: 100%;
|
||
height: 200px;
|
||
box-sizing: border-box;
|
||
font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace;
|
||
font-size: 13px;
|
||
border: 1px solid var(--ghfu-border);
|
||
border-radius: 8px;
|
||
padding: 8px 10px;
|
||
background: var(--ghfu-log-bg);
|
||
color: var(--ghfu-log-text);
|
||
resize: vertical;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.ghfu-ext-modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid var(--ghfu-divider);
|
||
}
|
||
|
||
</style>
|
||
|
||
<div class="cbi-map" id="ghfu-root">
|
||
<h2 id="ghfu_title"></h2>
|
||
<p class="ghfu-subtitle" id="ghfu_subtitle"></p>
|
||
<div class="cbi-section ghfu-layout">
|
||
<div class="ghfu-card ghfu-card-status">
|
||
<div class="ghfu-card-title" id="ghfu_status_label"></div>
|
||
<div id="ghfu_status"></div>
|
||
|
||
<div class="ghfu-card-actions">
|
||
<button class="cbi-button cbi-button-neutral ghfu-btn-accent" id="ghfu_backup" type="button"></button>
|
||
<button class="cbi-button cbi-button-action" id="ghfu_refresh" type="button"></button>
|
||
<button class="cbi-button cbi-button-neutral ghfu-btn-accent" id="ghfu_notes" type="button" disabled="disabled"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ghfu-card ghfu-card-basic">
|
||
<div class="ghfu-card-title" id="ghfu_basic_title"></div>
|
||
|
||
<div class="cbi-value">
|
||
<label class="cbi-value-title" for="ghfu_repo" id="ghfu_repo_label"></label>
|
||
<div class="cbi-value-field">
|
||
<input id="ghfu_repo" class="cbi-input-text ghfu-wide" type="text" value="smallprogram/OpenWrtAction" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cbi-value">
|
||
<label class="cbi-value-title" for="ghfu_release" id="ghfu_release_label"></label>
|
||
<div class="cbi-value-field">
|
||
<input id="ghfu_release" class="cbi-input-text ghfu-wide" type="text" readonly="readonly" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cbi-value">
|
||
<label class="cbi-value-title" for="ghfu_timeout" id="ghfu_timeout_label"></label>
|
||
<div class="cbi-value-field" style="display:flex;align-items:center;gap:10px;">
|
||
<input id="ghfu_timeout" class="cbi-input-text" type="number" min="5" max="300" value="15" style="width:80px;" />
|
||
<span id="ghfu_timeout_hint" style="color:var(--ghfu-text-muted);font-size:12px;"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cbi-value">
|
||
<div class="cbi-value-title"></div>
|
||
<div class="cbi-value-field ghfu-basic-toggle-row">
|
||
<div>
|
||
<input id="ghfu_keep" type="checkbox" checked="checked" />
|
||
<span id="ghfu_keep_label"></span>
|
||
</div>
|
||
<div>
|
||
<input id="ghfu_filter_ext_enabled" type="checkbox" checked="checked" />
|
||
<span id="ghfu_filter_ext_label"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ghfu-card-actions">
|
||
<button class="cbi-button cbi-button-neutral" id="ghfu_ext_config_btn" type="button"></button>
|
||
<button class="cbi-button cbi-button-action" id="ghfu_basic_save" type="button"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ghfu-card ghfu-card-assets">
|
||
<div class="ghfu-card-title" id="ghfu_assets_label"></div>
|
||
|
||
<div class="cbi-value">
|
||
<label class="cbi-value-title" for="ghfu_search" id="ghfu_search_label"></label>
|
||
<div class="cbi-value-field">
|
||
<input id="ghfu_search" class="cbi-input-text ghfu-wide" type="text" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cbi-value-field">
|
||
<div id="ghfu_assets"></div>
|
||
</div>
|
||
|
||
<div class="ghfu-card-actions">
|
||
<button class="cbi-button cbi-button-apply" id="ghfu_upgrade" type="button" disabled="disabled"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ghfu-card ghfu-card-log">
|
||
<div class="ghfu-card-title" id="ghfu_log_label"></div>
|
||
<div class="cbi-value-field">
|
||
<textarea id="ghfu_log" class="cbi-input-textarea" readonly="readonly"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ghfu-upgrade-modal">
|
||
<div class="ghfu-modal-box">
|
||
<div class="ghfu-modal-spinner"></div>
|
||
<div class="ghfu-modal-title" id="ghfu_modal_title"></div>
|
||
<div class="ghfu-modal-msg" id="ghfu_modal_msg"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ghfu-ext-modal">
|
||
<div class="ghfu-modal-box">
|
||
<div class="ghfu-modal-title" id="ghfu_ext_modal_title"></div>
|
||
<div style="font-size:12px;color:var(--ghfu-modal-muted);margin-top:4px;" id="ghfu_ext_modal_hint"></div>
|
||
<textarea id="ghfu_ext_textarea"></textarea>
|
||
<div class="ghfu-ext-modal-actions">
|
||
<button class="cbi-button cbi-button-neutral" id="ghfu_ext_cancel" type="button"></button>
|
||
<button class="cbi-button cbi-button-action" id="ghfu_ext_save_btn" type="button"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="text/javascript">
|
||
(function() {
|
||
var I18N = {
|
||
title: '<%:GitHub Firmware Upgrade%>',
|
||
subtitle: '<%:Firmware upgrade assistant based on GitHub Release.%>',
|
||
basicTitle: '<%:Basic Configuration%>',
|
||
repoLabel: '<%:GitHub Repository%>',
|
||
searchLabel: '<%:Firmware Fuzzy Search%>',
|
||
searchPlaceholder: '<%:Enter keyword to filter firmware names%>',
|
||
releaseLabel: '<%:Last Updated Firmware%>',
|
||
keepLabel: '<%:Keep Configuration%>',
|
||
filterExtLabel: '<%:Filter Valid Extensions%>',
|
||
extConfigBtn: '<%:Valid Extensions%>',
|
||
extConfigTitle: '<%:Valid Firmware Extensions%>',
|
||
extConfigHint: '<%:One extension per line (e.g. .img .img.gz)%>',
|
||
extConfigCancel: '<%:Cancel%>',
|
||
extConfigSaveBtn: '<%:Save%>',
|
||
extConfigSaving: '<%:Saving extensions...%>',
|
||
extConfigSaved: '<%:Extensions saved%>',
|
||
statusLabel: '<%:System and Release Status%>',
|
||
assetsLabel: '<%:Available Firmware%>',
|
||
assetsLabelWithCount: '<%:Available Firmware (Filtered: %d)%>',
|
||
logLabel: '<%:Logs%>',
|
||
refreshBtn: '<%:Fetch Latest Release%>',
|
||
upgradeBtn: '<%:Download and Upgrade System%>',
|
||
waitingStatus: '<%:Waiting to fetch latest release%>',
|
||
noMatch: '<%:No matching firmware%>',
|
||
beginFetch: '<%:Fetching latest release%>',
|
||
fetchFailed: '<%:Fetch failed%>',
|
||
requestFailed: '<%:Request failed: %>',
|
||
latestRelease: '<%:Latest release: %>',
|
||
statusUpdated: '<%:Status updated%>',
|
||
noNewStop: '<%:No new firmware currently, firmware selection is disabled%>',
|
||
installTime: '<%:System install time%>',
|
||
latestRel: '<%:Latest release%>',
|
||
pubTime: '<%:Published time%>',
|
||
releaseTotal: '<%:Assets total%>',
|
||
conclusion: '<%:Conclusion%>',
|
||
fetchingInProgress: '<%:Fetching in progress%>',
|
||
fetchGithubReleaseFailed: '<%:Failed to fetch GitHub release%>',
|
||
fetchFieldFailed: '<%:Fetch failed%>',
|
||
chooseFirmware: '<%:Please select firmware to upgrade%>',
|
||
chooseAsset: '<%:Please select a firmware asset%>',
|
||
beginUpgrade: '<%:Preparing upgrade: %>',
|
||
cmdStarted: '<%:Upgrade command started, device may reboot soon%>',
|
||
upgradeReqFailed: '<%:Upgrade request failed: %>',
|
||
selectedFirmware: '<%:Selected firmware: %>',
|
||
unknown: '<%:unknown%>',
|
||
backupBtn: '<%:Download Backup%>',
|
||
upgradingTitle: '<%:Upgrading System%>',
|
||
upgradingMsg: '<%:System is upgrading, please wait. The page will reload automatically when the system comes back online.%>',
|
||
beginDownload: '<%:Starting download: %>',
|
||
countdownPrefix: '<%:System upgrade will start in %>',
|
||
countdownSuffix: '<%: seconds%>',
|
||
startingUpgrade: '<%:Starting system upgrade...%>',
|
||
countdownLockTitle: '<%:Firmware Download Completed%>',
|
||
countdownLockMsg: '<%:Firmware is ready. System upgrade will start in %d seconds. Please do not leave this page.%>',
|
||
timeoutLabel: '<%:Fetch Timeout (seconds)%>',
|
||
timeoutHint: '<%:5 ~ 300 seconds. Limits GitHub API fetch time.%>',
|
||
saveBasicBtn: '<%:Save Basic Configuration%>',
|
||
savingBasic: '<%:Saving basic configuration...%>',
|
||
basicSaved: '<%:Basic configuration saved%>',
|
||
fetchTimedOut: '<%:Unable to fetch GitHub Release, please check your network connection%>',
|
||
notesBtn: '<%:Read Latest Release Notes%>',
|
||
notesUnavailable: '<%:Latest release page URL is unavailable, please fetch latest release again%>',
|
||
popupBlocked: '<%:Unable to open a new window. Please allow pop-ups in your browser.%>',
|
||
fallbackUsedHint: '<%:Latest release assets are not ready yet, using previous release with available firmware.%>',
|
||
partialAssetsHint: '<%:Latest release partial firmware may still be compiling, waiting to upload, or failed to compile. If your platform is missing, please wait for the next build result.%>',
|
||
autoFilterMatched: '<%:已自动筛选为上次更新的固件名%s,如果需要更改请删除模糊检索内容%>',
|
||
autoFilterNoMatch: '<%:最新release中没有固件名称%s,请手动筛选或等待下次仓库编译。%>',
|
||
businessLocked: '<%:Upgrade workflow is running, please wait until current operation finishes%>'
|
||
};
|
||
|
||
var assets = [];
|
||
var selectedAsset = null;
|
||
var latestReleaseName = '';
|
||
var latestReleaseUrl = '';
|
||
var lastInstallEpoch = 0;
|
||
var routerTzOffsetMin = null;
|
||
var rebootPollTimer = null;
|
||
var rebootPollAttempts = 0;
|
||
var rebootPollLimit = 36;
|
||
var rebootSeenOffline = false;
|
||
var upgradeModalActive = false;
|
||
var downloadPollTimer = null;
|
||
var filterExtEnabled = true;
|
||
var validExtensions = ['.img', '.img.gz', '.bin', '.tar', '.itb', '.trx', '.chk', '.dlf', '.ari'];
|
||
var upgradeWorkflowBusy = false;
|
||
var appVersion = '<%=app_version or ""%>';
|
||
|
||
function byId(id) { return document.getElementById(id); }
|
||
function t(key) { return I18N[key] || key; }
|
||
function trim(s) { return String(s || '').replace(/^\s+|\s+$/g, ''); }
|
||
|
||
function updateAssetsLabel(count) {
|
||
byId('ghfu_assets_label').textContent = t('assetsLabelWithCount').replace('%d', String(count || 0));
|
||
}
|
||
|
||
function updateTitle() {
|
||
byId('ghfu_title').textContent = appVersion ? (t('title') + ' v' + appVersion) : t('title');
|
||
}
|
||
|
||
function updateConfig(cfg) {
|
||
if (!cfg) { return; }
|
||
filterExtEnabled = cfg.filter_ext_enabled !== '0';
|
||
if (cfg.valid_extensions) {
|
||
var exts = String(cfg.valid_extensions).split(/\s+/).filter(Boolean);
|
||
if (exts.length > 0) { validExtensions = exts; }
|
||
}
|
||
var extEl = byId('ghfu_filter_ext_enabled');
|
||
if (extEl) { extEl.checked = filterExtEnabled; }
|
||
}
|
||
|
||
function updateFilteredAssetsLabel() {
|
||
updateAssetsLabel(getVisibleAssets().length);
|
||
}
|
||
|
||
function setDisabled(id, disabled) {
|
||
var el = byId(id);
|
||
if (el) {
|
||
el.disabled = !!disabled;
|
||
}
|
||
}
|
||
|
||
function syncInteractiveState() {
|
||
var root = byId('ghfu-root');
|
||
var busy = !!upgradeWorkflowBusy;
|
||
|
||
if (root) {
|
||
root.classList.toggle('ghfu-input-locked', busy);
|
||
}
|
||
|
||
setDisabled('ghfu_repo', busy);
|
||
setDisabled('ghfu_release', busy);
|
||
setDisabled('ghfu_timeout', busy);
|
||
setDisabled('ghfu_keep', busy);
|
||
setDisabled('ghfu_filter_ext_enabled', busy);
|
||
setDisabled('ghfu_search', busy);
|
||
|
||
setDisabled('ghfu_basic_save', busy);
|
||
setDisabled('ghfu_ext_config_btn', busy);
|
||
setDisabled('ghfu_refresh', busy);
|
||
setDisabled('ghfu_backup', busy);
|
||
setDisabled('ghfu_upgrade', busy || assets.length === 0);
|
||
setDisabled('ghfu_notes', busy || !latestReleaseUrl);
|
||
|
||
var radios = byId('ghfu_assets').querySelectorAll('input[type="radio"]');
|
||
radios.forEach(function(radio) {
|
||
radio.disabled = busy;
|
||
});
|
||
|
||
if (busy && root && root.contains(document.activeElement) && document.activeElement && document.activeElement.blur) {
|
||
document.activeElement.blur();
|
||
}
|
||
}
|
||
|
||
function setUpgradeWorkflowBusy(busy) {
|
||
upgradeWorkflowBusy = !!busy;
|
||
syncInteractiveState();
|
||
}
|
||
|
||
function ensureNotBusy() {
|
||
if (!upgradeWorkflowBusy) {
|
||
return true;
|
||
}
|
||
appendLog(t('businessLocked'));
|
||
return false;
|
||
}
|
||
|
||
function extractFirmwareName(text) {
|
||
var raw = trim(text || '');
|
||
if (!raw) {
|
||
return '';
|
||
}
|
||
|
||
var candidate = raw;
|
||
if (candidate.indexOf('/') !== -1) {
|
||
var seg = candidate.split('/');
|
||
while (seg.length > 0) {
|
||
var last = trim(seg.pop());
|
||
if (last) {
|
||
candidate = last;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return trim(candidate);
|
||
}
|
||
|
||
function selectAsset(asset, silent) {
|
||
if (upgradeWorkflowBusy) {
|
||
return;
|
||
}
|
||
if (!asset || !asset.url) {
|
||
return;
|
||
}
|
||
selectedAsset = asset;
|
||
byId('ghfu_release').value = (latestReleaseName ? (latestReleaseName + ' / ') : '') + (asset.name || '');
|
||
renderAssets();
|
||
if (!silent) {
|
||
appendLog(t('selectedFirmware') + (asset.name || ''));
|
||
}
|
||
}
|
||
|
||
function applyAutoFirmwareFilter(previousReleaseText) {
|
||
if (!assets.length) {
|
||
return;
|
||
}
|
||
|
||
var prevFirmware = extractFirmwareName(previousReleaseText);
|
||
if (!prevFirmware) {
|
||
return;
|
||
}
|
||
|
||
byId('ghfu_search').value = prevFirmware;
|
||
renderAssets();
|
||
|
||
var visible = getVisibleAssets();
|
||
var lowerPrev = prevFirmware.toLowerCase();
|
||
var exact = visible.filter(function(a) {
|
||
return String(a.name || '').toLowerCase() === lowerPrev;
|
||
});
|
||
var matched = exact.length ? exact : visible.filter(function(a) {
|
||
return String(a.name || '').toLowerCase().indexOf(lowerPrev) !== -1;
|
||
});
|
||
|
||
if (matched.length > 0) {
|
||
selectAsset(matched[0], true);
|
||
appendLog(t('autoFilterMatched').replace('%s', matched[0].name || prevFirmware));
|
||
return;
|
||
}
|
||
|
||
appendLog(t('autoFilterNoMatch').replace('%s', prevFirmware));
|
||
selectedAsset = null;
|
||
renderAssets();
|
||
}
|
||
|
||
/* 检测当前主题是否为暗色,并给 root 元素打标记 */
|
||
function applyTheme() {
|
||
var root = byId('ghfu-root');
|
||
if (!root) return;
|
||
var isDark = false;
|
||
|
||
/* 1. 系统媒体查询 */
|
||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||
isDark = true;
|
||
}
|
||
|
||
/* 2. 常见暗色类名(body / html) */
|
||
if (!isDark) {
|
||
var darkClasses = ['dark', 'is-dark', 'night', 'dark-mode', 'theme-dark', 'luci-dark'];
|
||
var body = document.body;
|
||
var html = document.documentElement;
|
||
for (var i = 0; i < darkClasses.length; i++) {
|
||
if (body.classList.contains(darkClasses[i]) ||
|
||
html.classList.contains(darkClasses[i])) {
|
||
isDark = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 3. data-theme / data-color-scheme 属性 */
|
||
if (!isDark) {
|
||
var dt = document.body.getAttribute('data-theme') ||
|
||
document.body.getAttribute('data-color-scheme') ||
|
||
document.documentElement.getAttribute('data-theme');
|
||
if (dt && dt.indexOf('dark') !== -1) isDark = true;
|
||
}
|
||
|
||
/* 4. 最终兜底:读取 body 实际背景色亮度 */
|
||
if (!isDark) {
|
||
try {
|
||
var bg = window.getComputedStyle(document.body).backgroundColor;
|
||
var m = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||
if (m) {
|
||
var lum = (0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3]) / 255;
|
||
if (lum < 0.4) isDark = true;
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
if (isDark) {
|
||
root.setAttribute('data-ghfu-dark', '1');
|
||
} else {
|
||
root.removeAttribute('data-ghfu-dark');
|
||
}
|
||
}
|
||
|
||
function initTexts() {
|
||
updateTitle();
|
||
byId('ghfu_subtitle').textContent = t('subtitle');
|
||
byId('ghfu_basic_title').textContent = t('basicTitle');
|
||
byId('ghfu_repo_label').textContent = t('repoLabel');
|
||
byId('ghfu_search_label').textContent = t('searchLabel');
|
||
byId('ghfu_search').placeholder = t('searchPlaceholder');
|
||
byId('ghfu_release_label').textContent = t('releaseLabel');
|
||
byId('ghfu_keep_label').textContent = t('keepLabel');
|
||
byId('ghfu_filter_ext_label').textContent = t('filterExtLabel');
|
||
byId('ghfu_ext_config_btn').textContent = t('extConfigBtn');
|
||
byId('ghfu_ext_modal_title').textContent = t('extConfigTitle');
|
||
byId('ghfu_ext_modal_hint').textContent = t('extConfigHint');
|
||
byId('ghfu_ext_cancel').textContent = t('extConfigCancel');
|
||
byId('ghfu_ext_save_btn').textContent = t('extConfigSaveBtn');
|
||
byId('ghfu_status_label').textContent = t('statusLabel');
|
||
updateAssetsLabel(0);
|
||
byId('ghfu_log_label').textContent = t('logLabel');
|
||
byId('ghfu_refresh').textContent = t('refreshBtn');
|
||
byId('ghfu_notes').textContent = t('notesBtn');
|
||
byId('ghfu_upgrade').textContent = t('upgradeBtn');
|
||
byId('ghfu_backup').textContent = t('backupBtn');
|
||
byId('ghfu_modal_title').textContent = t('upgradingTitle');
|
||
byId('ghfu_modal_msg').textContent = t('upgradingMsg');
|
||
byId('ghfu_timeout_label').textContent = t('timeoutLabel');
|
||
byId('ghfu_timeout_hint').textContent = t('timeoutHint');
|
||
byId('ghfu_basic_save').textContent = t('saveBasicBtn');
|
||
|
||
renderStatusCard({
|
||
conclusion: t('beginFetch'),
|
||
conclusionCls: 'ghfu-conclusion-warn',
|
||
installValue: t('fetchingInProgress'),
|
||
latestValue: t('fetchingInProgress'),
|
||
publishedValue: t('fetchingInProgress'),
|
||
releaseTotalValue: t('fetchingInProgress')
|
||
});
|
||
}
|
||
|
||
function pad2(v) {
|
||
return v < 10 ? ('0' + v) : String(v);
|
||
}
|
||
|
||
function toRouterDate(dateObj) {
|
||
if (!(dateObj instanceof Date) || isNaN(dateObj.getTime())) {
|
||
return null;
|
||
}
|
||
|
||
var browserOffset = -new Date().getTimezoneOffset();
|
||
var targetOffset = (typeof routerTzOffsetMin === 'number') ? routerTzOffsetMin : browserOffset;
|
||
var shifted = new Date(dateObj.getTime() + (targetOffset - browserOffset) * 60000);
|
||
return shifted;
|
||
}
|
||
|
||
function formatRouterDate(dateObj) {
|
||
var d = toRouterDate(dateObj);
|
||
if (!d) {
|
||
return '-';
|
||
}
|
||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' +
|
||
pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
|
||
}
|
||
|
||
function formatEpochToRouter(epoch) {
|
||
if (!epoch) {
|
||
return '-';
|
||
}
|
||
return formatRouterDate(new Date(epoch * 1000));
|
||
}
|
||
|
||
function formatIsoToRouter(iso) {
|
||
if (!iso) {
|
||
return '-';
|
||
}
|
||
return formatRouterDate(new Date(iso));
|
||
}
|
||
|
||
function appendLog(line) {
|
||
var el = byId('ghfu_log');
|
||
var ts = formatRouterDate(new Date());
|
||
el.value += '[' + ts + '] ' + line + '\n';
|
||
el.scrollTop = el.scrollHeight;
|
||
}
|
||
|
||
function setStatus(text) {
|
||
byId('ghfu_status').textContent = text;
|
||
}
|
||
|
||
function getCurrentInstallDisplay() {
|
||
if (lastInstallEpoch) {
|
||
return formatEpochToRouter(lastInstallEpoch);
|
||
}
|
||
return t('fetchingInProgress');
|
||
}
|
||
|
||
function getCurrentSelectedFirmwareDisplay(selectedRaw) {
|
||
var src = (selectedRaw !== undefined && selectedRaw !== null) ? selectedRaw : ((byId('ghfu_release').value || ''));
|
||
return extractFirmwareName(src) || '-';
|
||
}
|
||
|
||
function renderStatusCard(opts) {
|
||
var installText = t('installTime') + ': ' + (opts.installValue || '-');
|
||
var releaseText = t('latestRel') + ': ' + (opts.latestValue || '-');
|
||
var publishedText = t('pubTime') + ': ' + (opts.publishedValue || '-');
|
||
var releaseTotalText = t('releaseTotal') + ': ' + (opts.releaseTotalValue || '-');
|
||
var selectedText = t('releaseLabel') + ': ' + getCurrentSelectedFirmwareDisplay(opts.selectedRaw);
|
||
var conclusionText = opts.conclusion || '-';
|
||
var conclusionCls = opts.conclusionCls || 'ghfu-conclusion-ok';
|
||
var hintHtml = opts.hintHtml || '';
|
||
|
||
byId('ghfu_status').innerHTML =
|
||
'<div class="ghfu-status-hero ' + conclusionCls + '">' + escHtml(conclusionText) + '</div>' +
|
||
'<div class="ghfu-status-grid">' +
|
||
'<div class="ghfu-status-item">' + escHtml(installText) + '</div>' +
|
||
'<div class="ghfu-status-item">' + escHtml(releaseText) + '</div>' +
|
||
'<div class="ghfu-status-item">' + escHtml(publishedText) + '</div>' +
|
||
'<div class="ghfu-status-item">' + escHtml(releaseTotalText) + '</div>' +
|
||
'<div class="ghfu-status-item">' + escHtml(selectedText) + '</div>' +
|
||
'</div>' +
|
||
hintHtml;
|
||
}
|
||
|
||
function escHtml(s) {
|
||
return String(s || '').replace(/[&<>"']/g, function(c) {
|
||
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
||
});
|
||
}
|
||
|
||
function parseJsonResponse(r) {
|
||
return r.text().then(function(txt) {
|
||
try {
|
||
return JSON.parse(txt);
|
||
} catch (e) {
|
||
throw new Error('Server returned invalid JSON response');
|
||
}
|
||
});
|
||
}
|
||
|
||
function request(url, data) {
|
||
return fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||
body: new URLSearchParams(data).toString()
|
||
}).then(parseJsonResponse);
|
||
}
|
||
|
||
function requestWithTimeout(url, data, timeoutMs) {
|
||
var controller = new AbortController();
|
||
var timer = setTimeout(function() { controller.abort(); }, timeoutMs);
|
||
return fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||
body: new URLSearchParams(data).toString(),
|
||
signal: controller.signal
|
||
}).then(function(r) {
|
||
clearTimeout(timer);
|
||
return parseJsonResponse(r);
|
||
}).catch(function(err) {
|
||
clearTimeout(timer);
|
||
throw err;
|
||
});
|
||
}
|
||
|
||
function humanSize(size) {
|
||
if (!size || size <= 0) return '0 B';
|
||
var units = ['B', 'KB', 'MB', 'GB'];
|
||
var i = 0;
|
||
var n = size;
|
||
while (n >= 1024 && i < units.length - 1) {
|
||
n /= 1024;
|
||
i++;
|
||
}
|
||
return n.toFixed(i === 0 ? 0 : 2) + ' ' + units[i];
|
||
}
|
||
|
||
function shouldKeepAsset(asset) {
|
||
if (!asset || typeof asset !== 'object') {
|
||
return false;
|
||
}
|
||
|
||
if (!filterExtEnabled) {
|
||
return true;
|
||
}
|
||
|
||
var name = String(asset.name || '').toLowerCase();
|
||
for (var i = 0; i < validExtensions.length; i++) {
|
||
var ext = validExtensions[i].toLowerCase();
|
||
if (ext && name.length >= ext.length && name.slice(-ext.length) === ext) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function getVisibleAssets() {
|
||
return assets.filter(shouldKeepAsset);
|
||
}
|
||
|
||
function getSearchFilteredAssets() {
|
||
var raw = String(byId('ghfu_search').value || '').toLowerCase();
|
||
var kw = raw.replace(/[\s\u3000]+/g, ' ').replace(/^\s+|\s+$/g, '');
|
||
var tokens = kw ? kw.split(' ') : [];
|
||
return getVisibleAssets().filter(function(a) {
|
||
if (!tokens.length) {
|
||
return true;
|
||
}
|
||
var name = String(a.name || '').toLowerCase();
|
||
for (var i = 0; i < tokens.length; i++) {
|
||
if (name.indexOf(tokens[i]) === -1) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function renderAssets() {
|
||
var box = byId('ghfu_assets');
|
||
box.innerHTML = '';
|
||
|
||
var filtered = getSearchFilteredAssets();
|
||
|
||
updateAssetsLabel(filtered.length);
|
||
|
||
if (!filtered.length) {
|
||
box.textContent = t('noMatch');
|
||
return;
|
||
}
|
||
|
||
filtered.forEach(function(a, idx) {
|
||
var isSelected = !!(selectedAsset && selectedAsset.url === a.url);
|
||
var row = document.createElement('div');
|
||
row.className = 'ghfu-asset-row' + (isSelected ? ' ghfu-row-selected' : '');
|
||
|
||
var id = 'ghfu_asset_' + idx;
|
||
var input = document.createElement('input');
|
||
input.type = 'radio';
|
||
input.name = 'ghfu_asset';
|
||
input.id = id;
|
||
input.value = a.url || '';
|
||
input.addEventListener('change', function() {
|
||
if (upgradeWorkflowBusy) {
|
||
return;
|
||
}
|
||
selectAsset(a, false);
|
||
});
|
||
|
||
if (isSelected) {
|
||
input.checked = true;
|
||
}
|
||
|
||
var nameSpan = document.createElement('span');
|
||
nameSpan.className = 'ghfu-asset-name';
|
||
nameSpan.textContent = (a.name || '') + ' (' + humanSize(a.size || 0) + ')';
|
||
|
||
row.addEventListener('click', function() {
|
||
if (upgradeWorkflowBusy) {
|
||
return;
|
||
}
|
||
if (!input.checked) {
|
||
input.checked = true;
|
||
selectAsset(a, false);
|
||
}
|
||
});
|
||
|
||
row.appendChild(input);
|
||
row.appendChild(nameSpan);
|
||
box.appendChild(row);
|
||
});
|
||
|
||
syncInteractiveState();
|
||
}
|
||
|
||
function normalizeAssets(raw) {
|
||
if (Array.isArray(raw)) {
|
||
return raw;
|
||
}
|
||
if (!raw || typeof raw !== 'object') {
|
||
return [];
|
||
}
|
||
var keys = Object.keys(raw).sort(function(a, b) {
|
||
var na = parseInt(a, 10), nb = parseInt(b, 10);
|
||
if (!isNaN(na) && !isNaN(nb)) {
|
||
return na - nb;
|
||
}
|
||
return String(a).localeCompare(String(b));
|
||
});
|
||
var out = [];
|
||
keys.forEach(function(k) {
|
||
if (raw[k] && typeof raw[k] === 'object') {
|
||
out.push(raw[k]);
|
||
}
|
||
});
|
||
return out;
|
||
}
|
||
|
||
function normalizeLogLines(raw) {
|
||
if (Array.isArray(raw)) {
|
||
return raw;
|
||
}
|
||
|
||
if (raw === null || raw === undefined) {
|
||
return [];
|
||
}
|
||
|
||
if (typeof raw === 'string') {
|
||
return raw ? [raw] : [];
|
||
}
|
||
|
||
if (typeof raw === 'object') {
|
||
var keys = Object.keys(raw).sort(function(a, b) {
|
||
var na = parseInt(a, 10), nb = parseInt(b, 10);
|
||
if (!isNaN(na) && !isNaN(nb)) {
|
||
return na - nb;
|
||
}
|
||
return String(a).localeCompare(String(b));
|
||
});
|
||
var out = [];
|
||
keys.forEach(function(k) {
|
||
var v = raw[k];
|
||
if (v !== null && v !== undefined && String(v) !== '') {
|
||
out.push(String(v));
|
||
}
|
||
});
|
||
return out;
|
||
}
|
||
|
||
return [String(raw)];
|
||
}
|
||
|
||
function saveBasicConfig() {
|
||
if (!ensureNotBusy()) {
|
||
return;
|
||
}
|
||
|
||
var repo = trim(byId('ghfu_repo').value || '');
|
||
var keepConfig = byId('ghfu_keep').checked ? '1' : '0';
|
||
var filterExtEnabledVal = byId('ghfu_filter_ext_enabled').checked ? '1' : '0';
|
||
var timeoutSec = parseInt(byId('ghfu_timeout').value, 10) || 15;
|
||
if (timeoutSec < 5) timeoutSec = 5;
|
||
if (timeoutSec > 300) timeoutSec = 300;
|
||
byId('ghfu_timeout').value = String(timeoutSec);
|
||
|
||
byId('ghfu_basic_save').disabled = true;
|
||
appendLog(t('savingBasic'));
|
||
|
||
request('<%=url([[admin]], [[system]], [[ghfu]], [[config]])%>', {
|
||
repo: repo,
|
||
keep_config: keepConfig,
|
||
fetch_timeout: String(timeoutSec),
|
||
filter_ext_enabled: filterExtEnabledVal
|
||
}).then(function(resp) {
|
||
byId('ghfu_basic_save').disabled = false;
|
||
if (!resp.ok) {
|
||
appendLog(resp.msg || t('requestFailed'));
|
||
return;
|
||
}
|
||
|
||
if (resp.config) {
|
||
byId('ghfu_repo').value = resp.config.github_repo || repo;
|
||
byId('ghfu_keep').checked = (resp.config.keep_config || '1') === '1';
|
||
byId('ghfu_timeout').value = resp.config.fetch_timeout || String(timeoutSec);
|
||
updateConfig(resp.config);
|
||
}
|
||
|
||
appendLog(t('basicSaved'));
|
||
}).catch(function(err) {
|
||
byId('ghfu_basic_save').disabled = false;
|
||
appendLog(t('requestFailed') + err);
|
||
});
|
||
}
|
||
|
||
function stopDownloadPolling() {
|
||
if (downloadPollTimer) {
|
||
clearTimeout(downloadPollTimer);
|
||
downloadPollTimer = null;
|
||
}
|
||
}
|
||
|
||
function restoreButtonsAfterDownload() {
|
||
setUpgradeWorkflowBusy(false);
|
||
}
|
||
|
||
function pollDownloadProgress(keepConfig, offset) {
|
||
request('<%=url([[admin]], [[system]], [[ghfu]], [[download_status]])%>', {
|
||
offset: String(offset || 0)
|
||
}).then(function(resp) {
|
||
if (!resp || !resp.ok) {
|
||
appendLog(t('fetchFailed'));
|
||
restoreButtonsAfterDownload();
|
||
stopDownloadPolling();
|
||
return;
|
||
}
|
||
|
||
var lines = normalizeLogLines(resp.logs);
|
||
lines.forEach(appendLog);
|
||
|
||
if (resp.done) {
|
||
stopDownloadPolling();
|
||
if (resp.download_ok && resp.fw_name) {
|
||
startUpgradeCountdown(resp.fw_name, keepConfig);
|
||
} else {
|
||
restoreButtonsAfterDownload();
|
||
}
|
||
return;
|
||
}
|
||
|
||
downloadPollTimer = setTimeout(function() {
|
||
pollDownloadProgress(keepConfig, resp.next_offset || 0);
|
||
}, 1000);
|
||
}).catch(function(err) {
|
||
appendLog(t('requestFailed') + err);
|
||
restoreButtonsAfterDownload();
|
||
stopDownloadPolling();
|
||
});
|
||
}
|
||
|
||
function refreshStatus() {
|
||
if (!ensureNotBusy()) {
|
||
return;
|
||
}
|
||
|
||
var repo = (byId('ghfu_repo').value || '').trim();
|
||
var timeoutSec = parseInt(byId('ghfu_timeout').value, 10) || 15;
|
||
if (timeoutSec < 5) timeoutSec = 5;
|
||
if (timeoutSec > 300) timeoutSec = 300;
|
||
|
||
byId('ghfu_refresh').disabled = true;
|
||
byId('ghfu_notes').disabled = true;
|
||
renderStatusCard({
|
||
conclusion: t('beginFetch'),
|
||
conclusionCls: 'ghfu-conclusion-warn',
|
||
installValue: getCurrentInstallDisplay(),
|
||
latestValue: t('fetchingInProgress'),
|
||
publishedValue: t('fetchingInProgress'),
|
||
releaseTotalValue: t('fetchingInProgress')
|
||
});
|
||
appendLog(t('beginFetch'));
|
||
|
||
requestWithTimeout(
|
||
'<%=url([[admin]], [[system]], [[ghfu]], [[status]])%>',
|
||
{ repo: repo, fetch_timeout: String(timeoutSec) },
|
||
(timeoutSec + 5) * 1000
|
||
).then(function(resp) {
|
||
appVersion = String(resp && resp.app_version || '');
|
||
updateTitle();
|
||
byId('ghfu_refresh').disabled = false;
|
||
if (!resp.ok) {
|
||
byId('ghfu_notes').disabled = true;
|
||
latestReleaseUrl = '';
|
||
if (typeof resp.tz_offset_min === 'number') {
|
||
routerTzOffsetMin = resp.tz_offset_min;
|
||
}
|
||
if (resp.install_epoch) {
|
||
lastInstallEpoch = resp.install_epoch;
|
||
}
|
||
renderStatusCard({
|
||
conclusion: t('fetchGithubReleaseFailed'),
|
||
conclusionCls: 'ghfu-conclusion-warn',
|
||
installValue: resp.install_epoch ? formatEpochToRouter(resp.install_epoch) : getCurrentInstallDisplay(),
|
||
latestValue: t('fetchFieldFailed'),
|
||
publishedValue: t('fetchFieldFailed'),
|
||
releaseTotalValue: t('fetchFieldFailed'),
|
||
selectedRaw: (resp.config && resp.config.selected_release) || byId('ghfu_release').value
|
||
});
|
||
appendLog(resp.msg || t('fetchFailed'));
|
||
return;
|
||
}
|
||
|
||
byId('ghfu_repo').value = resp.repo || repo;
|
||
if (resp.config) {
|
||
byId('ghfu_release').value = resp.config.selected_release || '';
|
||
byId('ghfu_keep').checked = (resp.config.keep_config || '1') === '1';
|
||
updateConfig(resp.config);
|
||
}
|
||
if (typeof resp.fetch_timeout === 'number') {
|
||
byId('ghfu_timeout').value = String(resp.fetch_timeout);
|
||
}
|
||
|
||
if (typeof resp.tz_offset_min === 'number') {
|
||
routerTzOffsetMin = resp.tz_offset_min;
|
||
}
|
||
if (resp.install_epoch) {
|
||
lastInstallEpoch = resp.install_epoch;
|
||
}
|
||
|
||
latestReleaseName = (resp.latest && resp.latest.release_name) ? resp.latest.release_name : '';
|
||
latestReleaseUrl = (resp.latest && resp.latest.html_url) ? String(resp.latest.html_url) : '';
|
||
assets = normalizeAssets(resp.latest && resp.latest.assets);
|
||
selectedAsset = null;
|
||
// Always reset filter on refresh to avoid stale keyword hiding all assets.
|
||
byId('ghfu_search').value = '';
|
||
renderAssets();
|
||
applyAutoFirmwareFilter((resp.config && resp.config.selected_release) || '');
|
||
byId('ghfu_notes').disabled = !latestReleaseUrl;
|
||
// Allow repeated upgrade even when current system is already the latest,
|
||
// as long as release assets are available for selection.
|
||
byId('ghfu_upgrade').disabled = (assets.length === 0);
|
||
|
||
var conclusionCls = resp.has_new ? 'ghfu-conclusion-new' : 'ghfu-conclusion-ok';
|
||
var hintHtml = '';
|
||
if (resp.fallback_used) {
|
||
hintHtml = '<div class="ghfu-status-line ghfu-conclusion-warn">' + escHtml(t('fallbackUsedHint')) + '</div>';
|
||
} else if (resp.partial_assets) {
|
||
hintHtml = '<div class="ghfu-status-line ghfu-conclusion-warn">' + escHtml(t('partialAssetsHint')) + '</div>';
|
||
}
|
||
renderStatusCard({
|
||
conclusion: resp.msg || '-',
|
||
conclusionCls: conclusionCls,
|
||
installValue: resp.install_epoch ? formatEpochToRouter(resp.install_epoch) : t('unknown'),
|
||
latestValue: ((resp.latest && resp.latest.release_name) || '-'),
|
||
publishedValue: ((resp.latest && resp.latest.published_at) ? formatIsoToRouter(resp.latest.published_at) : '-'),
|
||
releaseTotalValue: (resp.latest && typeof resp.latest.release_total_count === 'number') ? String(resp.latest.release_total_count) : '-',
|
||
selectedRaw: (resp.config && resp.config.selected_release) || byId('ghfu_release').value,
|
||
hintHtml: hintHtml
|
||
});
|
||
|
||
appendLog(t('latestRelease') + ((resp.latest && resp.latest.release_name) || '-'));
|
||
appendLog(resp.msg || t('statusUpdated'));
|
||
if (resp.fallback_used) {
|
||
appendLog(t('fallbackUsedHint'));
|
||
} else if (resp.partial_assets) {
|
||
appendLog(t('partialAssetsHint'));
|
||
}
|
||
|
||
syncInteractiveState();
|
||
})
|
||
.catch(function(err) {
|
||
byId('ghfu_refresh').disabled = false;
|
||
byId('ghfu_notes').disabled = true;
|
||
latestReleaseUrl = '';
|
||
renderStatusCard({
|
||
conclusion: t('fetchGithubReleaseFailed'),
|
||
conclusionCls: 'ghfu-conclusion-warn',
|
||
installValue: getCurrentInstallDisplay(),
|
||
latestValue: t('fetchFieldFailed'),
|
||
publishedValue: t('fetchFieldFailed'),
|
||
releaseTotalValue: t('fetchFieldFailed')
|
||
});
|
||
byId('ghfu_upgrade').disabled = true;
|
||
if (err && err.name === 'AbortError') {
|
||
appendLog(t('fetchTimedOut'));
|
||
} else {
|
||
appendLog(t('requestFailed') + err);
|
||
}
|
||
});
|
||
}
|
||
|
||
function openLatestReleasePage() {
|
||
if (!latestReleaseUrl) {
|
||
appendLog(t('notesUnavailable'));
|
||
return;
|
||
}
|
||
var w = window.open(latestReleaseUrl, '_blank', 'noopener,noreferrer');
|
||
if (!w) {
|
||
appendLog(t('popupBlocked'));
|
||
}
|
||
}
|
||
|
||
function doDownload() {
|
||
if (!ensureNotBusy()) {
|
||
return;
|
||
}
|
||
|
||
var repo = (byId('ghfu_repo').value || '').trim();
|
||
var selectedRelease = (byId('ghfu_release').value || '').trim();
|
||
var keepConfig = byId('ghfu_keep').checked ? '1' : '0';
|
||
|
||
if (!selectedRelease) {
|
||
appendLog(t('chooseFirmware'));
|
||
return;
|
||
}
|
||
|
||
if (!selectedAsset || !selectedAsset.url) {
|
||
appendLog(t('chooseAsset'));
|
||
return;
|
||
}
|
||
|
||
setUpgradeWorkflowBusy(true);
|
||
stopDownloadPolling();
|
||
|
||
appendLog(t('beginDownload') + (selectedAsset.name || selectedRelease));
|
||
|
||
request('<%=url([[admin]], [[system]], [[ghfu]], [[download]])%>', {
|
||
repo: repo,
|
||
selected_release: selectedRelease,
|
||
keep_config: keepConfig,
|
||
asset_url: selectedAsset.url,
|
||
asset_name: selectedAsset.name || selectedRelease,
|
||
asset_size: String(selectedAsset.size || 0),
|
||
asset_digest: selectedAsset.sha256 || ''
|
||
}).then(function(resp) {
|
||
var lines = normalizeLogLines(resp.logs);
|
||
lines.forEach(appendLog);
|
||
if (!resp.ok) {
|
||
restoreButtonsAfterDownload();
|
||
return;
|
||
}
|
||
|
||
if (resp.started) {
|
||
pollDownloadProgress(keepConfig, 0);
|
||
return;
|
||
}
|
||
|
||
if (resp.fw_name) {
|
||
startUpgradeCountdown(resp.fw_name, keepConfig);
|
||
} else {
|
||
restoreButtonsAfterDownload();
|
||
}
|
||
}).catch(function(err) {
|
||
appendLog(t('upgradeReqFailed') + err);
|
||
restoreButtonsAfterDownload();
|
||
});
|
||
}
|
||
|
||
function startUpgradeCountdown(fwName, keepConfig) {
|
||
var sec = 5;
|
||
showUpgradeModal('countdown', sec);
|
||
appendLog(t('countdownPrefix') + sec + t('countdownSuffix'));
|
||
var timer = setInterval(function() {
|
||
sec--;
|
||
if (sec <= 0) {
|
||
clearInterval(timer);
|
||
appendLog(t('startingUpgrade'));
|
||
showUpgradeModal('upgrading');
|
||
doUpgradeRequest(fwName, keepConfig);
|
||
} else {
|
||
showUpgradeModal('countdown', sec);
|
||
appendLog(t('countdownPrefix') + sec + t('countdownSuffix'));
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function doUpgradeRequest(fwName, keepConfig) {
|
||
request('<%=url([[admin]], [[system]], [[ghfu]], [[upgrade]])%>', {
|
||
fw_name: fwName,
|
||
keep_config: keepConfig
|
||
}).then(function(resp) {
|
||
var lines = normalizeLogLines(resp.logs);
|
||
lines.forEach(appendLog);
|
||
if (resp.ok && resp.rebooting) {
|
||
showUpgradeModal('upgrading');
|
||
pollForReboot();
|
||
return;
|
||
}
|
||
|
||
hideUpgradeModal();
|
||
restoreButtonsAfterDownload();
|
||
}).catch(function(err) {
|
||
appendLog(t('upgradeReqFailed') + err);
|
||
hideUpgradeModal();
|
||
restoreButtonsAfterDownload();
|
||
});
|
||
}
|
||
|
||
function showUpgradeModal(stage, countdownSec) {
|
||
var modal = byId('ghfu-upgrade-modal');
|
||
var titleEl = byId('ghfu_modal_title');
|
||
var msgEl = byId('ghfu_modal_msg');
|
||
var currentStage = stage || 'upgrading';
|
||
|
||
if (modal) {
|
||
// Move modal to body root to avoid theme stacking-context clipping.
|
||
if (modal.parentNode !== document.body) {
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
if (currentStage === 'countdown') {
|
||
modal.classList.add('ghfu-modal-countdown');
|
||
} else {
|
||
modal.classList.remove('ghfu-modal-countdown');
|
||
}
|
||
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
if (titleEl && msgEl) {
|
||
if (currentStage === 'countdown') {
|
||
var safeSec = Math.max(1, parseInt(countdownSec, 10) || 1);
|
||
titleEl.textContent = t('countdownLockTitle');
|
||
msgEl.textContent = t('countdownLockMsg').replace('%d', String(safeSec));
|
||
} else {
|
||
titleEl.textContent = t('upgradingTitle');
|
||
msgEl.textContent = t('upgradingMsg');
|
||
}
|
||
}
|
||
|
||
upgradeModalActive = true;
|
||
document.body.classList.add('ghfu-upgrade-lock');
|
||
document.documentElement.classList.add('ghfu-upgrade-lock');
|
||
}
|
||
|
||
function hideUpgradeModal() {
|
||
var modal = byId('ghfu-upgrade-modal');
|
||
if (modal) {
|
||
modal.style.display = 'none';
|
||
modal.classList.remove('ghfu-modal-countdown');
|
||
}
|
||
|
||
byId('ghfu_modal_title').textContent = t('upgradingTitle');
|
||
byId('ghfu_modal_msg').textContent = t('upgradingMsg');
|
||
|
||
upgradeModalActive = false;
|
||
document.body.classList.remove('ghfu-upgrade-lock');
|
||
document.documentElement.classList.remove('ghfu-upgrade-lock');
|
||
}
|
||
|
||
function pollForReboot() {
|
||
if (rebootPollTimer) {
|
||
clearTimeout(rebootPollTimer);
|
||
}
|
||
|
||
rebootPollAttempts = 0;
|
||
rebootSeenOffline = false;
|
||
|
||
var heartbeatUrl = '<%=url([[admin]], [[system]], [[ghfu]], [[local_status]])%>';
|
||
|
||
var poll = function() {
|
||
rebootPollTimer = setTimeout(function() {
|
||
rebootPollAttempts++;
|
||
|
||
fetch(heartbeatUrl + '?_=' + Date.now(), {
|
||
method: 'POST',
|
||
cache: 'no-cache',
|
||
credentials: 'include'
|
||
})
|
||
.then(function() {
|
||
if (!rebootSeenOffline) {
|
||
poll();
|
||
return;
|
||
}
|
||
|
||
clearTimeout(rebootPollTimer);
|
||
hideUpgradeModal();
|
||
window.location.reload();
|
||
})
|
||
.catch(function() {
|
||
rebootSeenOffline = true;
|
||
|
||
// Keep waiting until router comes back online.
|
||
poll();
|
||
});
|
||
}, 2500);
|
||
};
|
||
|
||
poll();
|
||
}
|
||
|
||
function blockNavigationWhileUpgrading(evt) {
|
||
if (!upgradeModalActive) {
|
||
return;
|
||
}
|
||
|
||
var modal = byId('ghfu-upgrade-modal');
|
||
if (modal && modal.contains(evt.target)) {
|
||
return;
|
||
}
|
||
|
||
evt.preventDefault();
|
||
evt.stopPropagation();
|
||
}
|
||
|
||
document.addEventListener('click', blockNavigationWhileUpgrading, true);
|
||
document.addEventListener('mousedown', blockNavigationWhileUpgrading, true);
|
||
document.addEventListener('touchstart', blockNavigationWhileUpgrading, true);
|
||
|
||
window.addEventListener('beforeunload', function(e) {
|
||
if (!upgradeModalActive) {
|
||
return;
|
||
}
|
||
e.preventDefault();
|
||
e.returnValue = '';
|
||
});
|
||
|
||
byId('ghfu_refresh').addEventListener('click', refreshStatus);
|
||
byId('ghfu_upgrade').addEventListener('click', doDownload);
|
||
byId('ghfu_basic_save').addEventListener('click', saveBasicConfig);
|
||
byId('ghfu_search').addEventListener('input', renderAssets);
|
||
byId('ghfu_filter_ext_enabled').addEventListener('change', function() {
|
||
if (upgradeWorkflowBusy) { return; }
|
||
filterExtEnabled = this.checked;
|
||
renderAssets();
|
||
});
|
||
byId('ghfu_ext_config_btn').addEventListener('click', openExtConfigModal);
|
||
byId('ghfu_ext_cancel').addEventListener('click', closeExtConfigModal);
|
||
byId('ghfu_ext_save_btn').addEventListener('click', saveExtConfig);
|
||
byId('ghfu_backup').addEventListener('click', function() {
|
||
window.location.href = '<%=url([[admin]], [[system]], [[ghfu]], [[backup]])%>';
|
||
});
|
||
byId('ghfu_notes').addEventListener('click', openLatestReleasePage);
|
||
|
||
function openExtConfigModal() {
|
||
if (upgradeWorkflowBusy) { return; }
|
||
byId('ghfu_ext_textarea').value = validExtensions.join('\n');
|
||
byId('ghfu-ext-modal').classList.add('ghfu-modal-open');
|
||
byId('ghfu_ext_textarea').focus();
|
||
}
|
||
|
||
function closeExtConfigModal() {
|
||
byId('ghfu-ext-modal').classList.remove('ghfu-modal-open');
|
||
}
|
||
|
||
function saveExtConfig() {
|
||
var raw = byId('ghfu_ext_textarea').value || '';
|
||
var lines = raw.split(/[\r\n]+/).map(function(s) { return s.trim(); }).filter(Boolean);
|
||
if (lines.length === 0) {
|
||
appendLog(t('extConfigSaving'));
|
||
return;
|
||
}
|
||
var extStr = lines.join(' ');
|
||
byId('ghfu_ext_save_btn').disabled = true;
|
||
appendLog(t('extConfigSaving'));
|
||
|
||
request('<%=url([[admin]], [[system]], [[ghfu]], [[config]])%>', {
|
||
valid_extensions: extStr
|
||
}).then(function(resp) {
|
||
byId('ghfu_ext_save_btn').disabled = false;
|
||
if (!resp.ok) {
|
||
appendLog(resp.msg || t('requestFailed'));
|
||
return;
|
||
}
|
||
if (resp.config) {
|
||
updateConfig(resp.config);
|
||
} else {
|
||
validExtensions = lines;
|
||
}
|
||
renderAssets();
|
||
appendLog(t('extConfigSaved'));
|
||
closeExtConfigModal();
|
||
}).catch(function(err) {
|
||
byId('ghfu_ext_save_btn').disabled = false;
|
||
appendLog(t('requestFailed') + err);
|
||
});
|
||
}
|
||
|
||
function initLocalStatus() {
|
||
request('<%=url([[admin]], [[system]], [[ghfu]], [[local_status]])%>', {})
|
||
.then(function(resp) {
|
||
if (!resp || !resp.ok) {
|
||
return;
|
||
}
|
||
|
||
appVersion = String(resp.app_version || '');
|
||
updateTitle();
|
||
|
||
if (resp.config) {
|
||
byId('ghfu_repo').value = resp.config.github_repo || byId('ghfu_repo').value;
|
||
byId('ghfu_release').value = resp.config.selected_release || '';
|
||
byId('ghfu_keep').checked = (resp.config.keep_config || '1') === '1';
|
||
byId('ghfu_timeout').value = resp.config.fetch_timeout || byId('ghfu_timeout').value || '15';
|
||
updateConfig(resp.config);
|
||
}
|
||
|
||
if (typeof resp.tz_offset_min === 'number') {
|
||
routerTzOffsetMin = resp.tz_offset_min;
|
||
}
|
||
if (resp.install_epoch) {
|
||
lastInstallEpoch = resp.install_epoch;
|
||
}
|
||
|
||
renderStatusCard({
|
||
conclusion: t('beginFetch'),
|
||
conclusionCls: 'ghfu-conclusion-warn',
|
||
installValue: getCurrentInstallDisplay(),
|
||
latestValue: t('fetchingInProgress'),
|
||
publishedValue: t('fetchingInProgress'),
|
||
releaseTotalValue: t('fetchingInProgress'),
|
||
selectedRaw: (resp.config && resp.config.selected_release) || byId('ghfu_release').value
|
||
});
|
||
})
|
||
.catch(function() {
|
||
renderStatusCard({
|
||
conclusion: t('beginFetch'),
|
||
conclusionCls: 'ghfu-conclusion-warn',
|
||
installValue: getCurrentInstallDisplay(),
|
||
latestValue: t('fetchingInProgress'),
|
||
publishedValue: t('fetchingInProgress'),
|
||
releaseTotalValue: t('fetchingInProgress')
|
||
});
|
||
})
|
||
.then(function() {
|
||
refreshStatus();
|
||
});
|
||
}
|
||
|
||
initTexts();
|
||
applyTheme();
|
||
setUpgradeWorkflowBusy(false);
|
||
initLocalStatus();
|
||
})();
|
||
</script>
|
||
|
||
<%+footer%>
|