🎁 Sync 2026-10-09 12:18:17

This commit is contained in:
github-actions[bot] committed 2026-10-09 12:18:17 +08:00
1 parent c43003617d
commit 1ee3366de6
139 files changed
+1934 -500

No files matched your search

+4 -1
View File
@@ -9,9 +9,12 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Argon Theme
LUCI_DEPENDS:=+USE_APK:wget-any +!USE_APK:wget +jsonfilter
PKG_VERSION:=2.4.8
PKG_RELEASE:=27
PKG_RELEASE:=28
CONFIG_LUCI_CSSTIDY:=
# Ship template source across stable and snapshot runtimes. Precompiled
# bytecode would require the SDK's ucode format (see issue #715).
LUCI_MINIFY_UT:=0
include $(TOPDIR)/feeds/luci/luci.mk
+10
View File
@@ -78,6 +78,14 @@ uci commit argon
Without a key, the login page uses a local background. Bing and Wallhaven wallpaper options are unaffected.
## Custom browser icon and login logo
Under **System → Argon Config → Custom branding**, upload a browser icon and login logo separately, or use one image for both. Each can be restored to its default. PNG images must be 16–1024 pixels per side and at most 1 MiB. Browser icons must be square; login logos retain their aspect ratio in both login layouts.
Update both the theme and configuration plugin. Uploads and resets apply immediately without saving other settings; reload open pages to update their icons. The custom browser icon also supplies the touch icon and web manifest. Existing home-screen shortcuts may need to be added again.
Files are stored in `/www/luci-static/argon/branding/` without replacing packaged assets. The configuration plugin includes this directory in `keep.d` backups for upgrades that preserve configuration. Upgrades without configuration preservation reset the icons.
## Compatibility
Only the `master` branch is maintained now.
@@ -109,6 +117,8 @@ Choose the format used by the firmware's native package manager. The presence of
The APK assets use **APK v3 format**, which `apk-tools 2.x` (including `2.14.0`) cannot read. `--allow-untrusted` only bypasses signature trust checks; it does not add format support. `apk` cannot install IPK files either.
APK builds target the OpenWrt **25.12.5 stable SDK** and keep theme templates as source for runtime compatibility. The original v2.4.8 APK was built with a snapshot SDK and requires `ucode>=2026.02.27`, which OpenWrt 25.12.5 does not provide ([#715](https://github.com/jerrykuku/luci-theme-argon/issues/715)). That asset needs to be rebuilt with this fix; updating this repository does not replace already published packages. Do not bypass dependency checks to install incompatible bytecode.
For the GL.iNet / OpenWrt 23.05 environment in [#705](https://github.com/jerrykuku/luci-theme-argon/issues/705), download IPK files and install them with the firmware's existing `opkg`. There is no need to replace the system package manager.
Download `luci-theme-argon`, `luci-app-argon-config`, and any required `luci-i18n-argon-config-<language>` packages from [Releases][release]. Select the same format for all packages and use the actual asset filenames; do not construct version suffixes or rename file extensions.
+10
View File
@@ -78,6 +78,14 @@ uci commit argon
未配置密钥时,登录页会使用本地背景。Bing 和 Wallhaven 壁纸选项不受影响。
## 自定义网站图标与登录 Logo
在 **系统 → Argon 主题设置 → 自定义图标** 中,可以分别上传网站图标和登录页 Logo,也可以将同一张图片同时用于两处;每项都可恢复默认。支持 PNG 格式,每边 16–1024 像素,最大 1 MiB。网站图标需为正方形;登录 Logo 按原比例缩放,兼容经典侧栏和居中卡片。
请同时更新主题和配置插件。上传或恢复后立即生效,无需保存其他设置;已打开的页面需要刷新才能更新图标。自定义网站图标也用于手机桌面图标和 manifest,已添加到桌面的快捷方式可能需要重新添加。
自定义文件保存在 `/www/luci-static/argon/branding/`,不会覆盖主题内置图标。配置插件通过 `keep.d` 将此目录纳入保留配置升级的备份;不保留配置时不会保留自定义图标。
## 兼容性
目前仅维护 `master` 分支。
@@ -109,6 +117,8 @@ make -j1 V=s
本项目的 APK 使用 **APK v3 格式**,`apk-tools 2.x`(包括 `2.14.0`)无法读取。`--allow-untrusted` 只跳过签名信任检查,不能解决格式不兼容;`apk` 也不能安装 IPK。
APK 构建使用 OpenWrt **25.12.5 稳定版 SDK**,主题模板保留源码以兼容目标运行时。原始 v2.4.8 APK 使用 snapshot SDK 构建,要求 `ucode>=2026.02.27`,而 OpenWrt 25.12.5 不提供该版本([#715](https://github.com/jerrykuku/luci-theme-argon/issues/715))。该附件需要包含本次修复后重新构建;更新仓库代码不会替换已经发布的安装包。请勿绕过依赖检查安装不兼容的字节码。
对于 [#705](https://github.com/jerrykuku/luci-theme-argon/issues/705) 中的 GL.iNet / OpenWrt 23.05 环境,应下载 IPK 并使用固件原有的 `opkg` 安装,无需替换系统包管理器。
从 [Release][release] 下载主题包 `luci-theme-argon`、配置插件包 `luci-app-argon-config`,以及需要的 `luci-i18n-argon-config-<语言>` 语言包。所有包必须选择相同格式,以附件的实际文件名为准,不要自行拼接版本号或修改扩展名。
+20
View File
@@ -2,6 +2,22 @@
## v2.4.8 [ 2026.10.08 ]
### v2.4.8 r2 rebuild / 修订版重发
Republished on 2026-10-09 with package revision 2: APK packages use `2.4.8-r2`; IPK packages use `2.4.8-2` so both package managers recognize the update.
2026-10-09 重发,主题及配置插件包修订号更新为 2:APK 版本为 `2.4.8-r2`,IPK 版本为 `2.4.8-2`,便于从旧包升级。
- Fix stable OpenWrt 25.12 APK installation (#715): build with SDK 25.12.5 and ship template source.
- Improve dark sysupgrade dialog contrast and wrapping (`1055e79`).
- Correct notification banner positioning and spacing (`c70ed6b`).
- Keep the configuration plugin at `9bafffa`; login-page layout selection and custom favicon/Logo settings are not included.
- 修复稳定版 OpenWrt 25.12 的 APK 安装问题(#715):使用 25.12.5 SDK,模板保留源码。
- 修复深色升级弹窗的对比度与换行(`1055e79`)。
- 修复通知横幅位置和内部间距(`c70ed6b`)。
- 配置插件固定为 `9bafffa`,不包含登录页样式切换及自定义网站图标/Logo 设置。
### Installation / 安装说明
| Firmware / 固件 | Package / 安装包 |
@@ -13,6 +29,10 @@ APK assets use **APK v3** and cannot be read by `apk-tools 2.x`, including `2.14
APK 附件采用 **APK v3 格式**,`apk-tools 2.x`(包括 `2.14.0`)无法读取。在原生使用 opkg 的固件上额外安装 `apk`,并不会让固件兼容 APK v3;`--allow-untrusted` 不能解决格式不兼容,`apk` 也不能安装 IPK。
APK builds use the OpenWrt **25.12.5 stable SDK**. Theme templates are shipped as source to avoid snapshot-specific ucode bytecode requirements. If an older APK reports `ucode>=2026.02.27`, use a package rebuilt with the [#715 fix](https://github.com/jerrykuku/luci-theme-argon/issues/715); bypassing dependency checks does not make that bytecode compatible.
APK 使用 OpenWrt **25.12.5 稳定版 SDK** 构建,主题模板保留源码,避免 snapshot 的 ucode 字节码版本限制。若旧 APK 提示需要 `ucode>=2026.02.27`,请使用包含 [#715 修复](https://github.com/jerrykuku/luci-theme-argon/issues/715) 的重新构建包;绕过依赖检查不能解决字节码不兼容。
Download the theme, configuration plugin, and required language packages in the **same format**. Use the exact asset filenames below. For GL.iNet / OpenWrt 23.05, use IPK files with the firmware's existing `opkg`.
主题、配置插件和需要的语言包应选择**相同格式**,以本页附件实际文件名为准。GL.iNet / OpenWrt 23.05 请下载 IPK 并使用固件原有的 `opkg` 安装。
@@ -785,7 +785,9 @@ select[multiple="multiple"] {
}
.login-page .login-container .login-form .brand .icon {
width: 50px;
height: auto;
height: 50px;
object-fit: contain;
flex-shrink: 0;
margin-right: 25px;
}
.login-page .login-container .login-form .brand .brand-text {
@@ -950,6 +952,7 @@ select[multiple="multiple"] {
}
.login-page[data-login-style="centered"] .login-container .login-form .brand .icon {
width: 2.5rem;
height: 2.5rem;
margin: 0;
flex-shrink: 0;
}
+4 -1
View File
@@ -126,7 +126,9 @@
.icon {
width: 50px;
height: auto;
height: 50px;
object-fit: contain;
flex-shrink: 0;
margin-right: 25px;
}
@@ -315,6 +317,7 @@
.icon {
width: 2.5rem;
height: 2.5rem;
margin: 0;
flex-shrink: 0;
}
@@ -0,0 +1,35 @@
'use strict';
import * as fs from 'fs';
// Read only package-managed names from the public branding directory.
// Invalid, missing or linked files fall back to the built-in theme assets.
export function get_branding() {
const dir = '/www/luci-static/argon/branding/';
let result = {};
if (fs.lstat(rtrim(dir, '/'))?.type != 'directory')
return result;
const state = fs.lstat(dir + 'current.json');
if (state?.type != 'file' || state.size > 1024)
return result;
let config;
try { config = json(fs.readfile(dir + 'current.json')); }
catch (e) { return result; }
if (type(config) != 'object')
return result;
for (let kind in ['favicon', 'logo']) {
let name = config?.[kind];
if (type(name) != 'string' || !match(name, /^image-[0-9a-f]{64}\.png$/))
continue;
let info = fs.lstat(dir + name);
if (info?.type != 'file' || info.size < 67 || info.size > 1048576)
continue;
result[kind] = name;
}
if (result.favicon) {
let manifest = replace(result.favicon, /\.png$/, '.webmanifest');
if (fs.lstat(dir + manifest)?.type == 'file')
result.manifest = manifest;
}
return result;
};
@@ -0,0 +1,84 @@
'use strict';
const assert = require('node:assert/strict');
const { spawnSync, execFileSync } = require('node:child_process');
const { mkdtempSync, readFileSync, writeFileSync, rmSync } = require('node:fs');
const { tmpdir } = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const scripts = path.resolve(__dirname, '../.github/scripts');
function metadata(tag) {
const output = execFileSync('python3', [path.join(scripts, 'release_metadata.py'), tag], { encoding: 'utf8' });
return Object.fromEntries(output.trim().split('\n').map(line => line.split('=')));
}
test('stable versions retain their package versions and preview suffixes use native package formats', () => {
for (const tag of ['v2.4.8', '2.5.0']) {
const result = metadata(tag);
assert.equal(result.tag, tag);
assert.equal(result.prerelease, 'false');
assert.equal(result.apk_version, tag.replace(/^v/, ''));
assert.equal(result.ipk_version, result.apk_version);
}
for (const stage of ['alpha', 'beta', 'rc']) {
const result = metadata(`v2.5.0-${stage}.12`);
assert.equal(result.prerelease, 'true');
assert.equal(result.version, `2.5.0-${stage}.12`);
assert.equal(result.apk_version, `2.5.0_${stage}12`);
assert.equal(result.ipk_version, `2.5.0~${stage}12-1`);
}
});
test('invalid and unsupported tags fail before producing release outputs', () => {
for (const tag of ['latest', 'v2.5.0-beta', 'v2.5.0-beta.0', 'v2.5.0-preview.1', 'v2.5.0+build', 'v2.5.0\nprerelease=false']) {
const result = spawnSync('python3', [path.join(scripts, 'release_metadata.py'), tag], { encoding: 'utf8' });
assert.notEqual(result.status, 0, tag);
assert.equal(result.stdout, '', tag);
}
});
test('preview preparation and documentation sync cannot overwrite stable metadata', t => {
const dir = mkdtempSync(path.join(tmpdir(), 'argon-preview-docs-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const files = ['Makefile', 'README.md', 'README_ZH.md', 'RELEASE.md', 'RELEASE_ZH.md'];
for (const file of files) writeFileSync(path.join(dir, file), 'stable v2.4.8\n');
const env = { ...process.env, GITHUB_REPOSITORY: '', GITHUB_TOKEN: '' };
const prepare = spawnSync('bash', [path.join(scripts, 'prepare_release_commit.sh'), 'v2.5.0-beta.1'], { cwd: dir, env });
assert.equal(prepare.status, 1);
assert.match(prepare.stderr.toString(), /matching tags/);
const sync = spawnSync('bash', [path.join(scripts, 'update_release_docs.sh'), 'v2.5.0-beta.1'], { cwd: dir, env });
assert.equal(sync.status, 0, sync.stderr.toString());
for (const file of files) assert.equal(readFileSync(path.join(dir, file), 'utf8'), 'stable v2.4.8\n');
});
test('config source resolves matching lightweight and annotated tags, and never falls back for previews', t => {
const dir = mkdtempSync(path.join(tmpdir(), 'argon-preview-config-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const git = (...args) => execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
git('init', '-b', 'master');
git('config', 'user.name', 'Release test');
git('config', 'user.email', 'release-test@example.invalid');
git('commit', '--allow-empty', '-m', 'config preview');
const tagged = git('rev-parse', 'HEAD');
git('tag', 'v2.5.0-beta.1');
git('tag', '-a', 'v2.5.0-rc.1', '-m', 'annotated preview');
git('tag', 'v2.5.0');
git('commit', '--allow-empty', '-m', 'later master');
const master = git('rev-parse', 'HEAD');
const resolve = (tag, remote = dir) => spawnSync('bash', [path.join(scripts, 'resolve_config.sh'), remote, tag], { encoding: 'utf8' });
for (const tag of ['v2.5.0-beta.1', 'v2.5.0-rc.1', 'v2.5.0']) {
const result = resolve(tag);
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout.trim(), `sha=${tagged}`);
}
assert.equal(resolve('v2.6.0').stdout.trim(), `sha=${master}`);
const missing = resolve('v2.5.0-beta.2');
assert.notEqual(missing.status, 0);
assert.match(missing.stdout, /matching config tag/);
assert.doesNotMatch(missing.stdout, /sha=/);
const unavailable = resolve('v2.6.0', path.join(dir, 'missing.git'));
assert.notEqual(unavailable.status, 0);
assert.doesNotMatch(unavailable.stdout, /sha=/);
});
@@ -260,3 +260,67 @@ jsonfilter() { return 1; }
rmSync(dir, { recursive: true, force: true });
}
});
// Exercise the real filesystem module, including malformed state and stale files.
test('custom branding accepts only published assets and safely falls back', () => {
const dir = mkdtempSync(path.join(tmpdir(), 'argon-branding-test-'));
const assets = path.join(dir, 'branding');
const module = path.join(dir, 'luci', 'argon-branding.uc');
fs.mkdirSync(path.dirname(module));
const source = fs.readFileSync(path.join(root, 'root/usr/share/ucode/luci/argon-branding.uc'), 'utf8');
writeFileSync(module, source.replace('/www/luci-static/argon/branding/', assets + '/'));
const ucode = process.env.UCODE_BIN || 'ucode';
const args = ['-L', path.join(path.dirname(ucode), '*.so'), '-L', path.join(dir, '*.uc')];
const read = () => JSON.parse(execFileSync(ucode, [...args, '-e',
"import { get_branding } from 'luci.argon-branding'; print(sprintf('%J', get_branding()));"
], { encoding: 'utf8' }));
const image = 'image-' + 'a'.repeat(64) + '.png';
const manifest = image.replace('.png', '.webmanifest');
const index = path.join(assets, 'current.json');
try {
assert.deepEqual(read(), {});
fs.mkdirSync(assets);
writeFileSync(path.join(assets, image), Buffer.alloc(100));
writeFileSync(path.join(assets, manifest), '{}');
writeFileSync(index, JSON.stringify({ favicon: image, logo: image }));
assert.deepEqual(read(), { favicon: image, logo: image, manifest });
for (const bad of ['broken', 'null', '[]', '123', '"text"', '{"logo":"../elsewhere.png"}', ' '.repeat(1025)]) {
writeFileSync(index, bad);
assert.deepEqual(read(), {});
}
writeFileSync(index, JSON.stringify({ logo: image, favicon: '../outside.png' }));
assert.deepEqual(read(), { logo: image });
fs.unlinkSync(path.join(assets, image));
assert.deepEqual(read(), {});
writeFileSync(path.join(dir, 'outside.png'), Buffer.alloc(100));
fs.symlinkSync(path.join(dir, 'outside.png'), path.join(assets, image));
assert.deepEqual(read(), {});
fs.unlinkSync(index);
fs.symlinkSync(path.join(dir, 'outside.png'), index);
assert.deepEqual(read(), {});
fs.renameSync(assets, path.join(dir, 'saved'));
fs.symlinkSync(path.join(dir, 'saved'), assets);
assert.deepEqual(read(), {});
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('custom head icons replace every default candidate and restore as a group', () => {
const dir = mkdtempSync(path.join(tmpdir(), 'argon-head-test-'));
const source = fs.readFileSync(path.join(root, 'ucode/template/themes/argon/head_meta.ut'), 'utf8')
.replace("import { get_branding } from 'luci.argon-branding';", 'function get_branding() { return fixture; }');
const file = path.join(dir, 'head.ut');
writeFileSync(file, '{% let media="/luci-static/argon"; let hostname="Router"; let node={}; let bar_color="#123456"; %}' + source);
const render = fixture => execFileSync(process.env.UCODE_BIN || 'ucode', ['-T,', '-Dfixture=' + JSON.stringify(fixture), file], { encoding: 'utf8' });
try {
const defaults = render({});
assert.match(defaults, /favicon\.ico/);
assert.match(defaults, /apple-icon-144x144\.png/);
assert.match(defaults, /icon\/manifest\.json/);
const custom = render({ favicon: 'custom.png', manifest: 'custom.webmanifest' });
assert.match(custom, /rel="icon" type="image\/png" href="\/luci-static\/argon\/branding\/custom.png"/);
assert.match(custom, /rel="apple-touch-icon" href="\/luci-static\/argon\/branding\/custom.png"/);
assert.match(custom, /branding\/custom.webmanifest/);
assert.doesNotMatch(custom, /favicon\.ico|icon\/favicon|icon\/android|icon\/apple|icon\/manifest/);
assert.equal(render({ logo: 'custom.png' }), defaults);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
@@ -1,3 +1,7 @@
{%
import { get_branding } from 'luci.argon-branding';
const branding = get_branding();
%}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ hostname }}{{ node?.title ? ` - ${striptags(node.title)}` : '' }} - LuCI</title>
@@ -15,9 +19,17 @@
<meta name="theme-color" content="{{ bar_color }}">
<!-- Icons and Manifest -->
{% if (branding.favicon): %}
<link rel="icon" type="image/png" href="{{ media }}/branding/{{ branding.favicon }}">
<link rel="apple-touch-icon" href="{{ media }}/branding/{{ branding.favicon }}">
{% if (branding.manifest): %}
<link rel="manifest" href="{{ media }}/branding/{{ branding.manifest }}">
{% endif %}
{% else %}
<link rel="icon" type="image/x-icon" href="{{ media }}/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="{{ media }}/icon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ media }}/icon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="192x192" href="{{ media }}/icon/android-icon-192x192.png">
<link rel="apple-touch-icon" sizes="144x144" href="{{ media }}/icon/apple-icon-144x144.png">
<link rel="manifest" href="{{ media }}/icon/manifest.json">
{% endif %}
@@ -25,6 +25,7 @@
{%
import * as fs from 'fs';
import { cursor } from 'uci';
import { get_branding } from 'luci.argon-branding';
let cfg = cursor();
// Only known layouts may select login page styles; old configs stay classic.
@@ -98,6 +99,8 @@
const hostname = striptags(boardinfo?.hostname ?? '?');
const themeDir = media + "/background/";
const bgArry = selectBackground(themeDir);
const branding = get_branding();
const logo_url = branding.logo ? media + '/branding/' + branding.logo : media + '/img/argon.svg';
%}
<!-- Login Page Start -->
@@ -151,7 +154,7 @@
<div class="login-container">
<div class="login-form">
<!-- Logo Start -->
<a class="brand" href="/"><img src="{{ media }}/img/argon.svg" class="icon">
<a class="brand" href="/"><img src="{{ logo_url }}" class="icon" alt="" data-fallback="{{ media }}/img/argon.svg" onerror="this.onerror=null;this.src=this.dataset.fallback">
<span class="brand-text">{{ hostname }}</span>
</a>
<!-- Logo End -->