mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-08 05:04:21 +08:00
This commit is contained in:
1 parent
1ebb01f4e8
commit
f719901cab
44 files changed
+2190
-1439
No files matched your search
+2
-2
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => ({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
})[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 };
|
||||
@@ -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
@@ -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)/
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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#源码构建)。
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Master 分支的更新日志
|
||||
|
||||
## v3.1.3 [ 2026.11.06 ]
|
||||
|
||||
- 修复部分固件升级后 Argon 翻译丢失,保留已有翻译,并在后续在线升级时同步语言包(#737)。
|
||||
- 修复部分固件因下载器缺少 HTTPS 支持而无法获取在线壁纸的问题(#738)。
|
||||
- 修复网桥 VLAN 下拉菜单文字逐字竖排的问题(#738)。
|
||||
|
||||
## v3.1.2 [ 2026.11.02 ]
|
||||
|
||||
- 修复端口卡片换行和高度不一致,兼容 ImmortalWrt。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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'), '');
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ""
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user