💋 Sync 2026-11-06 17:44:38
Merge-upstream / merge (push) Canceled after 0s

This commit is contained in:
github-actions[bot] committed 2026-11-06 17:44:38 +08:00
1 parent 1ebb01f4e8
commit f719901cab
44 files changed
+2190 -1439

No files matched your search

+2 -2
View File
@@ -6,13 +6,13 @@ PKG_UPSTREAM_TAG:=v$(PKG_UPSTREAM_VERSION)
PKG_SHORT_SHA:=3f27ef3
PKG_VERSION:=2026.11.04.$(PKG_UPSTREAM_VERSION)
PKG_RELEASE:=24
PKG_RELEASE:=25
PKG_SOURCE:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=f03c662c952072d2e7bdae30e6ab5a0360606266
PKG_SOURCE_VERSION:=3b0c0d7f9e5fa5d086fcd45cc211380ad6ccb077
PKG_HASH:=skip
PKG_BUILD_VERSION:=$(PKG_UPSTREAM_TAG)
+3 -3
View File
@@ -5,10 +5,10 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=dae
PKG_VERSION:=2026.10.30
PKG_RELEASE:=53
PKG_VERSION:=2026.11.05
PKG_RELEASE:=54
PKG_SOURCE:=dae-src-2026.10.30-55c59e68c059.tar.gz
PKG_SOURCE:=dae-src-2026.11.05-7cf97ff42dc6.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_HASH:=skip
+3 -3
View File
@@ -5,10 +5,10 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=daed
PKG_VERSION:=2026.10.30
PKG_RELEASE:=65
PKG_VERSION:=2026.11.05
PKG_RELEASE:=66
PKG_SOURCE:=daed-src-2026.10.30-1850c01d032c.tar.gz
PKG_SOURCE:=daed-src-2026.11.05-b1e30bc02200.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_HASH:=skip
+3 -6
View File
@@ -11,8 +11,8 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH)
PKG_NAME:=istoreenhance
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=0.8.5
PKG_RELEASE:=14
PKG_SOURCE_DATE:=0.8.8
PKG_RELEASE:=15
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
ARCH_HEXCODE=8664
@@ -88,11 +88,8 @@ define Package/$(PKG_NAME)/install
$(INSTALL_BIN) ./files/istoreenhance.init $(1)/etc/init.d/istoreenhance
$(INSTALL_BIN) ./files/istoreenhance.uci-default $(1)/etc/uci-defaults/istoreenhance
$(INSTALL_DATA) ./files/kspeeder-plugin.json $(1)/usr/share/kspeeder/kspeeder-plugin.json
if [ -d "$(KSPEEDER_WEB_ROOT)" ] && [ -f "$(KSPEEDER_WEB_ROOT)/desktop-entry.js" ]; then \
if [ -d "$(KSPEEDER_WEB_ROOT)" ] && [ -f "$(KSPEEDER_WEB_ROOT)/index.html" ]; then \
$(CP) $(KSPEEDER_WEB_ROOT)/* $(1)/usr/share/kspeeder/www/; \
else \
$(INSTALL_DATA) ./files/www/desktop-entry.js $(1)/usr/share/kspeeder/www/desktop-entry.js; \
$(INSTALL_DATA) ./files/www/index.html $(1)/usr/share/kspeeder/www/index.html; \
fi
$(INSTALL_DATA) ./files/logo.png $(1)/usr/share/kspeeder/www/logo.png
ln -sf /usr/share/kspeeder/kspeeder-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/20-kspeeder.json
+25 -3
View File
@@ -5,9 +5,23 @@
"icon": "logo.png",
"staticRoot": "/usr/share/kspeeder/www",
"desktop": {
"mode": "module",
"entry": "desktop-entry.js",
"isolation": "shadow-dom"
"mode": "iframe",
"target": {
"scheme": "http",
"hostMode": "request-host",
"port": {
"default": 5003,
"keys": {
"uci": "istoreenhance.@istoreenhance[0].adminport"
}
},
"path": "/"
},
"access": {
"scope": "lan",
"blockedTitle": "请在局域网内打开 KSpeeder",
"blockedMessage": "KSpeeder 运行在设备的独立服务端口,请连接设备所在局域网后再打开。"
}
},
"standalone": {
"entry": "index.html",
@@ -21,6 +35,14 @@
"path": "/"
}
},
"config": {
"providerOrder": ["uci"],
"providers": {
"uci": {
"type": "uci"
}
}
},
"backend": {
"type": "http",
"scheme": "http",
-260
View File
@@ -1,260 +0,0 @@
const stateByElement = new WeakMap();
function normalizeBasePath(path) {
const value = typeof path === "string" && path.trim() ? path.trim() : "/apps/kspeeder/";
return value.endsWith("/") ? value : `${value}/`;
}
function normalizeAPIBase(context) {
if (context && typeof context.apiBase === "string" && context.apiBase.trim()) {
return context.apiBase.endsWith("/") ? context.apiBase : `${context.apiBase}/`;
}
return `${normalizeBasePath(context && context.basePath)}api/`;
}
function escapeHTML(value) {
return String(value == null ? "" : value).replace(/[&<>"']/g, (char) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[char]);
}
async function fetchJSON(url, options) {
const response = await fetch(url, {
credentials: "same-origin",
headers: { Accept: "application/json" },
...options,
});
const text = await response.text();
let payload = null;
if (text) {
try {
payload = JSON.parse(text);
} catch (error) {
throw new Error(`API returned non-JSON response (${response.status})`);
}
}
if (!response.ok) {
const message = payload && (payload.message || payload.error || payload.details);
throw new Error(message || `API request failed (${response.status})`);
}
return payload || {};
}
function mirrorCounts(overview) {
const mirrorsByProfile = overview && typeof overview.mirrors === "object" ? overview.mirrors : {};
const result = { online: 0, total: 0, servedSpeed: "", servedDownload: "" };
Object.values(mirrorsByProfile).forEach((profile) => {
const mirrors = Array.isArray(profile && profile.mirrors) ? profile.mirrors : [];
result.total += mirrors.length;
result.online += mirrors.filter((mirror) => mirror && mirror.status === "online").length;
if (!result.servedSpeed && profile && profile.served_speed) {
result.servedSpeed = profile.served_speed;
}
if (!result.servedDownload && profile && profile.served_download) {
result.servedDownload = profile.served_download;
}
});
return result;
}
function profileRows(overview) {
const mirrorsByProfile = overview && typeof overview.mirrors === "object" ? overview.mirrors : {};
return Object.entries(mirrorsByProfile).map(([profile, status]) => {
const mirrors = Array.isArray(status && status.mirrors) ? status.mirrors : [];
const online = mirrors.filter((mirror) => mirror && mirror.status === "online").length;
const best = mirrors.find((mirror) => mirror && mirror.status === "online") || mirrors[0] || {};
return `
<tr>
<td>${escapeHTML(profile)}</td>
<td>${online}/${mirrors.length}</td>
<td>${escapeHTML(best.current_speed || best.test_speed || "-")}</td>
<td>${escapeHTML(best.name || "-")}</td>
<td>${escapeHTML(best.error_info || "-")}</td>
</tr>
`;
}).join("");
}
function render(root, model) {
const counts = mirrorCounts(model.overview);
const source = model.overview && model.overview.source ? model.overview.source : "-";
const error = model.error ? `<div class="notice danger">${escapeHTML(model.error)}</div>` : "";
const refreshing = model.refreshing ? "disabled" : "";
const rows = profileRows(model.overview);
root.innerHTML = `
<style>
:host { color-scheme: light; }
.ks-page { box-sizing: border-box; min-height: 100%; background: #f7f8fa; color: #1f2937; font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.ks-wrap { box-sizing: border-box; max-width: 1080px; margin: 0 auto; padding: 18px; }
.ks-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 14px; }
h1 { margin: 0; font-size: 20px; line-height: 1.2; font-weight: 650; color: #111827; }
.sub { margin-top: 5px; color: #667085; font-size: 13px; }
.actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
button { border: 1px solid #cfd7e3; background: #fff; color: #1f2937; border-radius: 6px; padding: 7px 10px; font-size: 13px; cursor: pointer; }
button.primary { background: #0f766e; border-color: #0f766e; color: #fff; }
button:disabled { cursor: wait; opacity: .62; }
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
.card { border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; padding: 12px; min-width: 0; }
.label { color: #667085; font-size: 12px; margin-bottom: 6px; }
.value { font-size: 18px; font-weight: 650; color: #111827; overflow-wrap: anywhere; }
.notice { border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; background: #fff7ed; border: 1px solid #fed7aa; color: #9a3412; }
.notice.danger { background: #fef2f2; border-color: #fecaca; color: #b91c1c; }
.section { margin-top: 12px; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; background: #fff; }
.section-title { display: flex; justify-content: space-between; gap: 10px; padding: 11px 12px; border-bottom: 1px solid #e2e8f0; font-weight: 650; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
th, td { padding: 9px 12px; border-bottom: 1px solid #edf2f7; text-align: left; vertical-align: top; overflow-wrap: anywhere; }
th { color: #667085; font-size: 12px; font-weight: 600; background: #fbfcfd; }
tr:last-child td { border-bottom: 0; }
.empty { padding: 18px 12px; color: #667085; }
@media (max-width: 720px) {
.ks-wrap { padding: 12px; }
.ks-top { display: block; }
.actions { justify-content: flex-start; margin-top: 10px; }
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
th:nth-child(5), td:nth-child(5) { display: none; }
}
@media (max-width: 430px) {
.grid { grid-template-columns: 1fr; }
th:nth-child(4), td:nth-child(4) { display: none; }
}
</style>
<div class="ks-page">
<div class="ks-wrap">
<div class="ks-top">
<div>
<h1>KSpeeder</h1>
<div class="sub">Docker 镜像下载加速状态</div>
</div>
<div class="actions">
<button data-action="reload">刷新</button>
<button class="primary" data-action="refresh-mirrors" ${refreshing}>${model.refreshing ? "测速中" : "刷新测速"}</button>
</div>
</div>
${error}
<div class="grid">
<div class="card"><div class="label">版本</div><div class="value">${escapeHTML(model.summary.version || "-")}</div></div>
<div class="card"><div class="label">可用节点</div><div class="value">${counts.online}/${counts.total}</div></div>
<div class="card"><div class="label">当前下发</div><div class="value">${escapeHTML(counts.servedSpeed || "-")}</div></div>
<div class="card"><div class="label">累计下发</div><div class="value">${escapeHTML(counts.servedDownload || "-")}</div></div>
</div>
<div class="section">
<div class="section-title"><span>镜像节点</span><span>${escapeHTML(source)}</span></div>
${rows ? `
<table>
<thead>
<tr>
<th>类型</th>
<th>在线</th>
<th>速度</th>
<th>节点</th>
<th>错误</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
` : '<div class="empty">暂无镜像状态,确认 iStoreEnhance 服务已启动并已配置缓存目录。</div>'}
</div>
</div>
</div>
`;
}
async function loadModel(state) {
const [summary, overview] = await Promise.all([
fetchJSON(`${state.apiBase}summary`),
fetchJSON(`${state.apiBase}mirrors/overview`),
]);
state.model.summary = summary;
state.model.overview = overview;
state.model.error = "";
}
async function reload(state) {
try {
await loadModel(state);
} catch (error) {
state.model.error = error instanceof Error ? error.message : "加载失败";
} finally {
render(state.root, state.model);
}
}
async function refreshMirrors(state) {
state.model.refreshing = true;
state.model.error = "";
render(state.root, state.model);
try {
await fetchJSON(`${state.apiBase}mirrors/refresh`, { method: "POST" });
await loadModel(state);
if (state.hostAdapter && state.hostAdapter.toast) {
state.hostAdapter.toast.success("KSpeeder 测速已刷新");
}
} catch (error) {
state.model.error = error instanceof Error ? error.message : "刷新测速失败";
if (state.hostAdapter && state.hostAdapter.toast) {
state.hostAdapter.toast.error(state.model.error);
}
} finally {
state.model.refreshing = false;
render(state.root, state.model);
}
}
export async function bootstrap() {}
export async function mount(props) {
const root = props && props.domElement;
if (!root) {
return;
}
const context = props.context || {};
const state = {
root,
apiBase: normalizeAPIBase(context),
hostAdapter: props.hostAdapter,
model: {
summary: {},
overview: {},
error: "",
refreshing: false,
},
};
stateByElement.set(root, state);
root.addEventListener("click", handleClick);
render(root, state.model);
await reload(state);
}
export async function unmount(props) {
const root = props && props.domElement;
if (!root) {
return;
}
root.removeEventListener("click", handleClick);
root.innerHTML = "";
stateByElement.delete(root);
}
function handleClick(event) {
const target = event.target && event.target.closest ? event.target.closest("[data-action]") : null;
if (!target) {
return;
}
const root = event.currentTarget;
const state = stateByElement.get(root);
if (!state) {
return;
}
const action = target.getAttribute("data-action");
if (action === "reload") {
void reload(state);
} else if (action === "refresh-mirrors") {
void refreshMirrors(state);
}
}
export default { bootstrap, mount, unmount };
-32
View File
@@ -1,32 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>KSpeeder</title>
<style>
html, body, #root {
width: 100%;
height: 100%;
margin: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
import { mount } from "./desktop-entry.js";
mount({
domElement: document.getElementById("root"),
context: {
basePath: "/apps/kspeeder/",
apiBase: "/apps/kspeeder/api/",
app: { id: "kspeeder", name: "KSpeeder", entry: "desktop-entry.js" },
window: { id: "standalone", title: "KSpeeder" },
theme: { mode: "light", variant: "" }
}
});
</script>
</body>
</html>
+2 -2
View File
@@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_KS_CLI:=$(ARCH)
PKG_NAME:=ks-cli
PKG_VERSION:=0.8.7
PKG_RELEASE:=4
PKG_VERSION:=0.8.8
PKG_RELEASE:=5
PKG_SOURCE:=ks-cli-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$(PKG_VERSION)/
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-ipsec-server
PKG_VERSION:=20260807
PKG_RELEASE:=3
PKG_RELEASE:=5
PKG_MAINTAINER:=Lienol <lawlienol@gmail.com>
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for KSpeeder
LUCI_DEPENDS:=+istoreenhance +luci-lib-linkeaseauth +linkease-app-entry
LUCI_PKGARCH:=all
PKG_VERSION:=0.8.5-r1
PKG_RELEASE:=8
PKG_VERSION:=0.8.8-r1
PKG_RELEASE:=9
include $(TOPDIR)/feeds/luci/luci.mk
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.411
PKG_RELEASE:=66
PKG_VERSION:=0.1.412
PKG_RELEASE:=67
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
+1 -1
View File
@@ -11,7 +11,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-vantage
PKG_VERSION:=1.0.15
PKG_RELEASE:=27
PKG_RELEASE:=28
PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage dashboard
@@ -106,6 +106,45 @@ return baseclass.extend({
return out;
},
/* the option of a layout in /etc/config/vantage_theme */
option: function(key) { return 'layout_' + key.replace('-', '_'); },
/* a layout as that option's value: 'hero:0:4 chart:0:2:h' */
encode: function(list) {
return list.map(function(e) { return e.id + ':' + e.c + ':' + e.w + (e.hide ? ':h' : ''); }).join(' ');
},
/* the template's data-vt-look-layout, 'wide=hero:0:4 chart:0:2:h;mid=default',
as { wide: [ ... ], mid: null }: null is the default layout. Lists go
through normalize like any stored layout. */
decode: function(text) {
var out = Object.create(null);
String(text || '').split(';').forEach(function(part) {
var m = /^(wide|mid|narrow)(_wired)?=(.+)$/.exec(part);
if (!m) return;
out[m[1] + (m[2] ? '-wired' : '')] = m[3] === 'default' ? null : m[3].split(' ').map(function(t) {
var f = t.split(':'), e = { id: f[0], c: int(f[1], 0), w: int(f[2], 1) };
if (f[3] === 'h') e.hide = true;
return e;
});
});
return out;
},
/* this browser's layouts with the router's (decode): the router's
first for a login that may save there, for any other only where
this browser has none */
merge: function(mine, saved, first) {
var out = Object.create(null);
Object.keys(mine).forEach(function(k) { out[k] = mine[k]; });
Object.keys(saved).forEach(function(k) {
if (!first && own(out, k)) return;
if (saved[k]) out[k] = saved[k];
else delete out[k];
});
return out;
},
/* what is worth storing */
compact: function(list) {
return list.map(function(e) { var o = { id: e.id, c: e.c, w: e.w }; if (e.hide) o.hide = true; return o; });
@@ -123,6 +123,14 @@ return baseclass.extend({
Linear scales never go below CHART_FLOOR, so an idle link stays a
flat line, and round up with niceCeil. */
CHART_STYLES: [ 'overlay', 'log', 'mirror', 'mirror-split', 'lanes' ],
/* the style in force: the router's first for a login that may save
there, for any other only where this browser has none */
chartPick: function(saved, mine, first) {
var ok = this.CHART_STYLES;
if (ok.indexOf(saved) >= 0 && (first || ok.indexOf(mine) < 0)) return saved;
return ok.indexOf(mine) >= 0 ? mine : ok[0];
},
CHART_FLOOR: 1e4,
chartGeom: function(style, peakDn, peakUp) {
@@ -490,8 +490,25 @@ function writeFolds(v) {
try { window.localStorage.setItem(FOLD_KEY, JSON.stringify(v)); } catch (e) {}
}
/* Tile layouts the viewer arranged, per screen class, in this browser;
vantage.layout validates whatever comes back */
/* What the Vantage theme's template hands over (data-vt-look-*): under it
the router keeps the layouts and the chart style with the Appearance */
function lookAttr(name) {
var root = typeof document !== 'undefined' ? document.documentElement : null;
return (root && root.getAttribute ? root.getAttribute(name) : null) || '';
}
function lookSaves() { return lookAttr('data-vt-look-save') === 'yes'; }
/* Done hands the theme what changed; true if it took it (a login that may
save). Without it, or for any other login, it stays in this browser. */
function offerView(values, done) {
var ev = null;
try { ev = new CustomEvent('vantage:view-save', { detail: { values: values, done: done }, cancelable: true }); } catch (e) { ev = null; }
return !!ev && typeof window.dispatchEvent === 'function' && !window.dispatchEvent(ev);
}
/* Tile layouts the viewer arranged, per screen class, this browser's copy
of them and the router's; vantage.layout validates whatever comes back */
var LAYOUT_KEY = 'vantage.layout.v1';
function readLayouts() {
@@ -500,7 +517,7 @@ function readLayouts() {
var out = Object.create(null);
if (v && typeof v === 'object' && !Array.isArray(v))
Object.keys(v).forEach(function(k) { if (/^(wide|mid|narrow)(-wired)?$/.test(k) && Array.isArray(v[k])) out[k] = v[k]; });
return out;
return layout.merge(out, layout.decode(lookAttr('data-vt-look-layout')), lookSaves());
}
function writeLayouts(v) {
@@ -511,33 +528,42 @@ function writeLayouts(v) {
}
/* The throughput chart style the viewer picked (series.CHART_STYLES),
in this browser */
this browser's copy of it and the router's */
var CHART_KEY = 'vantage.chart.v1';
function readChartStyle() {
var v = null;
try { v = window.localStorage.getItem(CHART_KEY); } catch (e) { v = null; }
return series.CHART_STYLES.indexOf(v) >= 0 ? v : series.CHART_STYLES[0];
return series.chartPick(lookAttr('data-vt-look-chart'), v, lookSaves());
}
function writeChartStyle(v) {
try {
if (v === series.CHART_STYLES[0]) window.localStorage.removeItem(CHART_KEY);
/* the default needs no entry, unless the router has another */
if (v === series.CHART_STYLES[0] && !lookAttr('data-vt-look-chart')) window.localStorage.removeItem(CHART_KEY);
else window.localStorage.setItem(CHART_KEY, v);
} catch (e) {}
}
/* The network path's backdrop the viewer picked (backdrop.STYLES), in
this browser */
/* The network path's backdrop the viewer picked (backdrop.STYLES). Under
the Vantage theme its Appearance decides (saved for every browser on
the router where the viewer may) and keeps the one in force in
data-vt-backdrop; elsewhere this browser keeps it. */
var BACKDROP_KEY = 'vantage.backdrop.v1';
function readBackdrop() {
var v = null;
var root = typeof document !== 'undefined' ? document.documentElement : null;
var v = root && root.getAttribute ? root.getAttribute('data-vt-backdrop') : null;
if (backdrop.STYLES.indexOf(v) >= 0) return v;
try { v = window.localStorage.getItem(BACKDROP_KEY); } catch (e) { v = null; }
return backdrop.STYLES.indexOf(v) >= 0 ? v : backdrop.STYLES[0];
}
function writeBackdrop(v) {
/* the theme takes it (and cancels the event) */
var ev = null;
try { ev = new CustomEvent('vantage:backdrop-set', { detail: v, cancelable: true }); } catch (e) { ev = null; }
if (ev && typeof window.dispatchEvent === 'function' && !window.dispatchEvent(ev)) return;
try {
if (v === backdrop.STYLES[0]) window.localStorage.removeItem(BACKDROP_KEY);
else window.localStorage.setItem(BACKDROP_KEY, v);
@@ -728,7 +754,7 @@ return view.extend({
this.backdropStyle = readBackdrop();
/* picked under the theme's Appearance (menu-vantage.js), here or
in another tab */
var self = this, follow = function() { self.setBackdrop(readBackdrop()); };
var self = this, follow = function() { self.showBackdrop(readBackdrop()); };
if (window.addEventListener) {
window.addEventListener('vantage:backdrop', follow);
window.addEventListener('storage', function(ev) { if (ev.key === BACKDROP_KEY) follow(); });
@@ -1283,6 +1309,7 @@ return view.extend({
},
setEditing: function(on) {
if (!on && this.editing && this.saveView()) return;
this.editing = !!on;
this.el.root.classList.toggle('vt-editing', this.editing);
this.el.layoutBtn.setAttribute('aria-pressed', String(this.editing));
@@ -1291,10 +1318,48 @@ return view.extend({
at once, nothing held from the other mode */
this.held = Object.create(null);
this.doLayout();
if (this.editing) this.el.editDone.focus();
if (this.editing) {
this.viewStart = this.viewValues();
this.editNote(lookAttr('data-vt-look-save') === 'relogin' ? _('Log in again to save this for every browser.') : '');
this.el.editDone.focus();
}
else this.el.layoutBtn.focus();
},
editNote: function(text) {
this.el.editNote.hidden = !text;
txt(this.el.editNote, text);
},
/* the options of /etc/config/vantage_theme this dashboard stands for */
viewValues: function() {
var self = this, out = { chart: this.chartStyle };
[ 'wide', 'mid', 'narrow' ].forEach(function(tier) {
[ false, true ].forEach(function(wired) {
var k = layout.key(tier, wired);
out[layout.option(k)] = self.layouts[k] ? layout.encode(self.layouts[k]) : 'default';
});
});
return out;
},
/* Done: what changed goes to the theme in one write. True while that
runs: Customize closes when it is saved, and stays open with a note
if it was not, so the pick can be saved again. */
saveView: function() {
var self = this, now = this.viewValues(), then = this.viewStart || {}, out = {};
if (this.viewSaving) return true;
Object.keys(now).forEach(function(k) { if (now[k] !== then[k]) out[k] = now[k]; });
if (!Object.keys(out).length) return false;
this.viewSaving = offerView(out, function(ok) {
self.viewSaving = false;
if (!ok) { self.editNote(_('Could not save this for every browser. Try again.')); return; }
self.viewStart = now;
self.setEditing(false);
});
return this.viewSaving;
},
/* the throughput chart style, picked under Customize */
buildChartPick: function() {
var self = this, names = {
@@ -1322,10 +1387,18 @@ return view.extend({
return E('div', { 'class': 'vt-edit-field' }, [ E('label', { 'for': 'vt-backdrop-style' }, [ _('Network path background') ]), pick ]);
},
/* picked here (Customize): saved, and shown */
setBackdrop: function(v) {
if (backdrop.STYLES.indexOf(v) < 0 || v === this.backdropStyle) return;
this.backdropStyle = v;
writeBackdrop(v);
this.showBackdrop(v);
},
/* picked elsewhere (the theme's Appearance, another tab): shown only,
never saved back */
showBackdrop: function(v) {
if (backdrop.STYLES.indexOf(v) < 0 || v === this.backdropStyle) return;
this.backdropStyle = v;
if (this.backdrop) this.backdrop.style(v);
if (this.backdropPick && this.backdropPick.value !== v) this.backdropPick.value = v;
},
@@ -1341,10 +1414,11 @@ return view.extend({
buildEditBar: function() {
var self = this, el = this.el;
el.editHint = E('span', { 'class': 'vt-hint' });
el.editNote = E('span', { 'class': 'vt-hint', 'role': 'status', 'hidden': 'hidden' });
el.editHidden = E('div', { 'class': 'vt-edit-hidden' });
el.editSay = E('span', { 'class': 'vt-sr', 'role': 'status', 'aria-live': 'polite' });
el.editBar = E('div', { 'class': 'vt-editbar', 'role': 'region', 'aria-label': _('Customize the dashboard'), 'hidden': 'hidden' }, [
E('div', { 'class': 'vt-editbar-text' }, [ E('strong', {}, [ _('Customize the dashboard') ]), el.editHint ]),
E('div', { 'class': 'vt-editbar-text' }, [ E('strong', {}, [ _('Customize the dashboard') ]), el.editHint, el.editNote ]),
el.editHidden,
E('div', { 'class': 'vt-edit-picks' }, [ this.buildChartPick(), this.buildBackdropPick() ]),
E('div', { 'class': 'vt-editbar-actions' }, [
@@ -1417,9 +1491,10 @@ return view.extend({
var self = this, l = this.lay;
if (!l) return;
var shown = l.list.filter(function(e) { return !e.hide; });
txt(this.el.editHint, l.tier === 'wide' ? _('Drag a tile by its handle or use its buttons. Saved in this browser for wide screens.')
: l.tier === 'mid' ? _('Drag a tile by its handle or use its buttons. Saved in this browser for medium screens.')
: _('Drag a tile by its handle or use its buttons. Saved in this browser for narrow screens.'));
var every = lookSaves();
txt(this.el.editHint, l.tier === 'wide' ? (every ? _('Drag a tile by its handle or use its buttons. Done saves it for wide screens in every browser.') : _('Drag a tile by its handle or use its buttons. Saved in this browser for wide screens.'))
: l.tier === 'mid' ? (every ? _('Drag a tile by its handle or use its buttons. Done saves it for medium screens in every browser.') : _('Drag a tile by its handle or use its buttons. Saved in this browser for medium screens.'))
: every ? _('Drag a tile by its handle or use its buttons. Done saves it for narrow screens in every browser.') : _('Drag a tile by its handle or use its buttons. Saved in this browser for narrow screens.'));
this.el.root.classList.toggle('vt-edit-1col', l.cols === 1);
shown.forEach(function(e, i) {
var o = self.ov[e.id];
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+18 -3
View File
@@ -8,11 +8,12 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Argon Theme
# Older SDKs require the coreutils parent menu to enable coreutils-timeout.
LUCI_DEPENDS:=+luci-base +USE_APK:wget-any +!USE_APK:wget +jsonfilter +ucode-mod-math +flock +coreutils +coreutils-timeout
# Virtual wget providers include wget-nossl, but every wallpaper API uses HTTPS.
LUCI_DEPENDS:=+luci-base +wget-ssl +ca-bundle +jsonfilter +ucode-mod-math +flock +coreutils +coreutils-timeout
# Keep existing translation package names and the argon-config message domain.
LUCI_BASENAME:=argon-config
PKG_VERSION:=3.1.2
PKG_RELEASE:=41
PKG_VERSION:=3.1.3
PKG_RELEASE:=42
PKG_LICENSE:=Apache-2.0 MIT
# LuCI derives translations independently of the theme version. Keep release
@@ -36,6 +37,20 @@ define Package/luci-theme-argon/conffiles
/etc/config/argon
endef
# Some firmware builds bundle translations into the theme instead of separate
# language packages. Preserve them even when an older updater installs us alone.
define Package/luci-theme-argon/preinst
#!/bin/sh
root="$${IPKG_INSTROOT}"
for catalog in "$$root"/usr/lib/lua/luci/i18n/argon-config.*.lmo "$$root"/usr/lib/lua/luci/i18n/argon.*.lmo; do
[ -f "$$catalog" ] && [ ! -L "$$catalog" ] || continue
umask 077
mkdir -p "$$root/etc/argon-config-migration/i18n" || exit 1
cp -p "$$catalog" "$$root/etc/argon-config-migration/i18n/$${catalog##*/}" || exit 1
done
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
+4 -2
View File
@@ -41,7 +41,7 @@ It also supports automatic and manual switching between light and dark modes.
- **Layout fixes.** Improve dashboard actions, overview layouts, dynamic lists and custom IP/MAC inputs across narrow screens, light/dark modes and RTL layouts.
- **Compatibility improvements.** Improve compatibility with stable OpenWrt firmware.
Users on v3.0.0 or earlier must install v3.1.2 manually once to get online updates. Users on v3.1.0 or later can upgrade through Theme updates. See the [release notes](RELEASE.md).
Users on v3.0.0 or earlier must install v3.1.3 manually once to get online updates. Users on v3.1.0 or later can upgrade through Theme updates. See the [release notes](RELEASE.md).
## 3.0 highlights
@@ -99,7 +99,9 @@ Local image and video backgrounds are supported, along with Bing, Unsplash and W
Under **Theme updates**, check the latest stable release, read its release notes and download the package matching your firmware's package manager.
When a newer version is available, choose **Update now** and confirm to install the theme using the firmware’s native APK or OPKG package manager, preserving your Argon configuration. The page shows download, verification and installation status plus installation logs, and resumes monitoring after a page refresh. Reload the page after installation. Automatic installation requires a matching release package and SHA256 digest and does not perform downgrades. It updates the main theme package; separate translation packages remain managed by the system package manager.
When a newer version is available, choose **Update now** and confirm to install the theme using the firmware’s native APK or OPKG package manager, preserving your Argon configuration. The page shows download, verification and installation status plus installation logs, and resumes monitoring after a page refresh. Reload the page after installation. The updater downloads and verifies the theme and matching packages for your existing Argon translations before installing them. It also installs the configured LuCI language when that translation is available. Automatic installation requires matching release packages and SHA256 digests and does not downgrade the theme. If verified metadata is missing for an existing translation, update the theme and language packages manually from the same release.
Translations bundled into a firmware's theme package, such as KWRT's Chinese catalog, are preserved during the first upgrade to official packages, including upgrades started by an older updater. A separately installed language package can then take ownership of the catalog.
When building firmware, stop including the old standalone `luci-app-argon-config` plugin alongside the theme: its files conflict with the integrated settings. See [Build with OpenWrt](docs/DEVELOPMENT.md#build-with-openwrt) for existing build scripts.
+4 -2
View File
@@ -41,7 +41,7 @@ Argon 是**一款干净整洁的 OpenWrt LuCI 主题**,
- **布局修复**:优化 Dashboard 操作区、概览布局、动态列表和自定义 IP/MAC 输入框,兼顾窄屏、亮暗色和 RTL 布局。
- **兼容性改进**:改善稳定版 OpenWrt 固件上的兼容性。
v3.0.0 及更早版本用户需先手动安装一次 v3.1.2,之后即可使用在线更新。v3.1.0 及以上用户可直接通过主题更新升级。详见[更新日志](RELEASE_ZH.md)。
v3.0.0 及更早版本用户需先手动安装一次 v3.1.3,之后即可使用在线更新。v3.1.0 及以上用户可直接通过主题更新升级。详见[更新日志](RELEASE_ZH.md)。
## 3.0 主要特性
@@ -99,7 +99,9 @@ apk add --allow-untrusted ./luci-theme-argon-*.apk
在**主题更新**中手动检查最新稳定版、查看更新日志,并下载与固件包管理器匹配的安装包。
检测到更高版本后,可点击**立即更新**并确认,通过固件自带的 APK 或 OPKG 安装主题,保留当前 Argon 配置。页面显示下载、校验、安装状态及安装日志,刷新页面后可继续查看进度;安装完成后重新加载页面。自动安装要求正式发布提供对应安装包及 SHA256 校验值,不执行降级。此功能更新主题主包,独立语言包仍由系统包管理器管理。
检测到更高版本后,可点击**立即更新**并确认,通过固件自带的 APK 或 OPKG 安装主题,保留当前 Argon 配置。页面显示下载、校验、安装状态及安装日志,刷新页面后可继续查看进度;安装完成后重新加载页面。更新器会先下载并校验主题与现有 Argon 翻译对应的语言包,再执行安装;如果发布中提供了 LuCI 当前配置语言的翻译,也会一并安装。自动安装要求对应安装包及 SHA256 校验值,不降级主题。现有翻译缺少可校验的语言包信息时,请从同一次发布中手动安装主题和语言包。
从 KWRT 等将中文翻译内嵌在主题主包的固件升级时,首次安装官方主题会保留原有翻译,使用旧版更新器发起升级也适用。之后安装独立语言包即可完成翻译文件的归属交接。
自行编译固件时,不要再同时引入旧版独立 `luci-app-argon-config` 插件;它会与主题内置文件冲突。已有自动编译脚本的调整方式见[源码构建说明](docs/DEVELOPMENT_ZH.md#源码构建)。
+6
View File
@@ -1,5 +1,11 @@
# Update log for master branch
## v3.1.3 [ 2026.11.06 ]
- Preserve bundled Argon translations during upgrades and synchronize language packages in subsequent online updates (#737).
- Fix online wallpapers failing on firmware that selects a downloader without HTTPS support (#738).
- Keep bridge VLAN dropdown labels readable instead of wrapping one character per line (#738).
## v3.1.2 [ 2026.11.02 ]
- Fix port card wrapping and alignment, including ImmortalWrt.
+6
View File
@@ -1,5 +1,11 @@
# Master 分支的更新日志
## v3.1.3 [ 2026.11.06 ]
- 修复部分固件升级后 Argon 翻译丢失,保留已有翻译,并在后续在线升级时同步语言包(#737)。
- 修复部分固件因下载器缺少 HTTPS 支持而无法获取在线壁纸的问题(#738)。
- 修复网桥 VLAN 下拉菜单文字逐字竖排的问题(#738)。
## v3.1.2 [ 2026.11.02 ]
- 修复端口卡片换行和高度不一致,兼容 ImmortalWrt。
+3 -1
View File
@@ -93,7 +93,7 @@ Use monochrome SVGs with a `viewBox` and transparent background. CSS masks suppl
## Static update feed deployment
The router reads a static `/updates/stable.json` manifest without calling the GitHub API. After publication, CI uses its own `GITHUB_TOKEN` to retrieve the official release, verifies the theme package SHA256 digests and sizes, and generates one artifact. GitHub Pages publishes that artifact directly; Cloudflare Pages deploys the same files from a dedicated `updates` Git branch. Packages continue to download from the pinned GitHub Release, and translation packages remain separate.
The router reads a static `/updates/stable.json` manifest without calling the GitHub API. After publication, CI uses its own `GITHUB_TOKEN` to retrieve the official release, verifies theme and translation package SHA256 digests and sizes, and generates one artifact. GitHub Pages publishes that artifact directly; Cloudflare Pages deploys the same files from a dedicated `updates` Git branch. Packages continue to download from the pinned GitHub Release. The installer includes matching packages for existing Argon translations and the configured LuCI language when available.
Initial maintainer setup:
@@ -119,6 +119,8 @@ Run **Publish update feeds** again to republish GitHub Pages. Identical artifact
The manifest contains `schema`, `repository`, `tag`, `published_at`, `notes`, and the `apk`/`ipk` filenames with `_sha256`/`_size` fields. Metadata comes from the published package bytes rather than master's Makefile. Both formats must be complete and match the tag. Malformed feeds trigger fallback and never overwrite the previous successful cache. Checks are manual, with a five-minute success cache, a one-minute failure cooldown and a lock that coalesces concurrent requests.
Schema 1 also supports an optional `translations` object keyed by language package suffix, such as `zh-cn`. Each entry has the same six package fields for both formats. Names must match the theme's release, each translation is limited to 1 MiB, and at most 32 languages are accepted. Existing translations without verified metadata disable automatic installation. All required packages are downloaded and verified before the package manager runs; storage checks use their combined size, and completion requires the expected installed versions. The theme's pre-install hook and UCI initialization preserve bundled catalogs across a first upgrade made by an older updater, without replacing catalogs already supplied by independent language packages.
Source builds default to an empty CF primary. Configure one before packaging:
```sh
+3 -1
View File
@@ -93,7 +93,7 @@ lessc less/cascade.less htdocs/luci-static/argon/css/cascade.css
## 静态更新源部署
更新检查读取 `/updates/stable.json` 静态清单。路由器不调用 GitHub API;发布工作流使用自身的 `GITHUB_TOKEN` 读取正式 Release,核验主题包的 SHA256/大小后生成一份产物。GitHub Pages 直接发布该产物,Cloudflare Pages 从专用 `updates` 分支部署同一份文件。安装包继续从固定的 GitHub Release 版本下载,独立语言包仍单独安装。
更新检查读取 `/updates/stable.json` 静态清单。路由器不调用 GitHub API;发布工作流使用自身的 `GITHUB_TOKEN` 读取正式 Release,核验主题和语言包的 SHA256/大小后生成一份产物。GitHub Pages 直接发布该产物,Cloudflare Pages 从专用 `updates` 分支部署同一份文件。安装包继续从固定的 GitHub Release 版本下载;安装器会包含现有 Argon 翻译及可用的 LuCI 当前配置语言对应的包。
首次启用需要仓库维护者完成以下配置:
@@ -119,6 +119,8 @@ lessc less/cascade.less htdocs/luci-static/argon/css/cascade.css
清单字段包括 `schema`、`repository`、`tag`、`published_at`、`notes`,以及 `apk`/`ipk` 文件名和对应的 `_sha256`/`_size`。由最终发布包计算元数据,不从 master 的 Makefile 推断最新版。两种格式均须完整且属于同一版本;客户端拒绝损坏清单,并保留上一次成功记录。请求只在手动检查时发起,成功缓存五分钟,双源均失败后冷却一分钟。
Schema 1 还支持可选的 `translations` 对象,以 `zh-cn` 等语言包后缀为键,每项包含双格式对应的六个包字段。文件名须与主题发行版本匹配,单个语言包不超过 1 MiB,最多接受 32 种语言。现有翻译缺少可校验信息时不开放自动安装。所有所需包先完成下载和校验,再调用包管理器;空间检查按总大小计算,安装完成后核对各包版本。主题的 preinst 与 UCI 初始化会保留旧更新器首次升级时的内嵌翻译,并保留独立语言包已提供的文件。
源码构建默认不包含 CF 地址。需要自行指定时,在打包前执行:
```sh
@@ -3788,7 +3788,7 @@ body:not(.Interfaces) .cbi-rowstyle-2:first-child {
background: transparent;
color: inherit;
white-space: normal;
overflow-wrap: anywhere;
overflow-wrap: break-word;
cursor: pointer;
}
.cbi-dropdown[open] > ul.dropdown > li > label {
@@ -99,7 +99,9 @@
background: transparent;
color: inherit;
white-space: normal;
overflow-wrap: anywhere;
// Let full words contribute to the menu's intrinsic width. `anywhere`
// shrinks compact controls' expanded labels down to a single character.
overflow-wrap: break-word;
cursor: pointer;
> label {
@@ -1,5 +1,22 @@
#!/bin/sh
# OPKG/APK may remove catalogs owned by the previous theme. Keep existing
# translations until a separate language package takes ownership of them.
i18n_backup=/etc/argon-config-migration/i18n
if [ -d "$i18n_backup" ]; then
mkdir -p /usr/lib/lua/luci/i18n || exit 1
for catalog in "$i18n_backup"/argon-config.*.lmo "$i18n_backup"/argon.*.lmo; do
[ -f "$catalog" ] && [ ! -L "$catalog" ] || continue
target="/usr/lib/lua/luci/i18n/${catalog##*/}"
if [ ! -e "$target" ] && [ ! -L "$target" ]; then
cp -p "$catalog" "$target" || exit 1
fi
rm -f "$catalog"
done
rmdir "$i18n_backup" 2>/dev/null || true
rmdir /etc/argon-config-migration 2>/dev/null || true
fi
# The empty legacy package saves its conffile before releasing file ownership.
if [ -f /etc/argon-config-migration/argon ]; then
cp -p /etc/argon-config-migration/argon /etc/config/argon || exit 1
@@ -6,6 +6,10 @@ readonly state_dir='/var/run/argon-update'
readonly cache_file='/var/run/argon-update/release.json'
readonly sources_file='/usr/share/argon/update-sources.json'
readonly repository='jerrykuku/luci-theme-argon'
readonly i18n_dir='/usr/lib/lua/luci/i18n'
wget_bin=/usr/libexec/wget-ssl
[ -x "$wget_bin" ] || wget_bin="$(command -v uclient-fetch || command -v wget)"
opkg_query() (
# OPKG locks its database even for status and comparison commands. Keep our
@@ -66,7 +70,45 @@ load_sources() {
fi
}
normalize_manifest() {
valid_language() {
printf '%s\n' "$1" | grep -Eq '^[a-z]{2,3}(-[a-z0-9]{2,8})?$'
}
valid_translation_asset() {
[ "$1" = "$4" ] && printf '%s\n' "$2" | grep -Eq '^[a-f0-9]{64}$' &&
printf '%s\n' "$3" | grep -Eq '^[1-9][0-9]{0,6}$' && [ "$3" -le 1048576 ]
}
translation_languages() (
json_get_type kind translations
[ -n "$kind" ] || exit 0
[ "$kind" = object ] && json_select translations || exit 1
json_get_keys language_keys
count=0
for language_key in $language_keys; do
# jshn's reader converts hyphens to underscores. Recover the package
# suffix, then require filenames that exactly match that language.
language="$(printf '%s' "$language_key" | tr '_' '-')"
count=$((count + 1))
[ "$count" -le 32 ] && valid_language "$language" || exit 1
json_get_type kind "$language_key"
[ "$kind" = object ] && json_select "$language_key" || exit 1
for kind in ipk apk; do
json_get_var file "$kind"
json_get_var checksum "${kind}_sha256"
json_get_var bytes "${kind}_size"
case "$kind" in
ipk) expected="luci-i18n-argon-config-${language}_v${ipk#luci-theme-argon_}" ;;
apk) expected="luci-i18n-argon-config-${language}-${apk#luci-theme-argon-}" ;;
esac
valid_translation_asset "$file" "$checksum" "$bytes" "$expected" || exit 1
done
json_select ..
printf '%s\n' "$language"
done
)
read_manifest_fields() {
json_load_file "$1" || return 1
json_get_var schema schema
json_get_var manifest_repository repository
@@ -82,6 +124,17 @@ normalize_manifest() {
done
printf '%s\n' "$published_at" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$' || return 1
[ "${#notes}" -le 131072 ] || return 1
languages="$(translation_languages)" || return 1
}
normalize_manifest() {
local manifest_namespace language field
json_set_namespace manifest_input manifest_namespace
if ! read_manifest_fields "$1"; then
json_set_namespace "$manifest_namespace"
return 1
fi
json_set_namespace "$manifest_namespace"
# Copy only the contract fields, not remote status, URLs or local timestamps.
json_init
json_add_int schema 1
@@ -95,6 +148,29 @@ normalize_manifest() {
json_add_string apk_sha256 "$apk_sha256"
json_add_int ipk_size "$ipk_size"
json_add_int apk_size "$apk_size"
if [ -n "$languages" ]; then
json_add_object translations
for language in $languages; do
json_set_namespace manifest_input
json_select translations
json_select "$language"
for field in ipk apk ipk_sha256 apk_sha256 ipk_size apk_size; do
json_get_var "translation_$field" "$field"
done
json_select ..
json_select ..
json_set_namespace "$manifest_namespace"
json_add_object "$language"
json_add_string ipk "$translation_ipk"
json_add_string apk "$translation_apk"
json_add_string ipk_sha256 "$translation_ipk_sha256"
json_add_string apk_sha256 "$translation_apk_sha256"
json_add_int ipk_size "$translation_ipk_size"
json_add_int apk_size "$translation_apk_size"
json_close_object
done
json_close_object
fi
json_add_int checked_at "$(date +%s)"
}
@@ -131,7 +207,7 @@ check_updates() {
for source_url in "$primary_url" "$fallback_url"; do
[ -n "$source_url" ] || continue
# The two attempts together stay below rpcd's request timeout.
if (ulimit -f 2048; timeout -k 1 5 wget -T 4 -q -U 'luci-theme-argon' \
if (ulimit -f 2048; timeout -k 1 5 "$wget_bin" -T 4 -q -U 'luci-theme-argon' \
-O "$work/manifest.json" "$source_url") 2>/dev/null &&
[ "$(wc -c < "$work/manifest.json")" -le 1048576 ] && normalize_manifest "$work/manifest.json"; then
json_dump > "$work/cache.json"
@@ -172,6 +248,79 @@ select_package() {
[ "$size" -gt 0 ] && [ "$size" -le 8388608 ]
}
installed_translations() {
{
for catalog in "$i18n_dir"/argon-config.*.lmo "$i18n_dir"/argon.*.lmo; do
[ -f "$catalog" ] && [ ! -L "$catalog" ] || continue
language="${catalog##*/}"
language="${language#*.}"
printf '%s\n' "${language%.lmo}"
done
case "$package_manager" in
apk) apk list --installed 'luci-i18n-argon-config-*' 2>/dev/null |
sed -n 's/^luci-i18n-argon-config-\([a-z][a-z0-9-]*\)-[0-9].*/\1/p' ;;
opkg) opkg_query list-installed 'luci-i18n-argon-config-*' 2>/dev/null |
awk '$1 ~ /^luci-i18n-argon-config-/ { sub(/^luci-i18n-argon-config-/, "", $1); print $1 }' ;;
esac
} | sort -u | while IFS= read -r language; do
valid_language "$language" && printf '%s\n' "$language"
done
}
# Emit a private, validated download plan. Never install the theme first and
# discover afterwards that the language package is missing or corrupt.
package_plan() (
printf '%s %s %s\n' "$package" "$digest" "$size"
wanted="$(installed_translations)"
configured="$(uci -q get luci.main.lang 2>/dev/null | tr 'A-Z_' 'a-z-')"
case "$configured" in zh-cn|zh-hans) configured=zh-cn ;; zh-tw|zh-hant) configured=zh-tw ;; nb-no) configured=no ;; esac
if ! json_select translations; then
[ -z "$wanted" ]
exit $?
fi
if valid_language "$configured"; then
json_get_type kind "$configured"
[ "$kind" != object ] || wanted="$(printf '%s\n%s\n' "$wanted" "$configured" | sort -u)"
fi
for language in $wanted; do
json_select "$language" || exit 1
json_get_var file "$format"
json_get_var checksum "${format}_sha256"
json_get_var bytes "${format}_size"
case "$format" in
ipk) expected="luci-i18n-argon-config-${language}_v${target_version}_all.ipk" ;;
apk) expected="luci-i18n-argon-config-${language}-${target_version}.apk" ;;
esac
valid_translation_asset "$file" "$checksum" "$bytes" "$expected" || exit 1
printf '%s %s %s\n' "$file" "$checksum" "$bytes"
json_select ..
done
)
verify_translation_versions() {
local file checksum bytes name expected actual
while read -r file checksum bytes; do
case "$file" in luci-i18n-argon-config-*) ;; *) continue ;; esac
case "$package_manager" in
apk)
name="${file%-${target_version}.apk}"
expected="$target_version"
actual="$(apk list --installed "$name" 2>/dev/null | awk -v prefix="$name-" '
index($1, prefix) == 1 { print substr($1, length(prefix) + 1); exit }')" ;;
opkg)
name="${file%_v${target_version}_all.ipk}"
expected="v$target_version"
actual="$(opkg_query status "$name" 2>/dev/null | awk '
/^Version: / { version=$2 }
/^Status: .* installed$/ { installed=1 }
END { if (installed) print version }')" ;;
esac
[ "$actual" = "$expected" ] || return 1
done <<EOF
$1
EOF
}
is_upgrade() {
# Reject unknown/custom version syntax before asking the native comparator.
printf '%s\n' "$current_version" | grep -Eq '^[0-9]+(\.[0-9]+){1,3}((~|_)(alpha|beta|rc)[0-9]+|-(alpha|beta|rc)\.[0-9]+)?(-r?[0-9]+)?$' || return 1
@@ -244,7 +393,7 @@ respond() {
json_get_var checked_at checked_at
if [ "$1" = 0 ] && printf '%s\n' "$checked_at" | grep -Eq '^[0-9]{1,11}$' &&
[ "$now" -ge "$checked_at" ] && [ "$((now - checked_at))" -lt 300 ] &&
select_package && is_upgrade && command -v flock >/dev/null 2>&1 && command -v sha256sum >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1; then
select_package && is_upgrade && package_plan >/dev/null && command -v flock >/dev/null 2>&1 && command -v sha256sum >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1; then
can_upgrade=1
fi
json_add_int result "$1"
@@ -268,6 +417,7 @@ write_status() {
}
run_upgrade() {
local plan file checksum bytes total_size
# Run a private copy, since installing the theme replaces this RPC script.
work="${0%/*}"
case "$work" in "$state_dir"/job.??????) ;; *) return 1 ;; esac
@@ -277,27 +427,39 @@ run_upgrade() {
json_load_file "$work/release.json" && select_package || { write_status failed metadata; return; }
installed_version
is_upgrade || { write_status failed version; return; }
plan="$(package_plan)" || { write_status failed metadata; return; }
total_size="$(printf '%s\n' "$plan" | awk '{ size += $3 } END { print size }')"
# Leave room for download metadata and unpacking. The package manager also
# performs its own dependency and storage checks; never force those checks.
tmp_free="$(df -Pk "$state_dir" | awk 'END { print $4 }')"
root_path=/
[ ! -d /overlay ] || root_path=/overlay
root_free="$(df -Pk "$root_path" | awk 'END { print $4 }')"
if [ "${tmp_free:-0}" -lt "$((size / 1024 + 2048))" ] ||
[ "${root_free:-0}" -lt "$((size * 3 / 1024 + 1024))" ]; then
if [ "${tmp_free:-0}" -lt "$((total_size / 1024 + 2048))" ] ||
[ "${root_free:-0}" -lt "$((total_size * 3 / 1024 + 1024))" ]; then
write_status failed space
return
fi
write_status downloading
url="https://github.com/jerrykuku/luci-theme-argon/releases/download/$tag/$package"
(ulimit -f 16384; timeout -k 2 90 wget -T 20 -q -U 'luci-theme-argon' \
-O "$work/$package" "$url") 2>/dev/null || { write_status failed network; return; }
set --
while read -r file checksum bytes; do
url="https://github.com/jerrykuku/luci-theme-argon/releases/download/$tag/$file"
(ulimit -f 16384; timeout -k 2 90 "$wget_bin" -T 20 -q -U 'luci-theme-argon' \
-O "$work/$file" "$url") 2>/dev/null || { write_status failed network; return; }
set -- "$@" "$work/$file"
done <<EOF
$plan
EOF
write_status verifying
if [ "$(wc -c < "$work/$package")" -ne "$size" ] ||
[ "$(sha256sum "$work/$package" | awk '{ print $1 }')" != "$digest" ]; then
write_status failed checksum
return
fi
while read -r file checksum bytes; do
if [ "$(wc -c < "$work/$file")" -ne "$bytes" ] ||
[ "$(sha256sum "$work/$file" | awk '{ print $1 }')" != "$checksum" ]; then
write_status failed checksum
return
fi
done <<EOF
$plan
EOF
# Recheck immediately before modifying the package database.
installed_version
is_upgrade || { write_status failed version; return; }
@@ -305,8 +467,8 @@ run_upgrade() {
# Bound captured output without imposing a file-size limit on the installer.
(
case "$package_manager" in
apk) apk add --allow-untrusted "$work/$package" ;;
opkg) opkg install "$work/$package" ;;
apk) apk add --allow-untrusted "$@" ;;
opkg) opkg install "$@" ;;
esac
printf '%s\n' "$?" > "$work/exit"
) 2>&1 | tail -c 8192 > "$state_dir/log"
@@ -315,7 +477,7 @@ run_upgrade() {
return
fi
installed_version
if [ "$current_version" != "$target_version" ]; then
if [ "$current_version" != "$target_version" ] || ! verify_translation_versions "$plan"; then
write_status failed install
return
fi
@@ -331,6 +493,7 @@ start_upgrade() {
read_cache && [ "$now" -ge "$checked_at" ] && [ "$((now - checked_at))" -lt 300 ] &&
select_package && [ "$tag" = "$requested_tag" ] && [ "$package" = "$requested_package" ] || { fail 3; return; }
is_upgrade || { fail 5; return; }
package_plan >/dev/null || { fail 3; return; }
prepare_state || { fail 1; return; }
exec 9>"$state_dir/lock"
flock -n 9 || { fail 4; return; }
@@ -4,8 +4,7 @@
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
# the script will be excuted when `argon.@global[0].bing_background == '1'`
# defaults to 'bing' to be compatible with old config
# Default to Bing for the legacy bing_background configuration.
WEB_PIC_SRC="$(uci -q get argon.@global[0].online_wallpaper || echo 'bing')"
# API_KEY is used by Unsplash and Wallhaven when configured.
@@ -16,10 +15,23 @@ EXACT_RESO="$(uci -q get argon.@global[0].use_exact_resolution || echo '1')"
CACHE="/var/run/argon_${WEB_PIC_SRC}.url"
WRLOCK="/var/lock/argon_${WEB_PIC_SRC}.lock"
# Older releases give wget-ssl and wget-nossl the same alternative priority.
# Use the TLS binary directly even when /usr/bin/wget still selects wget-nossl.
WGET=/usr/libexec/wget-ssl
if [ ! -x "$WGET" ]; then
# Keep source-only deployments compatible with the firmware's downloader.
WGET="$(command -v uclient-fetch || command -v wget)"
fi
fetch_url() {
# GNU wget may retry beyond -T. Bound the whole request below rpcd's timeout.
timeout -k 1 6 "$WGET" -T 5 -qO- "$1"
}
fetch_pic_url() {
case "$WEB_PIC_SRC" in
bing)
local picpath="$(wget -T3 -qO- \
local picpath="$(fetch_url \
"https://www.bing.com/HPImageArchive.aspx?format=js&n=1" |
jsonfilter -qe '@.images[0].url' |
sed 's/1920x1080/UHD/g')"
@@ -27,20 +39,20 @@ fetch_pic_url() {
;;
unsplash)
if [ -n "$API_KEY" ]; then
wget -T3 -qO- "https://api.unsplash.com/photos/random?client_id=${API_KEY}" |
fetch_url "https://api.unsplash.com/photos/random?client_id=${API_KEY}" |
jsonfilter -qe '@["urls"]["regular"]'
fi
;;
unsplash_*)
local collection_id="${WEB_PIC_SRC#unsplash_}"
if [ -n "$API_KEY" ]; then
wget -T3 -qO- \
fetch_url \
"https://api.unsplash.com/photos/random?client_id=${API_KEY}&collections=${collection_id}" |
jsonfilter -qe '@["urls"]["regular"]'
fi
;;
wallhaven)
wget -T3 -qO- \
fetch_url \
"https://wallhaven.cc/api/v1/search?resolutions=1920x1080&sorting=random" |
jsonfilter -qe "@.data[0].path"
;;
@@ -49,7 +61,7 @@ fetch_pic_url() {
local use_reso="resolutions"
[ "$EXACT_RESO" -eq "1" ] || use_reso="atleast"
[ -z "$API_KEY" ] || API_KEY="apikey=$API_KEY&"
wget -T3 -qO- \
fetch_url \
"https://wallhaven.cc/api/v1/search?${API_KEY}q=id%3A${tag_id}&${use_reso}=1920x1080&sorting=random" |
jsonfilter -qe '@.data[0].path'
;;
@@ -58,9 +70,9 @@ fetch_pic_url() {
try_update() {
local lock="$WRLOCK"
exec 200>"$lock"
exec 9>"$lock"
if flock -n 200 >"/dev/null" 2>&1; then
if flock -n 9 >"/dev/null" 2>&1; then
local picurl="$(fetch_pic_url)"
if [ -n "$picurl" ]; then
@@ -70,7 +82,7 @@ try_update() {
# if not, browser only displays a blank image
# NOTE: only happens to Wallhaven.cc source images
case "$WEB_PIC_SRC" in
wallhaven*) wget -T3 -qO- "${picurl}" > /dev/null ;;
wallhaven*) fetch_url "${picurl}" > /dev/null ;;
esac
echo "$picurl" | tee "$CACHE"
@@ -79,7 +91,7 @@ try_update() {
# and let the login page select a local background instead.
: > "$CACHE"
fi
flock -u 200 >"/dev/null" 2>&1
flock -u 9 >"/dev/null" 2>&1
elif [ -s "$CACHE" ]; then
cat "$CACHE"
fi
@@ -51,7 +51,8 @@ if op != 'get': p.write_text(json.dumps(state))
PKG_UPGRADE='1', PATH=str(binary) + os.pathsep + os.environ['PATH'])
script = (ROOT / 'root/etc/uci-defaults/30_luci-theme-argon').read_text()
self.script = self.root / 'initialize'
self.script.write_text(script.replace('/etc/', str(self.root / 'etc') + '/'))
self.script.write_text(script.replace('/etc/', str(self.root / 'etc') + '/')
.replace('/usr/lib/lua/luci/i18n', str(self.root / 'i18n')))
def initialize(self, state=None):
if state is not None:
@@ -128,6 +129,40 @@ if op != 'get': p.write_text(json.dumps(state))
check=True, capture_output=True)
self.assertFalse((self.root / 'etc/argon-config-migration').exists())
def theme_preinst(self):
recipe = (ROOT / 'Makefile').read_text()
source = re.search(r'define Package/luci-theme-argon/preinst\n(.*?)\nendef', recipe, re.S).group(1)
script = self.root / 'theme-preinst'
script.write_text(source.replace('$$', '$'))
subprocess.run(['sh', str(script)], env=dict(self.env, IPKG_INSTROOT=str(self.root)),
check=True, capture_output=True)
def test_bundled_translations_survive_an_older_theme_only_updater(self):
original = self.root / 'usr/lib/lua/luci/i18n/argon-config.zh-cn.lmo'
original.parent.mkdir(parents=True)
original.write_bytes(b'previous Chinese catalog')
self.theme_preinst()
# The previous package removes its formerly owned catalog on upgrade.
original.unlink()
self.initialize()
restored = self.root / 'i18n/argon-config.zh-cn.lmo'
self.assertEqual(restored.read_bytes(), b'previous Chinese catalog')
self.assertFalse((self.root / 'etc/argon-config-migration').exists())
self.initialize()
self.assertEqual(restored.read_bytes(), b'previous Chinese catalog')
def test_language_package_catalog_is_not_replaced_by_the_migration_backup(self):
original = self.root / 'usr/lib/lua/luci/i18n/argon-config.de.lmo'
original.parent.mkdir(parents=True)
original.write_bytes(b'old German catalog')
self.theme_preinst()
target = self.root / 'i18n/argon-config.de.lmo'
target.parent.mkdir()
target.write_bytes(b'new official language package')
self.initialize()
self.assertEqual(target.read_bytes(), b'new official language package')
self.assertFalse((self.root / 'etc/argon-config-migration').exists())
if __name__ == '__main__':
unittest.main()
+165 -15
View File
@@ -25,9 +25,31 @@ class UpdateRpcTests(unittest.TestCase):
fakebin.mkdir()
scripts = {
'apk': '''case "$1" in
list) test -n "$APK_VERSION" || exit 1; v=$APK_VERSION; [ ! -f "$INSTALLED" ] || v=$(cat "$INSTALLED"); test -n "$v" || exit 1; printf 'luci-theme-argon-%s noarch [installed]\\n' "$v" ;;
list)
case "$3" in
luci-i18n-argon-config-*)
if [ -f "$LANG_INSTALLED" ]; then
while read -r name version; do
[ "$3" = 'luci-i18n-argon-config-*' ] || [ "$3" = "$name" ] || continue
printf '%s-%s noarch [installed]\\n' "$name" "$version"
done < "$LANG_INSTALLED"
fi
exit 0 ;;
esac
test -n "$APK_VERSION" || exit 1; v=$APK_VERSION; [ ! -f "$INSTALLED" ] || v=$(cat "$INSTALLED"); test -n "$v" || exit 1; printf 'luci-theme-argon-%s noarch [installed]\\n' "$v" ;;
version) test "$COMPARE_FAIL" != 1 || exit 1; [ "$3" = "$4" ] && printf '=\\n' || { [ "$3" = 3.0.0-r20261015 ] && printf '>\\n' || printf '<\\n'; } ;;
add) printf '%s\\n' "$@" >> "$MUTATIONS"; test "$INSTALL_FAIL" != 1 || { echo 'database locked'; exit 1; }; test "$WRONG_VERSION" = 1 || printf '3.0.0-r2' > "$INSTALLED"; echo 'Theme installed' ;;
add) printf '%s\\n' "$@" >> "$MUTATIONS"; test "$INSTALL_FAIL" != 1 || { echo 'database locked'; exit 1; }; test "$WRONG_VERSION" = 1 || printf '3.0.0-r2' > "$INSTALLED"
for file in "$@"; do
case "$file" in */luci-i18n-argon-config-*.apk)
name=${file##*/}; name=${name%-3.0.0-r2.apk}
if [ -f "$LANG_INSTALLED" ]; then
awk -v name="$name" '$1 != name' "$LANG_INSTALLED" > "$LANG_INSTALLED.tmp"
mv "$LANG_INSTALLED.tmp" "$LANG_INSTALLED"
fi
test "$WRONG_LANGUAGE_VERSION" = 1 || printf '%s 3.0.0-r2\\n' "$name" >> "$LANG_INSTALLED" ;;
esac
done
echo 'Theme installed' ;;
esac
''',
'opkg': '''if [ "$OPKG_QUERY_GUARD" = 1 ]; then
@@ -36,9 +58,23 @@ esac
sleep .05
fi
case "$1" in
status) v=$OPKG_VERSION; [ ! -f "$INSTALLED" ] || v=$(cat "$INSTALLED"); test -n "$v" || exit 1; printf 'Package: luci-theme-argon\\nVersion: %s\\nStatus: install ok installed\\n' "$v" ;;
list-installed) [ ! -f "$LANG_INSTALLED" ] || awk '{ print $1 " - " $2 }' "$LANG_INSTALLED" ;;
status)
case "$2" in luci-i18n-argon-config-*)
v=$(awk -v name="$2" '$1 == name { v=$2 } END { print v }' "$LANG_INSTALLED" 2>/dev/null)
test -n "$v" || exit 0
printf 'Package: %s\\nVersion: %s\\nStatus: install ok installed\\n' "$2" "$v"; exit 0 ;;
esac
v=$OPKG_VERSION; [ ! -f "$INSTALLED" ] || v=$(cat "$INSTALLED"); test -n "$v" || exit 1; printf 'Package: luci-theme-argon\\nVersion: %s\\nStatus: install ok installed\\n' "$v" ;;
compare-versions) [ "$3" = "<<" ] && [ "$COMPARE_FAIL" != 1 ] && [ "$2" != "$4" ] && [ "$2" != 3.0.0-20261015 ] ;;
install) printf '%s\\n' "$@" >> "$MUTATIONS"; test "$INSTALL_FAIL" != 1 || { echo 'database locked'; exit 1; }; test "$WRONG_VERSION" = 1 || printf '3.0.0-2' > "$INSTALLED"; echo 'Theme installed' ;;
install) printf '%s\\n' "$@" >> "$MUTATIONS"; test "$INSTALL_FAIL" != 1 || { echo 'database locked'; exit 1; }; test "$WRONG_VERSION" = 1 || printf '3.0.0-2' > "$INSTALLED"
for file in "$@"; do
case "$file" in */luci-i18n-argon-config-*.ipk)
name=${file##*/}; name=${name%_v3.0.0-2_all.ipk}
test "$WRONG_LANGUAGE_VERSION" = 1 || printf '%s v3.0.0-2\\n' "$name" >> "$LANG_INSTALLED" ;;
esac
done
echo 'Theme installed' ;;
esac
''',
'df': 'printf "Filesystem 1024-blocks Used Available Capacity Mounted on\\nroot 100000 1000 %s 1%% /\\n" "${FREE_KB:-99000}"\n',
@@ -56,30 +92,45 @@ case "$destination" in
*primary.argon.test*) test "$PRIMARY_FAIL" != 1 || exit 8; cp "${PRIMARY_PAYLOAD:-$PAYLOAD}" "$destination" ;;
*) cp "$PAYLOAD" "$destination" ;;
esac ;;
*) sleep "${DOWNLOAD_DELAY:-0}"; test "$DOWNLOAD_FAIL" != 1 || exit 8; cp "$PACKAGE_DATA" "$destination" ;;
*) sleep "${DOWNLOAD_DELAY:-0}"; test "$DOWNLOAD_FAIL" != 1 || exit 8
case "$url" in
*luci-i18n-argon-config-*) test "$LANG_DOWNLOAD_FAIL" != 1 || exit 8; cp "$LANG_DATA" "$destination" ;;
*) cp "$PACKAGE_DATA" "$destination" ;;
esac ;;
esac
'''
''',
'uci': 'test -z "$LUCI_LANGUAGE" || printf "%s\\n" "$LUCI_LANGUAGE"\n'
}
for name, content in scripts.items():
path = fakebin / name
path.write_text('#!/bin/sh\n' + content)
path.chmod(0o755)
helper = self.directory / 'json_fixture.py'
helper.write_text('''import json,sys
helper.write_text('''import json,re,sys
root=json.load(sys.stdin); path=json.loads(sys.argv[1]); op=sys.argv[2]; args=sys.argv[3:]
obj=root
for key in path: obj=obj[int(key)] if isinstance(obj,list) else obj[key]
def alias(key): return re.sub(r'[^a-zA-Z0-9_]', '_', str(key))
def resolve(key):
if key in obj: return key
return next((name for name in reversed(list(obj)) if alias(name)==key), key)
if op=='select':
if args[0]=='..': path=path[:-1]
else:
child=obj[int(args[0])] if isinstance(obj,list) else obj[args[0]]
key=args[0] if isinstance(obj,list) else resolve(args[0])
child=obj[int(key)] if isinstance(obj,list) else obj[key]
if not isinstance(child,(dict,list)): raise ValueError('not a container')
path.append(args[0])
path.append(key)
print(json.dumps(path))
elif op=='keys': print(' '.join(map(str,range(len(obj)) if isinstance(obj,list) else obj.keys())))
elif op=='keys': print(' '.join(map(alias,range(len(obj)) if isinstance(obj,list) else obj.keys())))
elif op=='get':
value=obj.get(args[0],'')
value=obj.get(resolve(args[0]),'')
print(int(value) if isinstance(value,bool) else '' if value is None else value)
elif op=='type':
value=obj.get(resolve(args[0]))
print('object' if isinstance(value,dict) else 'array' if isinstance(value,list) else
'boolean' if isinstance(value,bool) else 'int' if isinstance(value,int) else
'string' if isinstance(value,str) else '')
elif op=='add':
obj[args[1]]=int(args[2]) if args[0]=='int' else bool(int(args[2])) if args[0]=='boolean' else {} if args[0]=='object' else args[2]
print(json.dumps(root))
@@ -92,6 +143,7 @@ json_load() {{ FIXTURE_JSON=$1; FIXTURE_PATH='[]'; fixture_json validate >/dev/n
json_load_file() {{ json_load "$(cat "$1")"; }}
json_init() {{ FIXTURE_JSON='{{}}'; FIXTURE_PATH='[]'; }}
json_get_var() {{ fixture_value=$(fixture_json get "$2"); export "$1=$fixture_value"; }}
json_get_type() {{ fixture_value=$(fixture_json type "$2"); export "$1=$fixture_value"; }}
json_get_keys() {{ fixture_value=$(fixture_json keys); export "$1=$fixture_value"; }}
json_select() {{ fixture_value=$(fixture_json select "$1" 2>/dev/null) || return 1; FIXTURE_PATH=$fixture_value; }}
json_add_string() {{ FIXTURE_JSON=$(fixture_json add string "$1" "$2"); }}
@@ -100,8 +152,14 @@ json_add_boolean() {{ FIXTURE_JSON=$(fixture_json add boolean "$1" "$2"); }}
json_add_object() {{ FIXTURE_JSON=$(fixture_json add object "$1" ''); json_select "$1"; }}
json_close_object() {{ json_select ..; }}
json_set_namespace() {{
if [ "$1" = upgrade_status ]; then FIXTURE_SAVED_JSON=$FIXTURE_JSON; FIXTURE_SAVED_PATH=$FIXTURE_PATH; export "$2=base";
else FIXTURE_JSON=$FIXTURE_SAVED_JSON; FIXTURE_PATH=$FIXTURE_SAVED_PATH; fi
[ "$#" -lt 2 ] || export "$2=${{FIXTURE_NAMESPACE:-base}}"
printf '%s' "$FIXTURE_JSON" > '{self.directory}/namespace-'"${{FIXTURE_NAMESPACE:-base}}"'.json'
printf '%s' "$FIXTURE_PATH" > '{self.directory}/namespace-'"${{FIXTURE_NAMESPACE:-base}}"'.path'
FIXTURE_NAMESPACE=${{1:-base}}
if [ -f '{self.directory}/namespace-'"$FIXTURE_NAMESPACE"'.json' ]; then
FIXTURE_JSON=$(cat '{self.directory}/namespace-'"$FIXTURE_NAMESPACE"'.json')
FIXTURE_PATH=$(cat '{self.directory}/namespace-'"$FIXTURE_NAMESPACE"'.path')
else json_init; fi
}}
json_dump() {{ printf '%s\\n' "$FIXTURE_JSON"; }}
@@ -109,16 +167,22 @@ json_dump() {{ printf '%s\\n' "$FIXTURE_JSON"; }}
self.script = self.directory / 'rpc.sh'
sources = self.directory / 'sources.json'
sources.write_text(json.dumps({'primary': PRIMARY, 'fallback': FALLBACK}))
self.catalogs = self.directory / 'catalogs'
self.catalogs.mkdir()
self.script.write_text((ROOT / 'root/usr/libexec/rpcd/luci.argon_update').read_text()
.replace('/usr/share/libubox/jshn.sh', str(shim))
.replace('/usr/share/argon/update-sources.json', str(sources))
.replace('/var/run/argon-update/release.json', str(self.cache))
.replace('/var/run/argon-update', str(self.directory / 'state')))
.replace('/var/run/argon-update', str(self.directory / 'state'))
.replace('/usr/lib/lua/luci/i18n', str(self.catalogs))
.replace('wget_bin=/usr/libexec/wget-ssl', 'wget_bin=' + str(fakebin / 'wget')))
self.env = dict(os.environ, PATH=str(fakebin) + os.pathsep + os.environ['PATH'],
APK_VERSION='3.0.0-r1', OPKG_VERSION='', PAYLOAD=str(self.payload),
REQUESTS=str(self.requests), FETCH_FAIL='0', INSTALLED=str(self.directory / 'installed'),
MUTATIONS=str(self.directory / 'mutations'), PACKAGE_DATA=str(self.directory / 'package'))
MUTATIONS=str(self.directory / 'mutations'), PACKAGE_DATA=str(self.directory / 'package'),
LANG_DATA=str(self.directory / 'language'), LANG_INSTALLED=str(self.directory / 'languages-installed'))
(self.directory / 'package').write_bytes(b'argon-package')
(self.directory / 'language').write_bytes(b'argon-language')
self.release = {'schema': 1, 'repository': 'jerrykuku/luci-theme-argon', 'tag': 'v3.0.0',
'published_at': '2026-10-16T01:00:00Z', 'notes': '## 更新\n- 修复布局。',
'ipk': 'luci-theme-argon_3.0.0-2_all.ipk', 'apk': 'luci-theme-argon-3.0.0-r2.apk',
@@ -297,6 +361,92 @@ json_dump() {{ printf '%s\\n' "$FIXTURE_JSON"; }}
time.sleep(.05)
self.fail('worker did not finish')
def add_languages(self, *languages):
digest = hashlib.sha256((self.directory / 'language').read_bytes()).hexdigest()
self.release['translations'] = {language: {
'apk': f'luci-i18n-argon-config-{language}-3.0.0-r2.apk',
'ipk': f'luci-i18n-argon-config-{language}_v3.0.0-2_all.ipk',
'apk_sha256': digest, 'ipk_sha256': digest,
'apk_size': 14, 'ipk_size': 14,
} for language in languages}
self.payload.write_text(json.dumps(self.release))
def test_bundled_chinese_is_installed_with_the_theme_for_both_managers(self):
self.add_languages('zh-cn', 'de')
(self.catalogs / 'argon-config.zh-cn.lmo').write_bytes(b'bundled Chinese')
for manager in ['apk', 'opkg']:
with self.subTest(manager=manager):
for file in ['installed', 'languages-installed', 'mutations', 'requests']:
(self.directory / file).unlink(missing_ok=True)
self.env.update(APK_VERSION='3.0.0-r1' if manager == 'apk' else '',
OPKG_VERSION='3.0.0-1' if manager == 'opkg' else '')
self.assertTrue(self.call('check')['can_upgrade'])
self.assertEqual(self.start()['result'], 0)
done = self.finished()
self.assertEqual(done['upgrade']['phase'], 'complete', done)
command = (self.directory / 'mutations').read_text()
self.assertIn('luci-i18n-argon-config-zh-cn', command)
self.assertNotIn('luci-i18n-argon-config-de', command)
self.assertLess(command.index('luci-theme-argon'), command.index('luci-i18n-argon-config-zh-cn'))
self.assertNotIn('force-', command)
def test_missing_verified_language_metadata_blocks_theme_only_installation(self):
(self.catalogs / 'argon-config.zh-cn.lmo').write_bytes(b'bundled Chinese')
self.assertFalse(self.call('check')['can_upgrade'])
self.assertEqual(self.start()['result'], 3)
self.assertFalse((self.directory / 'mutations').exists())
def test_configured_locale_is_added_when_available_and_missing_catalog_packages_are_repaired(self):
self.add_languages('zh-cn', 'de')
self.env['LUCI_LANGUAGE'] = 'zh_Hans'
(self.directory / 'languages-installed').write_text('luci-i18n-argon-config-de 3.0.0-r1\n')
self.assertTrue(self.call('check')['can_upgrade'])
self.assertEqual(self.start()['result'], 0)
self.assertEqual(self.finished()['upgrade']['phase'], 'complete')
command = (self.directory / 'mutations').read_text()
for language in ['zh-cn', 'de']:
self.assertIn('luci-i18n-argon-config-' + language, command)
def test_language_download_or_checksum_failure_never_mutates_the_theme(self):
self.add_languages('zh-cn')
(self.catalogs / 'argon-config.zh-cn.lmo').touch()
self.call('check')
for failure in ['network', 'checksum']:
with self.subTest(failure=failure):
self.env['LANG_DOWNLOAD_FAIL'] = '1' if failure == 'network' else '0'
if failure == 'checksum':
(self.directory / 'language').write_bytes(b'corrupt')
self.assertEqual(self.start()['result'], 0)
done = self.finished()
self.assertEqual(done['upgrade']['phase'], 'failed')
self.assertEqual(done['upgrade']['error'], failure, done)
self.assertFalse((self.directory / 'mutations').exists())
def test_language_version_is_verified_before_reporting_complete(self):
self.add_languages('zh-cn')
(self.catalogs / 'argon-config.zh-cn.lmo').touch()
self.env['WRONG_LANGUAGE_VERSION'] = '1'
self.call('check')
self.assertEqual(self.start()['result'], 0)
self.assertEqual(self.finished()['upgrade']['error'], 'install')
def test_translation_metadata_is_validated_and_only_contract_fields_are_cached(self):
self.add_languages('zh-cn')
original = self.release['translations']['zh-cn'].copy()
for bad in [[], {'../zh-cn': original}, {'zh-cn': dict(original, apk_size=0)},
{'zh-cn': dict(original, ipk_sha256='bad')},
{'zh-cn': dict(original, apk='luci-i18n-argon-config-zh-cn-4.0.0-r1.apk')}]:
with self.subTest(metadata=bad):
(self.directory / 'state/retry_at').unlink(missing_ok=True)
self.payload.write_text(json.dumps(dict(self.release, translations=bad)))
self.assertNotEqual(self.call('check')['result'], 0)
self.assertFalse(self.cache.exists())
(self.directory / 'state/retry_at').unlink(missing_ok=True)
self.payload.write_text(json.dumps(dict(self.release, translations={'zh-cn': dict(original, url='https://untrusted.invalid/')})))
result = self.call('check')
self.assertEqual(result['translations']['zh-cn'], original)
self.assertNotIn('untrusted.invalid', self.requests.read_text())
def test_background_install_for_both_managers_and_status_survives_reloads(self):
for manager in ['apk', 'opkg']:
with self.subTest(manager=manager):
@@ -43,6 +43,45 @@ class UpdateSiteTests(unittest.TestCase):
def generate(self, release=None):
return SITE.generate(release or self.release, self.packages, 'jerrykuku/luci-theme-argon', self.output)
def add_language(self, language='zh-cn'):
metadata = SITE.resolve(self.release['tag_name'])
names = [f'luci-i18n-argon-config-{language}_{metadata["ipk_i18n_version"]}_all.ipk',
f'luci-i18n-argon-config-{language}-{metadata["apk_i18n_version"]}.apk']
for name in names:
data = ('translation bytes for ' + name).encode()
(self.packages / name).write_bytes(data)
self.release['assets'].append({'name': name, 'size': len(data),
'digest': 'sha256:' + hashlib.sha256(data).hexdigest()})
return names
def test_language_packages_are_versioned_and_verified_in_both_formats(self):
names = self.add_language()
self.add_language('de')
manifest = self.generate()
self.assertEqual(set(manifest['translations']), {'zh-cn', 'de'})
for kind, name in zip(['ipk', 'apk'], names):
entry = manifest['translations']['zh-cn']
self.assertEqual(entry[kind], name)
self.assertEqual(entry[kind + '_sha256'], hashlib.sha256((self.packages / name).read_bytes()).hexdigest())
self.assertEqual(entry[kind + '_size'], (self.packages / name).stat().st_size)
def test_missing_corrupt_or_wrong_version_language_packages_block_publication(self):
for problem in ['missing-format', 'corrupt', 'wrong-version', 'duplicate']:
with self.subTest(problem=problem):
self.release = self.release_for('v3.1.0')
names = self.add_language()
if problem == 'missing-format':
self.release['assets'].pop()
elif problem == 'corrupt':
(self.packages / names[0]).write_bytes(b'corrupt translation')
elif problem == 'wrong-version':
self.release['assets'][-1]['name'] = names[1].replace('3.1.0', '3.0.0')
else:
self.release['assets'].append(self.release['assets'][-1])
with self.assertRaises(ValueError):
self.generate()
self.assertFalse(self.output.exists())
def test_manifest_contains_exact_revision_names_digests_sizes_and_notes(self):
for tag in ['v3.0.0', 'v3.1.0']:
release = self.release_for(tag)
@@ -294,8 +294,11 @@ jsonfilter() { return 1; }
try {
const script = path.join(dir, 'wallpaper.sh');
const cache = path.join(dir, 'wallpaper.url');
const downloader = path.join(dir, 'wget');
writeFileSync(downloader, '#!/bin/sh\nexit 1\n', { mode: 0o755 });
writeFileSync(cache, 'https://example.com/old.jpg\n');
writeFileSync(script, source.replace(imports, stub)
.replace('WGET=/usr/libexec/wget-ssl', `WGET="${downloader}"`)
.replace('CACHE="/var/run/argon_${WEB_PIC_SRC}.url"', 'CACHE="$TEST_CACHE"')
.replace('WRLOCK="/var/lock/argon_${WEB_PIC_SRC}.lock"', 'WRLOCK="$TEST_LOCK"'));
for (const wallpaper of ['unsplash', 'unsplash_123']) {
@@ -306,7 +309,7 @@ jsonfilter() { return 1; }
assert.deepEqual(JSON.parse(output), { url: '' });
}
rmSync(cache);
const failedFetch = execFileSync('bash', [script, 'call', 'get_url'], {
const failedFetch = execFileSync('sh', [script, 'call', 'get_url'], {
input: '{}\n', encoding: 'utf8',
env: { ...process.env, TEST_WALLPAPER: 'unsplash', TEST_KEY: 'test-key', TEST_CACHE: cache, TEST_LOCK: path.join(dir, 'lock') }
});
@@ -0,0 +1,113 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const test = require('node:test');
function fixture(t, budget = '6') {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'argon-wallpaper-'));
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
const bin = path.join(dir, 'bin');
fs.mkdirSync(bin);
const cache = path.join(dir, 'wallpaper.url');
const requests = path.join(dir, 'requests');
const ssl = path.join(bin, 'wget-ssl');
const downloader = `#!/bin/sh
printf '%s\\n' "$0 $*" >> "$REQUESTS"
[ "$FETCH_MODE" != failure ] || exit 1
[ "$FETCH_MODE" != hang ] || sleep 20
printf '%s' "$FETCH_PAYLOAD"
`;
for (const name of ['wget-ssl', 'uclient-fetch'])
fs.writeFileSync(path.join(bin, name), downloader, { mode: 0o755 });
fs.writeFileSync(path.join(bin, 'wget'), '#!/bin/sh\necho "HTTPS support not compiled in" >&2\nexit 1\n', { mode: 0o755 });
fs.writeFileSync(path.join(bin, 'jsonfilter'), `#!/usr/bin/env python3
import json,re,sys
try:
value=json.load(sys.stdin)
for key in re.findall(r'[a-zA-Z_]+|[0-9]+',sys.argv[-1]):
value=value[int(key)] if isinstance(value,list) else value[key]
print(value)
except (ValueError,KeyError,IndexError,TypeError):
sys.exit(1)
`, { mode: 0o755 });
const stub = `
uci() {
case "$*" in
*online_wallpaper) printf '%s\\n' "$WALLPAPER_SOURCE" ;;
*use_api_key) printf '%s\\n' fixture-key ;;
*use_exact_resolution) printf '1\\n' ;;
esac
}
json_init() { json_url=''; }
json_add_string() { json_url="$2"; }
json_dump() { printf '{"url":"%s"}\\n' "$json_url"; }
json_cleanup() { :; }
`;
const script = path.join(dir, 'rpc.sh');
const source = fs.readFileSync(path.join(__dirname, '../root/usr/libexec/rpcd/luci.argon_wallpaper'), 'utf8')
.replace('. /lib/functions.sh\n. /usr/share/libubox/jshn.sh', stub)
.replace('CACHE="/var/run/argon_${WEB_PIC_SRC}.url"', `CACHE="${cache}"`)
.replace('WRLOCK="/var/lock/argon_${WEB_PIC_SRC}.lock"', `WRLOCK="${dir}/lock"`)
.replace('WGET=/usr/libexec/wget-ssl', `WGET="${ssl}"`)
.replace('timeout -k 1 6', `timeout -k 1 ${budget}`);
fs.writeFileSync(script, source);
return {
cache, ssl,
requests: () => fs.existsSync(requests) ? fs.readFileSync(requests, 'utf8').trim().split('\n') : [],
call(source = 'bing', body = { images: [{ url: '/th?id=Fixture_1920x1080.jpg' }] }, mode = '') {
return JSON.parse(execFileSync('sh', ['-c', 'ulimit -n 64; exec sh "$@"', 'sh', script, 'call', 'get_url'], {
input: '{}\n', encoding: 'utf8', timeout: 3000,
env: { ...process.env, PATH: bin + path.delimiter + process.env.PATH,
REQUESTS: requests, WALLPAPER_SOURCE: source, FETCH_PAYLOAD: JSON.stringify(body), FETCH_MODE: mode }
}));
}
};
}
test('cold wallpaper requests use TLS despite a non-SSL wget, then reuse the cache', t => {
for (const [source, body, expected] of [
['bing', { images: [{ url: '/th?id=Fixture_1920x1080.jpg' }] }, 'https://www.bing.com/th?id=Fixture_UHD.jpg'],
['unsplash', { urls: { regular: 'https://images.unsplash.com/fixture.jpg' } }, 'https://images.unsplash.com/fixture.jpg'],
['wallhaven', { data: [{ path: 'https://w.wallhaven.cc/full/fixture.jpg' }] }, 'https://w.wallhaven.cc/full/fixture.jpg']
]) {
const f = fixture(t);
assert.deepEqual(f.call(source, body), { url: expected });
assert.equal(fs.readFileSync(f.cache, 'utf8'), expected + '\n');
const first = f.requests();
assert.ok(first.length > 0 && first.every(line => line.startsWith(f.ssl + ' ')));
assert.deepEqual(f.call(source, body, 'failure'), { url: expected });
assert.deepEqual(f.requests(), first);
}
});
test('source deployments use uclient-fetch when the standalone TLS binary is absent', t => {
const f = fixture(t);
fs.unlinkSync(f.ssl);
assert.equal(f.call().url, 'https://www.bing.com/th?id=Fixture_UHD.jpg');
assert.ok(f.requests().every(line => line.includes('/uclient-fetch ')));
});
test('failed wallpaper fetches return a local fallback and retry after the cooldown', t => {
const f = fixture(t);
assert.deepEqual(f.call('bing', {}, 'failure'), { url: '' });
assert.equal(fs.readFileSync(f.cache, 'utf8'), '');
assert.equal(f.requests().length, 1);
assert.deepEqual(f.call(), { url: '' });
assert.equal(f.requests().length, 1);
const expired = new Date(Date.now() - 122000);
fs.utimesSync(f.cache, expired, expired);
assert.equal(f.call().url, 'https://www.bing.com/th?id=Fixture_UHD.jpg');
assert.equal(f.requests().length, 2);
});
test('a stalled downloader cannot hold the login wallpaper RPC indefinitely', t => {
const f = fixture(t, '0.2');
const start = Date.now();
assert.deepEqual(f.call('bing', {}, 'hang'), { url: '' });
assert.ok(Date.now() - start < 2000);
assert.equal(fs.readFileSync(f.cache, 'utf8'), '');
});
+6 -1
View File
@@ -11,7 +11,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-vantage
PKG_VERSION:=1.0.15
PKG_RELEASE:=33
PKG_RELEASE:=34
PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage theme
@@ -34,6 +34,11 @@ LUCI_MINIFY_CSS:=0
LUCI_BASENAME:=vantage-theme
PKG_PO_VERSION:=$(PKG_VERSION)-r$(PKG_RELEASE)
# Appearance for every browser (Appearance under the colour-theme button)
define Package/luci-theme-vantage/conffiles
/etc/config/vantage_theme
endef
define Package/luci-theme-vantage/postrm
#!/bin/sh
# opkg (24.10) also runs the old package's postrm on an upgrade, as
@@ -30,6 +30,35 @@ const LOGIN_KEY = 'vantage.login'; /* read by the login page's login.js */
picked here when the dashboard is installed; the dashboard listens for BACKDROP_EVENT */
const BACKDROP_KEY = 'vantage.backdrop.v1';
const BACKDROP_EVENT = 'vantage:backdrop';
/* Appearance for every browser: saved on the router, in
/etc/config/vantage_theme (section appearance), by whoever may (rpcd
group luci-theme-vantage); header.ut and sysauth.ut hand its values over
as data-vt-look-<name>, and data-vt-look-save says whether this login
may save: 'yes', 'relogin' (it would at a new login: one from before
the group existed) or 'no'. A pick made without that right stays in
this browser (the own key) and wins here, but not for a login that may
save (those keys are dropped then); this browser's last pick (the old
key, also what the dashboard reads under another theme) counts until
the router has one. */
const LOOK = {
theme: { own: 'vantage.theme.own', before: THEME_KEY, ok: [ 'auto', 'light', 'dark' ] },
login: { own: 'vantage.login.own', before: LOGIN_KEY, ok: [ 'grid', 'stars', 'none' ] },
backdrop: { own: 'vantage.backdrop.own', before: BACKDROP_KEY, ok: [ 'grid', 'stars', 'none' ] }
};
const LOOK_SYNC = 'vantage.look'; /* another tab saved one: { name, value } */
const LOOK_SET_EVENT = 'vantage:backdrop-set'; /* the dashboard's own picker hands its pick over */
/* The dashboard's Customize saves its chart style and layouts with the
Appearance, in one write when it is done: { values, done(ok) }. The
template reads them back with these same rules. */
const VIEW_SAVE_EVENT = 'vantage:view-save';
const VIEW_CHARTS = [ 'overlay', 'log', 'mirror', 'mirror-split', 'lanes' ];
const VIEW_LAYOUT = /^(default|[a-z]{1,8}:[0-3]:[1-4](:h)?( [a-z]{1,8}:[0-3]:[1-4](:h)?){0,7})$/;
const callLookSet = rpc.declare({ object: 'uci', method: 'set', params: [ 'config', 'section', 'values' ], reject: true });
const callLookCommit = rpc.declare({ object: 'uci', method: 'commit', params: [ 'config' ], reject: true });
/* While an apply awaits its rollback, rpcd refuses commit and revert alike, but not set: a pick set
then could not be taken back, and would go out unseen with the next Save & Apply. So a save starts
with this revert, which has nothing to undo and is refused in just that case. */
const callLookRevert = rpc.declare({ object: 'uci', method: 'revert', params: [ 'config' ], reject: true });
/* 24px stroke icons, drawn for this theme: constant [tag, attributes] lists,
built with createElementNS (no markup parsing, no user data) */
@@ -124,6 +153,30 @@ function svg(name, size) {
}
function storageGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
/* the value in force, from [ pending, own, router, before ]: the first
the theme knows, else the first choice (the default); the templates'
inline scripts and login.js do the same without the pending one */
function lookValue(ok, list) {
for (const v of list) if (ok.indexOf(v) >= 0) return v;
return ok[0];
}
function lookWrite(values) {
return callLookRevert('vantage_theme')
.then(() => callLookSet('vantage_theme', 'appearance', values))
.then(() => callLookCommit('vantage_theme').catch(e => callLookRevert('vantage_theme').then(() => { throw e; }, () => { throw e; })));
}
function viewValue(name, value) {
if (name === 'chart') return VIEW_CHARTS.indexOf(value) >= 0;
return /^layout_(wide|mid|narrow)(_wired)?$/.test(name) && typeof value === 'string' && VIEW_LAYOUT.test(value);
}
/* the router's value of one, as the templates set it (literal names) */
function setLookAttr(root, name, value) {
if (name === 'theme') root.setAttribute('data-vt-look-theme', value);
else if (name === 'login') root.setAttribute('data-vt-look-login', value);
else if (name === 'backdrop') root.setAttribute('data-vt-look-backdrop', value);
}
function storageSet(k, v) { try { if (v == null) localStorage.removeItem(k); else localStorage.setItem(k, v); } catch (e) {} }
function isTyping(el) {
@@ -775,7 +828,7 @@ return baseclass.extend({
/* Appearance opened before the menu came in lacks the network
path's group (hasDashboard): add it now */
if (this.appear && this.hasDashboard() && !this.appear.querySelector('input[name="vt-backdrop"]')) {
this.appear.appendChild(this.backdropGroup());
this.appear.insertBefore(this.backdropGroup(), this.appearNote);
this.syncAppearance();
}
this.dp = (L.env.dispatchpath || []).slice();
@@ -1423,14 +1476,56 @@ return baseclass.extend({
/* -------------------------------------------------------- theme */
/* may this login save Appearance for every browser (header.ut): yes, relogin or no */
lookState() {
const v = this.root.getAttribute('data-vt-look-save');
return v === 'yes' || v === 'relogin' ? v : 'no';
},
/* one Appearance setting (LOOK), the value in force */
lookPref(name) {
const k = LOOK[name], mine = this.lookState() === 'yes' ? null : storageGet(k.own);
return lookValue(k.ok, [ (this.lookPending || {})[name], mine, this.root.getAttribute('data-vt-look-' + name), storageGet(k.before) ]);
},
/* Save one for every browser where this login may, else for this
browser. Saves queue in order and only the latest pick of each is
written; `apply` shows it at once and again when done. A save that
fails leaves the pick shown on this page, with a note. */
setLook(name, value, apply) {
const k = LOOK[name];
const pending = this.lookPending || (this.lookPending = {});
if (k.ok.indexOf(value) < 0 || pending[name] === value) return;
if (this.lookState() !== 'yes') {
storageSet(k.own, value);
storageSet(k.before, value);
apply();
return;
}
const seqs = this.lookSeqs || (this.lookSeqs = {}), seq = seqs[name] = (seqs[name] || 0) + 1;
pending[name] = value;
this.lookFailed = false;
apply();
const latest = () => seqs[name] === seq;
this.lookChain = (this.lookChain || Promise.resolve()).then(() => {
if (!latest()) return;
return lookWrite({ [name]: value })
.then(() => {
/* the router's value first, so other tabs never see a gap */
setLookAttr(this.root, name, value);
storageSet(LOOK_SYNC, JSON.stringify({ name: name, value: value, at: Date.now() }));
storageSet(k.before, value);
if (latest()) delete pending[name];
}, () => { if (latest()) this.lookFailed = true; });
}).then(apply);
},
themePref() {
const p = storageGet(THEME_KEY);
return (p === 'light' || p === 'dark') ? p : 'auto';
return this.lookPref('theme');
},
setTheme(pref) {
storageSet(THEME_KEY, pref === 'auto' ? null : pref);
this.applyTheme();
this.setLook('theme', pref, () => this.applyTheme());
},
applyTheme() {
@@ -1450,8 +1545,7 @@ return baseclass.extend({
/* the login page's backdrop (login.js): grid packets unless set */
loginPref() {
const p = storageGet(LOGIN_KEY);
return (p === 'stars' || p === 'none') ? p : 'grid';
return this.lookPref('login');
},
loginNames() {
@@ -1459,8 +1553,7 @@ return baseclass.extend({
},
setLoginPref(pref) {
storageSet(LOGIN_KEY, pref === 'grid' ? null : pref);
this.syncAppearance();
this.setLook('login', pref, () => this.syncAppearance());
},
/* Vantage's dashboard, where this user may open it: its network path
@@ -1470,13 +1563,20 @@ return baseclass.extend({
},
backdropPref() {
const p = storageGet(BACKDROP_KEY);
return (p === 'stars' || p === 'none') ? p : 'grid';
return this.lookPref('backdrop');
},
setBackdropPref(pref) {
storageSet(BACKDROP_KEY, pref === 'grid' ? null : pref);
window.dispatchEvent(new CustomEvent(BACKDROP_EVENT));
this.setLook('backdrop', pref, () => this.applyBackdrop());
},
/* the dashboard reads the one in force from data-vt-backdrop */
applyBackdrop() {
const v = this.backdropPref();
if (this.root.getAttribute('data-vt-backdrop') !== v) {
this.root.setAttribute('data-vt-backdrop', v);
window.dispatchEvent(new CustomEvent(BACKDROP_EVENT));
}
this.syncAppearance();
},
@@ -1495,11 +1595,42 @@ return baseclass.extend({
}
const mq = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
if (mq && mq.addEventListener) mq.addEventListener('change', () => { if (this.themePref() === 'auto') this.applyTheme(); });
/* a login that may save: this browser's private picks no longer count */
if (this.lookState() === 'yes') Object.keys(LOOK).forEach(n => storageSet(LOOK[n].own, null));
/* another tab of this browser changed one, or saved one on the router */
const keys = [ LOOK_SYNC ];
Object.keys(LOOK).forEach(n => keys.push(LOOK[n].own, LOOK[n].before));
window.addEventListener('storage', ev => {
if (ev.key === THEME_KEY) this.applyTheme();
else if (ev.key === LOGIN_KEY || ev.key === BACKDROP_KEY) this.syncAppearance();
if (keys.indexOf(ev.key) < 0) return;
if (ev.key === LOOK_SYNC) {
let m = null;
try { m = JSON.parse(ev.newValue); } catch (e) {}
if (m && Object.prototype.hasOwnProperty.call(LOOK, m.name) && LOOK[m.name].ok.indexOf(m.value) >= 0)
setLookAttr(this.root, m.name, m.value);
}
this.applyTheme();
this.applyBackdrop();
});
/* the dashboard's own picker (Customize), saved as Appearance's */
window.addEventListener(LOOK_SET_EVENT, ev => {
if (ev.detail && LOOK.backdrop.ok.indexOf(ev.detail) >= 0) {
ev.preventDefault();
this.setBackdropPref(ev.detail);
}
});
/* Customize's Done, for a login that may save; for any other it
stays in that browser, which the dashboard says */
window.addEventListener(VIEW_SAVE_EVENT, ev => {
const d = ev.detail, values = {};
if (this.lookState() !== 'yes' || !d || !d.values || typeof d.done !== 'function') return;
Object.keys(d.values).forEach(n => { if (viewValue(n, d.values[n])) values[n] = d.values[n]; });
if (!Object.keys(values).length) return;
ev.preventDefault();
this.lookChain = (this.lookChain || Promise.resolve()).then(() => lookWrite(values))
.then(() => d.done(true), () => d.done(false)).catch(() => {});
});
this.applyTheme();
this.applyBackdrop();
},
/* Appearance, under the colour-theme button: the colour theme, the
@@ -1535,7 +1666,10 @@ return baseclass.extend({
[ 'auto', _('Auto', 'vantage-theme'), 'auto' ], [ 'light', _('Light', 'vantage-theme'), 'sun' ], [ 'dark', _('Dark', 'vantage-theme'), 'moon' ]
], v => this.setTheme(v)),
group('vt-login', _('Login background'), [ [ 'grid', login.grid ], [ 'stars', login.stars ], [ 'none', login.none ] ], v => this.setLoginPref(v))
].concat(this.hasDashboard() ? [ this.backdropGroup() ] : []));
].concat(this.hasDashboard() ? [ this.backdropGroup() ] : [], [
/* where the picks are kept, when not for every browser (syncAppearance) */
(this.appearNote = E('p', { 'class': 'vt-appear-note', 'role': 'status', 'hidden': 'hidden' }))
]));
box.addEventListener('keydown', ev => {
if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); this.closeAppearance(true); }
else if (ev.key === 'Tab') {
@@ -1561,6 +1695,14 @@ return baseclass.extend({
if (!this.appear) return;
const now = { 'vt-colour': this.themePref(), 'vt-login': this.loginPref(), 'vt-backdrop': this.backdropPref() };
Array.prototype.forEach.call(this.appear.querySelectorAll('input[type="radio"]'), r => { r.checked = (now[r.name] === r.value); });
const state = this.lookState();
const note = state === 'relogin' ? _('Log in again to save Appearance for every browser.')
: state !== 'yes' ? _('Saved in this browser only.')
: this.lookFailed ? _('Could not save this for every browser. Try again.') : '';
if (this.appearNote) {
if (this.appearNote.textContent !== note) this.appearNote.textContent = note;
this.appearNote.hidden = !note;
}
},
openAppearance(btn) {
@@ -1,16 +1,19 @@
/*
* Vantage theme - login page backdrop, over the page grid: packets running
* along the grid lines (the default) or a starfield flown through, as
* picked under Appearance (localStorage vantage.login, per browser: unset,
* "stars" or "none"). One canvas, no libraries: a frame is a handful of
* batched draws, at most about 60 a second, and nothing runs while the
* picked under Appearance: saved for every browser on the router
* (sysauth.ut's data-vt-look-login), else in this browser (localStorage
* vantage.login.own for one who may not save it for everyone, vantage.login
* from before; menu-vantage.js lookValue). One canvas, no libraries: a
* frame is a handful of batched draws, at most about 60 a second, and nothing runs while the
* window is in the background. With reduced motion it draws one still
* frame. Logging in speeds it up while the form posts.
*/
(function () {
'use strict';
var pick = null;
try { pick = localStorage.getItem('vantage.login'); } catch (e) {}
var v = [ null, document.documentElement.getAttribute('data-vt-look-login'), null ], ok = [ 'grid', 'stars', 'none' ], pick = 'grid', i;
try { v[0] = localStorage.getItem('vantage.login.own'); v[2] = localStorage.getItem('vantage.login'); } catch (e) {}
for (i = 2; i >= 0; i--) if (ok.indexOf(v[i]) >= 0) pick = v[i];
if (pick === 'none') return;
var cv = document.createElement('canvas'), ctx = cv.getContext && cv.getContext('2d');
if (!ctx || typeof Path2D !== 'function') return;
@@ -623,6 +623,9 @@ a.vt-crumb:hover { color: var(--text); }
}
.vt-appearance[hidden] { display: none; }
.vt-appear-set { min-width: 0; margin: 0; padding: 0; border: 0; }
/* where the picks are kept, when not for every browser */
.vt-appear-note { max-width: 32ch; margin: 0; color: var(--text-3); font-size: var(--fs-sm); line-height: 1.4; }
.vt-appear-note[hidden] { display: none; }
.vt-appear-set > legend { margin: 0 0 6px; padding: 0; color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }
.vt-appear-track { display: flex; background: var(--seg-bg); border-radius: var(--r-lg); box-shadow: inset 0 0 0 1px var(--line); }
.vt-appear-opt {
+106 -87
View File
@@ -13,7 +13,7 @@ msgid_plural "%d more chains at this hook"
msgstr[0] ""
msgstr[1] ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1350
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1403
msgctxt "vantage-theme"
msgid "%d result"
msgid_plural "%d results"
@@ -56,7 +56,7 @@ msgctxt "vantage-theme"
msgid "Accept"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1270
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1323
msgid "Actions"
msgstr ""
@@ -65,22 +65,22 @@ msgctxt "nft match any traffic"
msgid "Any packet"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1258
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1259
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1260
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1263
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1267
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1533
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:84
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1664
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:132
msgctxt "vantage-theme"
msgid "Appearance"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:41
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:48
msgid "Authorization Required"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1535
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
msgctxt "vantage-theme"
msgid "Auto"
msgstr ""
@@ -95,7 +95,7 @@ msgctxt "vantage-theme"
msgid "Back to the calling chain"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:72
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:120
msgctxt "vantage-theme"
msgid "Breadcrumb"
msgstr ""
@@ -113,17 +113,17 @@ msgctxt "vantage-theme"
msgid "Bridged between ports"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:409
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462
msgid "Buffered"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:420
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:430
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:434
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
msgid "Cache"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:409
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462
msgid "Cached"
msgstr ""
@@ -142,19 +142,19 @@ msgctxt "vantage-theme"
msgid "Chains no rule jumps to directly"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:491
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:544
msgid "Channel"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:897
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
msgid "Collapse navigation"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1534
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1665
msgid "Colour theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1294
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1347
msgid "Command palette"
msgstr ""
@@ -173,7 +173,11 @@ msgctxt "vantage-theme"
msgid "Continues in %s, then comes back here"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1535
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1701
msgid "Could not save this for every browser. Try again."
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
msgctxt "vantage-theme"
msgid "Dark"
msgstr ""
@@ -197,8 +201,8 @@ msgstr ""
msgid "Egress"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:897
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:57
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:105
msgid "Expand navigation"
msgstr ""
@@ -215,7 +219,7 @@ msgid ""
"their counters see only its first packets."
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1260
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313
msgid "Follow system theme"
msgstr ""
@@ -233,17 +237,17 @@ msgid ""
"if br_netfilter is enabled; the ruleset doesn't show that.)"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:421
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:435
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:488
msgctxt "vantage-theme"
msgid "Free"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1458
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
msgid "Grid packets"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:52
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:100
msgctxt "vantage-theme"
msgid "Home"
msgstr ""
@@ -260,9 +264,9 @@ msgstr ""
msgid "IPv6 traffic table \"%h\""
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:420
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:429
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:433
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:482
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:486
msgctxt "vantage-theme"
msgid "In use"
msgstr ""
@@ -276,11 +280,11 @@ msgctxt "vantage-theme"
msgid "Input"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:45
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:52
msgid "Invalid username and/or password! Please try again."
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1290
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343
msgid "Jump to a page or action…"
msgstr ""
@@ -289,27 +293,31 @@ msgctxt "vantage-theme"
msgid "Leaves unmatched unchanged"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1535
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
msgctxt "vantage-theme"
msgid "Light"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:23
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:57
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:28
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:64
msgid "Log in"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1269
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:87
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1699
msgid "Log in again to save Appearance for every browser."
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:135
msgid "Log out"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1263
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1537
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1668
msgid "Login background"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:51
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:99
msgctxt "vantage-theme"
msgid "Main navigation"
msgstr ""
@@ -319,13 +327,13 @@ msgctxt "vantage-theme"
msgid "Masquerade"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:404
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:419
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:457
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:472
msgid "Memory"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1806
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1823
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1948
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1965
msgctxt "vantage-theme"
msgid "More actions"
msgstr ""
@@ -343,8 +351,8 @@ msgstr ""
msgid "Network device table \"%h\""
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1267
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1526
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1657
msgid "Network path background"
msgstr ""
@@ -353,7 +361,7 @@ msgctxt "vantage-theme"
msgid "Next rule"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1348
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1401
msgid "No matching pages"
msgstr ""
@@ -367,17 +375,17 @@ msgctxt "vantage-theme"
msgid "No rules: %s"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1458
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
msgctxt "vantage-theme"
msgid "None"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1165
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1218
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:17
msgid "Not found"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1185
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1238
msgid "Other pages in %s"
msgstr ""
@@ -391,7 +399,7 @@ msgctxt "vantage-theme"
msgid "Packet path"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:93
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:141
msgid "Page tabs"
msgstr ""
@@ -405,7 +413,7 @@ msgctxt "vantage-theme"
msgid "Passes unmatched"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:54
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:61
msgid "Password"
msgstr ""
@@ -434,7 +442,7 @@ msgctxt "vantage-theme"
msgid "Reject"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1292
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1345
msgctxt "vantage-theme"
msgid "Results"
msgstr ""
@@ -454,13 +462,17 @@ msgctxt "vantage-theme"
msgid "Rules table"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1290
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:75
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:77
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1700
msgid "Saved in this browser only."
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:123
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:125
msgid "Search pages"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1269
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322
msgid "Session"
msgstr ""
@@ -471,33 +483,33 @@ msgid_plural "Show %d more rules"
msgstr[0] ""
msgstr[1] ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1963
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2105
msgid "Show full description"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1952
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2094
msgid "Show full help: %s"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:48
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:96
msgid "Skip to content"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1458
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
msgid "Starfield"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:456
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:509
msgid "Storage"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:442
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:444
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:495
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:497
msgctxt "vantage-theme"
msgid "Swap"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:409
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462
msgid "Swap free"
msgstr ""
@@ -506,12 +518,12 @@ msgctxt "vantage-theme"
msgid "This device"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:421
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474
msgctxt "vantage-theme"
msgid "Total"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:408
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:461
msgid "Total Available"
msgstr ""
@@ -525,22 +537,29 @@ msgctxt "vantage-theme"
msgid "Unchanged"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1258
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311
msgid "Use dark theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1259
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312
msgid "Use light theme"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:408
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:461
msgid "Used"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:50
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:57
msgid "Username"
msgstr ""
#: luci-theme-vantage/root/usr/share/rpcd/acl.d/luci-theme-vantage.json:3
msgid ""
"Vantage theme: save its Appearance (colour mode, login and network path "
"backgrounds) and the dashboard's chart style and layouts for every browser, "
"in /etc/config/vantage_theme only"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:454
msgctxt "vantage-theme"
msgid "Verdict map %s"
@@ -551,16 +570,16 @@ msgctxt "vantage-theme"
msgid "View"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:484
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:537
msgid "Wireless"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:425
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:478
msgctxt "vantage-theme"
msgid "available"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1300
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1353
msgid "close"
msgstr ""
@@ -574,12 +593,12 @@ msgctxt "vantage-theme"
msgid "drops unmatched %s"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:471
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:475
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "free"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1298
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1351
msgid "navigate"
msgstr ""
@@ -598,7 +617,7 @@ msgctxt "vantage-theme"
msgid "on %s"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1299
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1352
msgctxt "vantage-theme"
msgid "open"
msgstr ""
@@ -608,9 +627,9 @@ msgctxt "vantage-theme"
msgid "priority %d"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:420
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:430
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:434
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
msgid "reclaimable"
msgstr ""
@@ -619,12 +638,12 @@ msgctxt "vantage-theme"
msgid "rules drop some"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:471
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:475
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "total"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:471
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:475
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "used"
msgstr ""
+113 -87
View File
@@ -21,7 +21,7 @@ msgid "%d more chain at this hook"
msgid_plural "%d more chains at this hook"
msgstr[0] "此钩子还有 %d 条链"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1350
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1403
msgctxt "vantage-theme"
msgid "%d result"
msgid_plural "%d results"
@@ -62,7 +62,7 @@ msgctxt "vantage-theme"
msgid "Accept"
msgstr "接受"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1270
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1323
msgid "Actions"
msgstr "操作"
@@ -71,22 +71,22 @@ msgctxt "nft match any traffic"
msgid "Any packet"
msgstr "任意数据包"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1258
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1259
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1260
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1263
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1267
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1533
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:84
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1664
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:132
msgctxt "vantage-theme"
msgid "Appearance"
msgstr "外观"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:41
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:48
msgid "Authorization Required"
msgstr "需要授权"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1535
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
msgctxt "vantage-theme"
msgid "Auto"
msgstr "自动"
@@ -101,7 +101,7 @@ msgctxt "vantage-theme"
msgid "Back to the calling chain"
msgstr "返回调用链"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:72
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:120
msgctxt "vantage-theme"
msgid "Breadcrumb"
msgstr "面包屑"
@@ -119,17 +119,17 @@ msgctxt "vantage-theme"
msgid "Bridged between ports"
msgstr "端口间桥接"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:409
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462
msgid "Buffered"
msgstr "已缓冲"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:420
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:430
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:434
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
msgid "Cache"
msgstr "缓存"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:409
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462
msgid "Cached"
msgstr "已缓存"
@@ -148,19 +148,19 @@ msgctxt "vantage-theme"
msgid "Chains no rule jumps to directly"
msgstr "没有规则直接跳转到的链"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:491
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:544
msgid "Channel"
msgstr "信道"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:897
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
msgid "Collapse navigation"
msgstr "收起导航"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1534
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1665
msgid "Colour theme"
msgstr "颜色主题"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1294
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1347
msgid "Command palette"
msgstr "命令面板"
@@ -179,7 +179,11 @@ msgctxt "vantage-theme"
msgid "Continues in %s, then comes back here"
msgstr "转入 %s,随后返回此处"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1535
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1701
msgid "Could not save this for every browser. Try again."
msgstr "无法为所有浏览器保存此设置,请重试。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
msgctxt "vantage-theme"
msgid "Dark"
msgstr "深色"
@@ -203,8 +207,8 @@ msgstr "丢弃未匹配流量"
msgid "Egress"
msgstr "出口"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:897
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:57
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:105
msgid "Expand navigation"
msgstr "展开导航"
@@ -223,7 +227,7 @@ msgstr ""
"流量卸载已开启:一条转发连接建立后,它的数据包可以跳过 prerouting、forward "
"和 postrouting,因此这些链和它们的计数器只会看到该连接最初的数据包。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1260
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313
msgid "Follow system theme"
msgstr "跟随系统主题"
@@ -244,17 +248,17 @@ msgstr ""
"达 input、forward 或 output 链,这里也没有桥接链会过滤它们。(启用 "
"br_netfilter 时内核可以把它们交给 IP 链;规则集不体现这一点。)"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:421
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:435
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:488
msgctxt "vantage-theme"
msgid "Free"
msgstr "空闲"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1458
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
msgid "Grid packets"
msgstr "网格数据包"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:52
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:100
msgctxt "vantage-theme"
msgid "Home"
msgstr "首页"
@@ -271,9 +275,9 @@ msgstr "IPv4/IPv6 流量表 \"%h\""
msgid "IPv6 traffic table \"%h\""
msgstr "IPv6 流量表 \"%h\""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:420
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:429
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:433
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:482
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:486
msgctxt "vantage-theme"
msgid "In use"
msgstr "已用"
@@ -287,11 +291,11 @@ msgctxt "vantage-theme"
msgid "Input"
msgstr "输入"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:45
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:52
msgid "Invalid username and/or password! Please try again."
msgstr "用户名或密码错误,请重试。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1290
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343
msgid "Jump to a page or action…"
msgstr "跳转到页面或操作…"
@@ -300,27 +304,31 @@ msgctxt "vantage-theme"
msgid "Leaves unmatched unchanged"
msgstr "未匹配流量保持原样"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1535
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
msgctxt "vantage-theme"
msgid "Light"
msgstr "浅色"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:23
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:57
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:28
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:64
msgid "Log in"
msgstr "登录"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1269
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:87
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1699
msgid "Log in again to save Appearance for every browser."
msgstr "重新登录后即可为所有浏览器保存外观设置。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:135
msgid "Log out"
msgstr "退出登录"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1263
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1537
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1668
msgid "Login background"
msgstr "登录背景"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:51
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:99
msgctxt "vantage-theme"
msgid "Main navigation"
msgstr "主导航"
@@ -330,13 +338,13 @@ msgctxt "vantage-theme"
msgid "Masquerade"
msgstr "伪装(NAT)"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:404
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:419
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:457
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:472
msgid "Memory"
msgstr "内存"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1806
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1823
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1948
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1965
msgctxt "vantage-theme"
msgid "More actions"
msgstr "更多操作"
@@ -354,8 +362,8 @@ msgstr "网络"
msgid "Network device table \"%h\""
msgstr "网络设备表 \"%h\""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1267
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1526
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1657
msgid "Network path background"
msgstr "网络路径背景"
@@ -364,7 +372,7 @@ msgctxt "vantage-theme"
msgid "Next rule"
msgstr "下一条规则"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1348
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1401
msgid "No matching pages"
msgstr "没有匹配的页面"
@@ -378,17 +386,17 @@ msgctxt "vantage-theme"
msgid "No rules: %s"
msgstr "无规则:%s"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1458
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
msgctxt "vantage-theme"
msgid "None"
msgstr "无"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1165
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1218
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:17
msgid "Not found"
msgstr "未找到"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1185
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1238
msgid "Other pages in %s"
msgstr "%s 中的其他页面"
@@ -402,7 +410,7 @@ msgctxt "vantage-theme"
msgid "Packet path"
msgstr "数据包路径"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:93
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:141
msgid "Page tabs"
msgstr "页面标签页"
@@ -416,7 +424,7 @@ msgctxt "vantage-theme"
msgid "Passes unmatched"
msgstr "放行未匹配流量"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:54
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:61
msgid "Password"
msgstr "密码"
@@ -445,7 +453,7 @@ msgctxt "vantage-theme"
msgid "Reject"
msgstr "拒绝"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1292
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1345
msgctxt "vantage-theme"
msgid "Results"
msgstr "结果"
@@ -465,13 +473,17 @@ msgctxt "vantage-theme"
msgid "Rules table"
msgstr "规则表"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1290
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:75
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:77
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1700
msgid "Saved in this browser only."
msgstr "仅保存在此浏览器中。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:123
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:125
msgid "Search pages"
msgstr "搜索页面"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1269
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322
msgid "Session"
msgstr "会话"
@@ -481,33 +493,33 @@ msgid "Show %d more rule"
msgid_plural "Show %d more rules"
msgstr[0] "再显示 %d 条规则"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1963
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2105
msgid "Show full description"
msgstr "显示完整描述"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1952
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2094
msgid "Show full help: %s"
msgstr "显示完整帮助:%s"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:48
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:96
msgid "Skip to content"
msgstr "跳至正文"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1458
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
msgid "Starfield"
msgstr "星空"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:456
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:509
msgid "Storage"
msgstr "存储"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:442
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:444
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:495
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:497
msgctxt "vantage-theme"
msgid "Swap"
msgstr "交换空间"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:409
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462
msgid "Swap free"
msgstr "空闲交换空间"
@@ -516,12 +528,12 @@ msgctxt "vantage-theme"
msgid "This device"
msgstr "本设备"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:421
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474
msgctxt "vantage-theme"
msgid "Total"
msgstr "总计"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:408
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:461
msgid "Total Available"
msgstr "总可用"
@@ -535,22 +547,29 @@ msgctxt "vantage-theme"
msgid "Unchanged"
msgstr "不变"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1258
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311
msgid "Use dark theme"
msgstr "使用深色主题"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1259
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312
msgid "Use light theme"
msgstr "使用浅色主题"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:408
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:461
msgid "Used"
msgstr "已用"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:50
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:57
msgid "Username"
msgstr "用户名"
#: luci-theme-vantage/root/usr/share/rpcd/acl.d/luci-theme-vantage.json:3
msgid ""
"Vantage theme: save its Appearance (colour mode, login and network path "
"backgrounds) and the dashboard's chart style and layouts for every browser, "
"in /etc/config/vantage_theme only"
msgstr "Vantage 主题:为所有浏览器保存其外观设置(颜色模式、登录页与网络路径背景)以及仪表盘的图表样式和布局,仅写入 /etc/config/vantage_theme"
#: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:454
msgctxt "vantage-theme"
msgid "Verdict map %s"
@@ -561,16 +580,16 @@ msgctxt "vantage-theme"
msgid "View"
msgstr "视图"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:484
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:537
msgid "Wireless"
msgstr "无线"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:425
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:478
msgctxt "vantage-theme"
msgid "available"
msgstr "可用"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1300
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1353
msgid "close"
msgstr "关闭"
@@ -584,12 +603,12 @@ msgctxt "vantage-theme"
msgid "drops unmatched %s"
msgstr "丢弃未匹配的 %s"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:471
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:475
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "free"
msgstr "空闲"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1298
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1351
msgid "navigate"
msgstr "移动"
@@ -608,7 +627,7 @@ msgctxt "vantage-theme"
msgid "on %s"
msgstr "接口 %s"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1299
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1352
msgctxt "vantage-theme"
msgid "open"
msgstr "打开"
@@ -618,9 +637,9 @@ msgctxt "vantage-theme"
msgid "priority %d"
msgstr "优先级 %d"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:420
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:430
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:434
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
msgid "reclaimable"
msgstr "可回收"
@@ -629,16 +648,23 @@ msgctxt "vantage-theme"
msgid "rules drop some"
msgstr "部分规则会丢弃"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:471
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:475
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "total"
msgstr "总计"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:471
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:475
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "used"
msgstr "已用"
#~ msgid ""
#~ "Vantage theme: save its Appearance (colour mode, login and network path "
#~ "backgrounds) for every browser, in /etc/config/vantage_theme only"
#~ msgstr ""
#~ "Vantage 主题:为所有浏览器保存其外观设置(颜色模式、登录页与网络路径背"
#~ "景),仅写入 /etc/config/vantage_theme"
#~ msgctxt "vantage-theme"
#~ msgid "Press"
#~ msgstr "按"
@@ -0,0 +1,14 @@
# Vantage theme settings (luci-theme-vantage).
#
# Appearance for every browser on this router, as picked under the theme's
# Appearance button; an option left out keeps each browser's own pick:
#
# option theme 'auto' auto, light or dark
# option login 'grid' the login page's background: grid, stars or none
# option backdrop 'grid' the dashboard's network path background, likewise
#
# The dashboard's Customize saves here too: its chart style (option chart) and
# one layout per screen size (option layout_wide, layout_mid, layout_narrow and
# the same with _wired), which are best changed there.
config appearance 'appearance'
@@ -0,0 +1,11 @@
{
"luci-theme-vantage": {
"description": "Vantage theme: save its Appearance (colour mode, login and network path backgrounds) and the dashboard's chart style and layouts for every browser, in /etc/config/vantage_theme only",
"write": {
"ubus": {
"uci": [ "set", "commit", "revert" ]
},
"uci": [ "vantage_theme" ]
}
}
}
@@ -16,11 +16,48 @@
const found = !!dispatched?.action?.type && dispatched.action.type != 'firstchild';
const page_title = !authed ? null : !found ? _('Not found') : dispatched?.title ? striptags(_(dispatched.title)) : null;
const lang = entityencode(dispatcher.lang ?? 'en', true);
const boardinfo = ubus.call("system", "board");
const hostname = striptags(boardinfo?.hostname ?? '?');
/* Appearance for every browser (/etc/config/vantage_theme): a value the
theme knows, else '' (each browser keeps its own pick). Logged in only:
nothing is read for an anonymous 404 or CSRF page */
const look = (opt, ok) => { const v = authed ? uci.get('vantage_theme', 'appearance', opt) : null; return (type(v) == 'string' && index(ok, v) >= 0) ? v : ''; };
const look_theme = look('theme', [ 'auto', 'light', 'dark' ]);
const look_login = look('login', [ 'grid', 'stars', 'none' ]);
const look_backdrop = look('backdrop', [ 'grid', 'stars', 'none' ]);
/* The dashboard's chart style and layouts, saved with it. A layout passes
only in the form vantage.layout reads, 'wide=hero:0:4 chart:0:2:h;mid=default' */
const look_chart = look('chart', [ 'overlay', 'log', 'mirror', 'mirror-split', 'lanes' ]);
const look_layout = (() => {
let out = [];
if (authed)
for (let k in [ 'wide', 'mid', 'narrow', 'wide_wired', 'mid_wired', 'narrow_wired' ]) {
const v = uci.get('vantage_theme', 'appearance', 'layout_' + k);
if (type(v) == 'string' && match(v, /^(default|[a-z]{1,8}:[0-3]:[1-4](:h)?( [a-z]{1,8}:[0-3]:[1-4](:h)?){0,7})$/))
push(out, k + '=' + v);
}
return join(';', out);
})();
/* May this login save it for every browser? 'yes'; 'relogin' when rpcd
would grant the theme's group at a new login and no reload of rpcd
has given it to this one yet (the install's reload reaches open
logins); 'no' otherwise, and without the config's section. Only the
word reaches the page. */
const look_save = (() => {
if (!authed || type(uci.get_all('vantage_theme', 'appearance')) != 'object') return 'no';
const held = ctx.authacl?.['access-group']?.['luci-theme-vantage'];
if (type(held) == 'array' && index(held, 'write') >= 0) return 'yes';
let would = false;
uci.foreach('rpcd', 'login', s => {
if (s.username !== ctx.authuser) return;
for (let p in (type(s.write) == 'array' ? s.write : [ s.write ]))
if (type(p) == 'string' && wildcard('luci-theme-vantage', p)) would = true;
});
return would ? 'relogin' : 'no';
})();
const boardinfo = ubus.call("system", "board");
const hostname = striptags(boardinfo?.hostname ?? '?');
-%}
<!DOCTYPE html>
<html lang="{{ lang }}" data-darkmode="true">
<html lang="{{ lang }}" data-darkmode="true" data-vt-look-theme="{{ look_theme }}" data-vt-look-login="{{ look_login }}" data-vt-look-backdrop="{{ look_backdrop }}" data-vt-look-chart="{{ look_chart }}" data-vt-look-layout="{{ look_layout }}" data-vt-look-save="{{ look_save }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
@@ -28,11 +65,22 @@
<title>{{ page_title ? entityencode(page_title, true) + ' · ' : '' }}LuCI</title>
<script>
(function (r) {
var p = 'auto', rail = 'pinned';
try { p = localStorage.getItem('vantage.theme') || 'auto'; rail = localStorage.getItem('vantage.rail') || 'pinned'; } catch (e) {}
/* a pick made without the right to save it for everyone (not for a
login that may), the router's, this browser's from before, the
default (lookValue in the theme's menu) */
function look(name, own, before, ok) {
var v = [ null, r.getAttribute('data-vt-look-' + name), null ], i;
try { v[0] = r.getAttribute('data-vt-look-save') === 'yes' ? null : localStorage.getItem(own); v[2] = localStorage.getItem(before); } catch (e) {}
for (i = 0; i < 3; i++) if (ok.indexOf(v[i]) >= 0) return v[i];
return ok[0];
}
var p = look('theme', 'vantage.theme.own', 'vantage.theme', [ 'auto', 'light', 'dark' ]), rail = '';
try { rail = localStorage.getItem('vantage.rail') || 'pinned'; } catch (e) {}
var dark = (p === 'dark') || (p !== 'light' && !!(window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches));
r.setAttribute('data-darkmode', dark ? 'true' : 'false');
r.setAttribute('data-theme', p);
/* the dashboard's network path background, read by luci-app-vantage */
r.setAttribute('data-vt-backdrop', look('backdrop', 'vantage.backdrop.own', 'vantage.backdrop.v1', [ 'grid', 'stars', 'none' ]));
if (rail === 'pinned') r.setAttribute('data-rail', 'pinned');
})(document.documentElement);
</script>
@@ -5,18 +5,23 @@
fuser is attacker-controlled (formvalue also reads the query string): it is
only tested, never echoed; the error is generic. The form posts back to the
requested URL (no action attribute). The inline script is data-free: the
HTTPS probe reads its port list from an entity-encoded data attribute.
HTTPS probe reads its port list from an entity-encoded data attribute. The
Appearance saved for every browser (/etc/config/vantage_theme) comes as
data attributes of values the theme knows only.
-#}
{%
let tls = uci.get('uhttpd', 'main', 'listen_https') ?? [];
if (type(tls) == 'string')
tls = split(tls, /\s+/);
const tls_ports = uniq(filter(map(tls, e => +(match(e, /\d+$/)?.[0] ?? 0)), p => (p > 0 && p <= 65535)));
const look = (opt, ok) => { const v = uci.get('vantage_theme', 'appearance', opt); return (type(v) == 'string' && index(ok, v) >= 0) ? v : ''; };
const look_theme = look('theme', [ 'auto', 'light', 'dark' ]);
const look_login = look('login', [ 'grid', 'stars', 'none' ]);
const boardinfo = ubus.call("system", "board");
const hostname = striptags(boardinfo?.hostname ?? '?');
-%}
<!DOCTYPE html>
<html lang="{{ entityencode(dispatcher.lang ?? 'en', true) }}" data-darkmode="true">
<html lang="{{ entityencode(dispatcher.lang ?? 'en', true) }}" data-darkmode="true" data-vt-look-theme="{{ look_theme }}" data-vt-look-login="{{ look_login }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
@@ -25,8 +30,10 @@
<title>{{ entityencode(_('Log in'), true) }} · LuCI</title>
<script>
(function (r) {
var p = 'auto';
try { p = localStorage.getItem('vantage.theme') || 'auto'; } catch (e) {}
/* as header.ut: a pick of this browser's own, the router's, this browser's from before, auto */
var v = [ null, r.getAttribute('data-vt-look-theme'), null ], ok = [ 'auto', 'light', 'dark' ], p = 'auto', i;
try { v[0] = localStorage.getItem('vantage.theme.own'); v[2] = localStorage.getItem('vantage.theme'); } catch (e) {}
for (i = 2; i >= 0; i--) if (ok.indexOf(v[i]) >= 0) p = v[i];
var dark = (p === 'dark') || (p !== 'light' && !!(window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches));
r.setAttribute('data-darkmode', dark ? 'true' : 'false');
r.setAttribute('data-theme', p);