mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 12:24:33 +08:00
Compare commits
2
Commits
6a58ba53d0
...
c5a2a2ae78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5a2a2ae78 | ||
|
|
2506a3d587 |
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user