Compare commits

..
2 Commits
Author SHA1 Message Date
github-actions[bot] c5a2a2ae78 🐤 Sync 2026-08-20 23:56:12 2026-08-20 23:56:12 +08:00
github-actions[bot] 2506a3d587 🎨 Sync 2026-08-20 20:39:01 2026-08-20 20:39:01 +08:00
21 changed files with 525 additions and 63 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH)
PKG_NAME:=istoreenhance
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=0.7.15
PKG_RELEASE:=4
PKG_RELEASE:=5
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
ARCH_HEXCODE=8664
@@ -25,7 +25,7 @@ ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=c74312de1d50b9de65d0044fcf7c4d88a1f9917c
PKG_SOURCE:=iStoreEnhance-binary-$(PKG_SOURCE_DATE).tar.gz
PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v0.7.15/
PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$(PKG_SOURCE_DATE)/
PKG_HASH:=skip
PKG_BUILD_DIR:=$(BUILD_DIR)/iStoreEnhance-binary-$(PKG_SOURCE_DATE)
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease-common-bin
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=9
PKG_RELEASE:=11
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=1d2184f103526f9427259c2aba880486cbd930fd
PKG_SOURCE_VERSION:=b5611e40da63fec2b6a0bb93b272fe0fdc704370
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=12
PKG_RELEASE:=14
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=1d2184f103526f9427259c2aba880486cbd930fd
PKG_SOURCE_VERSION:=b5611e40da63fec2b6a0bb93b272fe0fdc704370
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
+2 -2
View File
@@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkeasefull
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=3.0.11
PKG_RELEASE:=11
PKG_RELEASE:=13
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
PKG_HASH:=skip
endif
PKG_SOURCE_VERSION:=1d2184f103526f9427259c2aba880486cbd930fd
PKG_SOURCE_VERSION:=b5611e40da63fec2b6a0bb93b272fe0fdc704370
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
@@ -240,11 +240,21 @@ return view.extend({
return [ fam, pr ].filter(x => x).join(' · ') || '';
};
/* Sort the whole range here, not just the page: ui.Table only ever
* orders the rows it was handed, so paging first would turn the
* headers into a per-page sort while the count below still speaks
* for every measurement. */
let rows = entries.slice().reverse();
/* Build the cells first, then sort them: the sort key comes out of a
* cell, and ui.Table only ever orders the rows it was handed, so
* paging first would turn the headers into a per-page sort while the
* count below still speaks for every measurement. */
let rows = entries.slice().reverse().map(e => [
[ e.epoch ?? 0, when(e) ],
(e.server && e.server.name) || '',
e.interface || '',
protoCell(e),
[ num(e.download_mbps), fmtNum(e.download_mbps, 2) ],
[ num(e.upload_mbps), fmtNum(e.upload_mbps, 2) ],
[ num(e.ping_ms), fmtNum(e.ping_ms, 1) ],
[ num(e.jitter_ms), fmtNum(e.jitter_ms, 1) ]
]);
const sorting = this.table.getActiveSortState();
if (sorting)
@@ -263,17 +273,8 @@ return view.extend({
this.page = pages - 1;
this.renderPager(rows.length, pages);
this.table.update(rows.slice(this.page * pageSize, (this.page + 1) * pageSize));
this.table.update(rows.slice(this.page * pageSize, (this.page + 1) * pageSize).map(e => [
[ e.epoch ?? 0, when(e) ],
(e.server && e.server.name) || '',
e.interface || '',
protoCell(e),
[ num(e.download_mbps), fmtNum(e.download_mbps, 2) ],
[ num(e.upload_mbps), fmtNum(e.upload_mbps, 2) ],
[ num(e.ping_ms), fmtNum(e.ping_ms, 1) ],
[ num(e.jitter_ms), fmtNum(e.jitter_ms, 1) ]
]));
},
/* Shown only when there is more than one page; the count is always
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.8.19
PKG_RELEASE:=232
PKG_RELEASE:=233
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -778,7 +778,7 @@ function gen_config_server(node)
u.username = user.username
u.password = user.password
end
if node.protocol == "shadowsocks" or node.protocol == "trojan" then
if node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria2" or node.protocol == "anytls" then
u.name = user.username
u.password = user.password
end
@@ -801,10 +801,6 @@ function gen_config_server(node)
u.password = user.password
u.uuid = user.uuid
end
if node.protocol == "hysteria2" then
u.name = user.username
u.password = user.password
end
if node.protocol == "wireguard" then
u.public_key = user.wireguard_public_key
u.pre_shared_key = user.wireguard_pre_shared_key
@@ -0,0 +1,27 @@
#!/bin/sh
# Devices without a hardware RTC boot with a wrong system clock: sysfixtime can
# only restore the mtime of the newest file under /etc, so the clock is usually
# hours behind after a cold boot. passwall is then started by
# /etc/hotplug.d/iface/98-passwall on ifup, which typically happens before NTP
# has corrected the time, and time-sensitive handshakes (VMess AEAD, TLS) fail.
#
# Nothing restarts passwall once the clock is corrected, so it stays broken
# until the user restarts it manually. Restart once when ntpd reports that the
# time is valid -- the same approach dnsmasq uses for DNSSEC in
# /etc/hotplug.d/ntp/25-dnsmasqsec.
[ "$ACTION" = "stratum" ] || exit 0
. /usr/share/passwall/utils.sh
NTP_LOCK_FILE="${LOCK_PATH}/${CONFIG}_ntp.lock"
[ -f "${NTP_LOCK_FILE}" ] && exit 0
([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && {
echo $$ > ${NTP_LOCK_FILE}
/etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
logger -p notice -t network -s "${CONFIG}: restart after NTP time became valid"
}
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.8.17
PKG_RELEASE:=93
PKG_VERSION:=26.8.20
PKG_RELEASE:=94
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -800,7 +800,7 @@ function gen_config_server(node)
u.username = user.username
u.password = user.password
end
if node.protocol == "shadowsocks" or node.protocol == "trojan" then
if node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria2" or node.protocol == "anytls" then
u.name = user.username
u.password = user.password
end
@@ -823,10 +823,6 @@ function gen_config_server(node)
u.password = user.password
u.uuid = user.uuid
end
if node.protocol == "hysteria2" then
u.name = user.username
u.password = user.password
end
if node.protocol == "wireguard" then
u.public_key = user.wireguard_public_key
u.pre_shared_key = user.wireguard_pre_shared_key
@@ -0,0 +1,27 @@
#!/bin/sh
# Devices without a hardware RTC boot with a wrong system clock: sysfixtime can
# only restore the mtime of the newest file under /etc, so the clock is usually
# hours behind after a cold boot. passwall2 is then started by
# /etc/hotplug.d/iface/98-passwall2 on ifup, which typically happens before NTP
# has corrected the time, and time-sensitive handshakes (VMess AEAD, TLS) fail.
#
# Nothing restarts passwall2 once the clock is corrected, so it stays broken
# until the user restarts it manually. Restart once when ntpd reports that the
# time is valid -- the same approach dnsmasq uses for DNSSEC in
# /etc/hotplug.d/ntp/25-dnsmasqsec.
[ "$ACTION" = "stratum" ] || exit 0
. /usr/share/passwall2/utils.sh
NTP_LOCK_FILE="${LOCK_PATH}/${CONFIG}_ntp.lock"
[ -f "${NTP_LOCK_FILE}" ] && exit 0
([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && {
echo $$ > ${NTP_LOCK_FILE}
/etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
logger -p notice -t network -s "${CONFIG}: restart after NTP time became valid"
}
@@ -2011,7 +2011,7 @@ local function curl(url, file, ua, mode, hwid)
end
function get_headers()
local cache_file = CACHE_PATH .. "/sub_curl_headers"
local cache_file = api.CACHE_PATH .. "/sub_curl_headers"
if fs.access(cache_file) then
return luci.sys.exec("cat " .. cache_file)
end
@@ -13,10 +13,10 @@ address, so they stay local).
|---|---|---|---|
| main.css (identity/raw) | ≤ 193 KB | size | production build, 2026-08 (191,899 B; +~2 KB custom-background mode, +~0.5 KB router progress bar / live region; the bar is now Turbo-shaped — width/opacity only, inserted per navigation) |
| login.css (identity/raw) | ≤ 12 KB | size | production build, 2026-07 (10,935 B, token-pruned) |
| menu-aurora.js (identity/raw) | ≤ 21.5 KB | size | production build, 2026-08 (20,851 B; +1.3 KB for the router's `syncRoute`/`closeSurfaces` hooks) |
| menu-aurora.js (identity/raw) | ≤ 22 KB | size | production build, 2026-08 (21,809 B; +0.8 KB palette recents: record on pick, pure-LRU browse order, storage validation; +0.2 KB ">" logout command) |
| router-aurora.js (identity/raw, Navigation-API browsers only) | ≤ 15 KB | size | production build, 2026-08 (14,844 B: template shells fetched, per-render listener teardown, timeout-as-failure, expiry gate, readonly folding, node css, wildcard actions, trickling progress bar, same-URL reload rule, visibility gate, contract check) |
| Default logo (identity/raw) | ≤ 16 KB | size | production build, 2026-07 (15,057 B) |
| Core admin cold theme assets (identity/raw) | ≤ 267 KB | size | main CSS + menu JS + router JS + default font + logo, 2026-08 (≈266.3 KB; the router is a one-time cost that removes per-click dispatcher work) |
| Core admin cold theme assets (identity/raw) | ≤ 267.5 KB | size | main CSS + menu JS + router JS + default font + logo, 2026-08 (≈267.2 KB; the router is a one-time cost that removes per-click dispatcher work; +1.0 KB palette recents and logout command) |
| Login cold theme assets, excluding configured background (identity/raw) | ≤ 55 KB | size | login CSS + default font + logo, 2026-07 (49,572 B) |
| Blocking requests before first paint | ≤ 4 | count | current waterfall |
| Repeat-visit asset requests | ≈ 0 | count | target state; package-built CSS/JS URLs are versioned, but long-lived cache headers still need live verification |
@@ -2,6 +2,8 @@
"require baseclass";
"require ui";
const PALETTE_RECENTS_KEY = "aurora.paletteRecents";
return baseclass.extend({
__init__() {
ui.menu.load().then((tree) => this.render(tree));
@@ -599,8 +601,12 @@ return baseclass.extend({
if (!toggle || this.paletteIndex) return;
this.paletteIndex = [];
let logout = null;
items.forEach((item) => {
if (item.isLogout) return;
if (item.isLogout) {
logout = item;
return;
}
if (!item.hasChildren) {
this.paletteIndex.push({
title: item.title,
@@ -645,6 +651,18 @@ return baseclass.extend({
}),
);
// Logout is a command, not a destination: it rides ">" only (last, after
// the modes), stays out of the plain browse list, and is never recorded
// as a recent — but a typed query still reaches it.
if (logout)
this.paletteIndex.push({
title: logout.title,
name: logout.name,
group: null,
href: logout.href,
isLogout: true,
});
// Only msgids that already exist in a shipped catalog (luci-base, or the
// config app's as above) are used — the theme intentionally ships no
// translations of its own.
@@ -785,8 +803,15 @@ return baseclass.extend({
// the clicked anchor is replaced by the re-render, which would strand
// focus on <body> with the dialog still open.
results.addEventListener("click", (e) => {
const mode = e.target?.closest?.(".cmdk-row")?.dataset.mode;
if (!mode) return;
const row = e.target?.closest?.(".cmdk-row");
if (!row) return;
const mode = row.dataset.mode;
if (!mode) {
// Navigation rows: remember the pick, then let the click navigate
// (same-document router or full load) untouched.
if (row.dataset.name) this.recordPaletteRecent(row.dataset.name);
return;
}
e.preventDefault();
setTheme(mode);
syncSwitchers();
@@ -876,6 +901,46 @@ return baseclass.extend({
// Empty query matches everything at score 0 (the browse list); title hits
// outrank name/path and group hits and are the only ones highlighted.
// localStorage can be unavailable (privacy modes) or hold anything after
// a downgrade — both read as "no history". No size cap: dedupe bounds the
// list by the pages actually visited, i.e. the menu's own scale.
readPaletteRecents() {
try {
const list = JSON.parse(localStorage.getItem(PALETTE_RECENTS_KEY));
return Array.isArray(list)
? list.filter((name) => typeof name === "string")
: [];
} catch {
return [];
}
},
recordPaletteRecent(name) {
try {
localStorage.setItem(
PALETTE_RECENTS_KEY,
JSON.stringify([
name,
...this.readPaletteRecents().filter((kept) => kept !== name),
]),
);
} catch {
// History is decorative; a sealed or full store just means none.
}
},
// Pure LRU: every visited page floats, so long use converges on a fully
// most-recently-used browse order (Sublime's model). Names that vanished
// from the menu (ACL change, package removed) drop out here; command rows
// never float, whatever the store says.
paletteBrowseRecents() {
return this.readPaletteRecents().filter((name) =>
this.paletteIndex.some(
(page) => page.name === name && !page.mode && !page.isLogout,
),
);
},
matchPaletteEntry(q, page) {
if (!q) return { score: 0, ranges: null };
@@ -888,23 +953,37 @@ return baseclass.extend({
return rest && { score: rest.score, ranges: null };
},
renderPaletteResults(value) {
collectPaletteMatches(value) {
let q = value.trim().toLowerCase();
// A leading ">" scopes the list to the command rows (the prototype's
// command-only mode); "" covers CJK IMEs emitting the full-width form.
const cmdOnly = q[0] === ">" || q[0] === "";
if (cmdOnly) q = q.slice(1).trim();
const theme = localStorage.getItem("aurora.theme") || "device";
// Browsing floats the recently opened pages; recency rides the same
// stable sort as fuzzy rank, so ties keep menu order and typed queries
// (which leave recents empty) stay pure fuzzy.
const recents = q || cmdOnly ? [] : this.paletteBrowseRecents();
const matches = [];
for (const page of this.paletteIndex) {
if (cmdOnly && !page.mode) continue;
if (cmdOnly && !page.mode && !page.isLogout) continue;
if (page.isLogout && !cmdOnly && !q) continue;
const m = this.matchPaletteEntry(q, page);
if (m) matches.push({ page, score: m.score, ranges: m.ranges });
if (!m) continue;
const at = recents.indexOf(page.name);
matches.push({
page,
score: at < 0 ? m.score : recents.length - at,
ranges: m.ranges,
});
}
// sort() is spec-stable: equal scores keep menu order (and the browse
// list stays in menu order by skipping the sort entirely).
if (q) matches.sort((a, b) => b.score - a.score);
if (q || recents.length) matches.sort((a, b) => b.score - a.score);
return matches;
},
renderPaletteResults(value) {
const matches = this.collectPaletteMatches(value);
const theme = localStorage.getItem("aurora.theme") || "device";
this.paletteInput.removeAttribute("aria-activedescendant");
this.paletteList.replaceChildren();
@@ -926,6 +1005,7 @@ return baseclass.extend({
href: page.href || "#",
};
if (page.mode) attributes["data-mode"] = page.mode;
else if (!page.isLogout) attributes["data-name"] = page.name;
if (current) attributes["aria-current"] = "true";
this.paletteList.appendChild(
@@ -23,7 +23,10 @@ test("production assets stay within raw-transfer budgets", () => {
// binds.
assert.ok(main <= 193_000, "main.css exceeds 193 KB");
assert.ok(login <= 12_000, "login.css exceeds 12 KB");
assert.ok(menu <= 21_500, "menu-aurora.js exceeds 21.5 KB");
// 22K: palette recents (record on pick, pure-LRU browse order, storage
// validation) added ~0.8 KB and the ">" logout command ~0.2 KB. The total
// budget below moved by the same amount.
assert.ok(menu <= 22_000, "menu-aurora.js exceeds 22 KB");
// 15K: the expiry gate, readonly folding, menu.d node css, wildcard
// actions, progress bar, visibility gate and contract check added ~3 KB
// over the first cut; the total-transfer budget below moved by the same
@@ -31,8 +34,8 @@ test("production assets stay within raw-transfer budgets", () => {
assert.ok(router <= 15_000, "router-aurora.js exceeds 15 KB");
assert.ok(logo <= 16_000, "logo.svg exceeds 16 KB");
assert.ok(
main + menu + router + font + logo <= 267_000,
"admin assets exceed 267 KB",
main + menu + router + font + logo <= 267_500,
"admin assets exceed 267.5 KB",
);
assert.ok(login + font + logo <= 55_000, "login assets exceed 55 KB");
});
@@ -13,7 +13,10 @@ const E = (tagName, attributes, children) => ({
children,
});
const loadMenuModule = () => {
const loadMenuModule = (
localStorage = {},
{ document = {}, navigator = { platform: "" } } = {},
) => {
const baseclass = {
extend(module) {
return module;
@@ -34,8 +37,9 @@ const loadMenuModule = () => {
"document",
"window",
"localStorage",
"navigator",
source,
)(baseclass, ui, E, L, (value) => value, {}, {}, {});
)(baseclass, ui, E, L, (value) => value, document, {}, localStorage, navigator);
};
const menu = loadMenuModule();
@@ -174,3 +178,287 @@ test("skips highlighting when case folding would skew the offsets", () => {
test("renders the plain title when there are no ranges", () => {
assert.deepEqual(menu.highlightPaletteMatch("Status", null), ["Status"]);
});
// ---- Recents: recording, storage hygiene, and browse-order floating ----
const RECENTS_KEY = "aurora.paletteRecents";
const fakeStorage = (initial = {}) => {
const map = new Map(Object.entries(initial));
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
map,
};
};
const navPage = (name, title, group = "Group") => ({
title,
name,
group,
href: `/${name}`,
});
const paletteWith = (storage, index) => {
const module = loadMenuModule(storage);
module.paletteIndex = index;
return module;
};
const browseIndex = () => [
navPage("status/overview", "概览", "状态"),
navPage("status/syslog", "系统日志", "状态"),
navPage("network/iface", "接口", "网络"),
navPage("network/firewall", "防火墙", "网络"),
{ title: "浅色模式", name: "theme light", group: "主题", mode: "light" },
];
const stored = (recents) => ({ [RECENTS_KEY]: JSON.stringify(recents) });
const names = (matches) => matches.map((match) => match.page.name);
test("a recorded selection lands at the head of the stored recents", () => {
const storage = fakeStorage();
const palette = paletteWith(storage, browseIndex());
palette.recordPaletteRecent("status/syslog");
palette.recordPaletteRecent("network/iface");
assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [
"network/iface",
"status/syslog",
]);
});
test("re-recording an entry moves it up instead of duplicating it", () => {
const storage = fakeStorage(stored(["network/iface", "status/syslog"]));
const palette = paletteWith(storage, browseIndex());
palette.recordPaletteRecent("status/syslog");
assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [
"status/syslog",
"network/iface",
]);
});
test("the store has no artificial cap — dedupe is the only bound", () => {
const storage = fakeStorage(stored(["h", "g", "f", "e", "d", "c", "b", "a"]));
const palette = paletteWith(storage, browseIndex());
palette.recordPaletteRecent("i");
assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [
"i",
"h",
"g",
"f",
"e",
"d",
"c",
"b",
"a",
]);
});
test("corrupt or foreign stored values read as no history", () => {
for (const value of ["not json", '"just a string"', "[1,2,3]", "{}"]) {
const palette = paletteWith(
fakeStorage({ [RECENTS_KEY]: value }),
browseIndex(),
);
assert.deepEqual(palette.readPaletteRecents(), [], value);
}
});
test("a throwing storage neither breaks reads nor records", () => {
const hostile = {
getItem: () => {
throw new Error("privacy mode");
},
setItem: () => {
throw new Error("quota");
},
};
const palette = paletteWith(hostile, browseIndex());
assert.deepEqual(palette.readPaletteRecents(), []);
assert.doesNotThrow(() => palette.recordPaletteRecent("status/overview"));
});
test("browsing floats stored recents in recency order, menu order behind", () => {
const palette = paletteWith(
fakeStorage(stored(["network/iface", "status/syslog"])),
browseIndex(),
);
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"network/iface",
"status/syslog",
"status/overview",
"network/firewall",
"theme light",
]);
});
test("every visited page floats in recency order; vanished names drop", () => {
const wide = [
navPage("p1", "One"),
navPage("p2", "Two"),
navPage("p3", "Three"),
navPage("p4", "Four"),
navPage("p5", "Five"),
navPage("p6", "Six"),
navPage("p7", "Seven"),
];
const palette = paletteWith(
fakeStorage(stored(["removed/page", "p7", "p6", "p5", "p4", "p3", "p2"])),
wide,
);
// Pure LRU: all six visited pages float, only p1 keeps its menu slot.
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"p7",
"p6",
"p5",
"p4",
"p3",
"p2",
"p1",
]);
});
test("a stored name never floats a command row", () => {
const palette = paletteWith(
fakeStorage(stored(["theme light"])),
browseIndex(),
);
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"status/overview",
"status/syslog",
"network/iface",
"network/firewall",
"theme light",
]);
});
test("typed queries rank by fuzzy score alone", () => {
const palette = paletteWith(
fakeStorage(stored(["diag"])),
[navPage("dns", "DNS Settings"), navPage("diag", "Diagnostics")],
);
// "Diagnostics" is the recent entry, but the tight title hit must win.
assert.deepEqual(names(palette.collectPaletteMatches("dns")), [
"dns",
"diag",
]);
});
test("'>' browsing lists command rows only, unaffected by recents", () => {
const palette = paletteWith(
fakeStorage(stored(["network/iface"])),
browseIndex(),
);
assert.deepEqual(names(palette.collectPaletteMatches(">")), ["theme light"]);
});
// ---- Logout as a ">"-only command ----
const logoutIndex = () => [
navPage("p1", "Alpha"),
navPage("p2", "Beta"),
navPage("p3", "Gamma"),
navPage("p4", "Delta"),
navPage("p5", "Epsilon"),
navPage("p6", "Zeta"),
{ title: "浅色模式", name: "theme light", group: "主题", mode: "light" },
{ title: "Logout", name: "logout", group: null, href: "/logout", isLogout: true },
];
test("'>' browsing includes the logout command", () => {
const palette = paletteWith(fakeStorage(), logoutIndex());
assert.deepEqual(names(palette.collectPaletteMatches(">")), [
"theme light",
"logout",
]);
});
test("plain browsing hides the logout command", () => {
const palette = paletteWith(fakeStorage(), logoutIndex());
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"p1",
"p2",
"p3",
"p4",
"p5",
"p6",
"theme light",
]);
});
test("typed queries still reach logout", () => {
const palette = paletteWith(fakeStorage(), logoutIndex());
assert.deepEqual(names(palette.collectPaletteMatches("logout")), ["logout"]);
});
test("a stored logout name never floats", () => {
const palette = paletteWith(
fakeStorage(stored(["logout", "p6", "p5", "p4", "p3", "p2"])),
logoutIndex(),
);
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"p6",
"p5",
"p4",
"p3",
"p2",
"p1",
"theme light",
]);
});
test("initPalette appends logout as the last command row", () => {
const toggle = {
setAttribute() {},
addEventListener() {},
};
const doc = {
querySelector: (sel) => (sel === "#cmdk-trigger" ? toggle : null),
addEventListener() {},
};
const palette = loadMenuModule(fakeStorage(), { document: doc });
palette.initPalette([
{
name: "status",
title: "Status",
hasChildren: true,
pages: [{ name: "overview", title: "Overview", href: "/overview" }],
},
{
name: "logout",
title: "Logout",
href: "/logout",
hasChildren: false,
isLogout: true,
},
]);
const last = palette.paletteIndex.at(-1);
assert.equal(last.isLogout, true);
assert.equal(last.href, "/logout");
assert.equal(last.title, "Logout");
// After the theme commands, so ">" lists modes first, logout last.
assert.ok(palette.paletteIndex.findIndex((page) => page.mode) >= 0);
assert.ok(
palette.paletteIndex.findIndex((page) => page.mode) <
palette.paletteIndex.indexOf(last),
);
});
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=1.2.3
PKG_RELEASE:=69
PKG_VERSION:=1.2.4
PKG_RELEASE:=70
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
PKG_RELEASE:=29
PKG_RELEASE:=30
endif
LUCI_TITLE:=Footstrap Theme
@@ -86,11 +86,53 @@ function fittersEnabled() {
function run() {
if (!fittersEnabled()) return;
runAll(_fitters, 'fitter');
/* the page is settled now, so this is the position the next mutation must be measured against
* unless a correction is already on its way, which would make this reference the drifted one */
/* the page is settled now: this is the height the NEXT tick may not go below, and the position
* the next mutation must be measured against the latter unless a correction is already on its
* way, which would make this reference the drifted one */
holdFloor();
if (!_anchorPending) rememberRest();
}
/* ---- THE DOCUMENT MAY NOT GET SHORTER WHILE A TICK IS IN FLIGHT ----
*
* The correction below puts a reader back after the engine has moved them. This is the other half,
* and it is the half that means they were never moved: `dom.content()` what every LuCI poll calls
* to refresh a section empties the container before it refills it, and a document that is briefly
* shorter than the offset the reader is at is a document the engine clamps the offset into. Nothing
* puts that back afterwards, because the shortening was never real.
*
* So the CONTENT COLUMN keeps a floor: `min-height`, set to the height it had at the last settled
* moment and held until the next one. A section emptying inside it takes nothing off the document,
* there is no shorter document to clamp into, and the tick is invisible. The floor is re-measured
* after every settled batch, so a page that genuinely got shorter is shorter one frame later.
*
* WHY THE FLOOR IS ON `.fs-content` AND NOT ON THE CONTAINER BEING SWAPPED. A version of this
* shipped in fs-overview.js, pinning the container and releasing it in the same statement sequence
* `dom.content()` performs no layout, so no layout ever saw the pin and it did nothing at all
* (measured: 1882px still clamped away with the pin in place). The fix reported from the field wraps
* `dom.content()` itself and releases two frames later, which does work at the price of patching
* a luci-base API every app on the router shares, and up to seven read/write pairs per call. One
* element that the theme owns, measured once per settled batch, is the same protection: measured on
* a live router with the corrections switched off, the wrapper and this floor both hold the reader
* at 0px against a 337px drift without either, at 16 measurements against 154 wrapped calls.
*
* NOT WHILE THE READER SCROLLS. Clearing the floor to re-measure is a layout read, and the flick is
* where this file spends its comments avoiding those; the floor simply stays where it was, which is
* still a floor. */
function holdFloor() {
/* ONLY WHERE THE THEME IS RESPONSIBLE, same rule as the correction and for a measured reason: an
* engine that anchors by itself reads the held height as one more thing that moved. On Chromium
* in the sidebar layout at 1440 the reader ended 15px off with the floor held and 0px without it,
* reproducibly, while the floor bought that engine nothing it puts the reader back on its own. */
if (ENGINE_ANCHORS || scrolling()) return;
const el = document.querySelector('.fs-content');
if (!el) return; /* the login page has no content column */
/* cleared BEFORE the read, or the floor would measure itself and never come down */
el.style.minHeight = '';
const h = el.offsetHeight;
if (h > 0) el.style.minHeight = h + 'px';
}
/* ---- IS THE PAGE MOVING RIGHT NOW? ASKED OF THE POSITION, NEVER OF THE EVENTS ----
*
* A pass that reads layout must not run while the page moves: on a phone every such read forces a
@@ -193,7 +235,9 @@ function sampleMotion() {
}
if (scrolling()) { requestAnimationFrame(sampleMotion); return; }
_sampling = false;
/* the reader has stopped: this is a still moment, and the reference for the next tick */
/* the reader has stopped: this is a still moment, so the floor and the reference both belong to
* where the page now stands */
holdFloor();
rememberRest();
/* the page has held still for SCROLL_IDLE: whatever was put off may run now */
if (_deferred) {
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=opera-proxy
PKG_VERSION:=1.28.0
PKG_RELEASE:=9
PKG_VERSION:=1.29.0
PKG_RELEASE:=10
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
PKG_LICENSE:=MIT