mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 20:34:19 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5a2a2ae78 | ||
|
|
2506a3d587 |
@@ -12,7 +12,7 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH)
|
|||||||
PKG_NAME:=istoreenhance
|
PKG_NAME:=istoreenhance
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=0.7.15
|
PKG_SOURCE_DATE:=0.7.15
|
||||||
PKG_RELEASE:=4
|
PKG_RELEASE:=5
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
ARCH_HEXCODE=8664
|
ARCH_HEXCODE=8664
|
||||||
@@ -25,7 +25,7 @@ ARCH_HEXCODE=1b0c
|
|||||||
endif
|
endif
|
||||||
PKG_SOURCE_VERSION:=c74312de1d50b9de65d0044fcf7c4d88a1f9917c
|
PKG_SOURCE_VERSION:=c74312de1d50b9de65d0044fcf7c4d88a1f9917c
|
||||||
PKG_SOURCE:=iStoreEnhance-binary-$(PKG_SOURCE_DATE).tar.gz
|
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_HASH:=skip
|
||||||
|
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/iStoreEnhance-binary-$(PKG_SOURCE_DATE)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/iStoreEnhance-binary-$(PKG_SOURCE_DATE)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
|||||||
PKG_NAME:=linkease-common-bin
|
PKG_NAME:=linkease-common-bin
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=1.7.5
|
PKG_SOURCE_DATE:=1.7.5
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=11
|
||||||
|
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
|||||||
ARCH_HEXCODE=1b0c
|
ARCH_HEXCODE=1b0c
|
||||||
endif
|
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:=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_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)
|
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
|
PKG_NAME:=linkease
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=1.7.5
|
PKG_SOURCE_DATE:=1.7.5
|
||||||
PKG_RELEASE:=12
|
PKG_RELEASE:=14
|
||||||
|
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
|||||||
ARCH_HEXCODE=1b0c
|
ARCH_HEXCODE=1b0c
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PKG_SOURCE_VERSION:=1d2184f103526f9427259c2aba880486cbd930fd
|
PKG_SOURCE_VERSION:=b5611e40da63fec2b6a0bb93b272fe0fdc704370
|
||||||
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
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_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)
|
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
|
PKG_NAME:=linkeasefull
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=3.0.11
|
PKG_SOURCE_DATE:=3.0.11
|
||||||
PKG_RELEASE:=11
|
PKG_RELEASE:=13
|
||||||
|
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
|||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PKG_SOURCE_VERSION:=1d2184f103526f9427259c2aba880486cbd930fd
|
PKG_SOURCE_VERSION:=b5611e40da63fec2b6a0bb93b272fe0fdc704370
|
||||||
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
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_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)
|
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(' · ') || '–';
|
return [ fam, pr ].filter(x => x).join(' · ') || '–';
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Sort the whole range here, not just the page: ui.Table only ever
|
/* Build the cells first, then sort them: the sort key comes out of a
|
||||||
* orders the rows it was handed, so paging first would turn the
|
* cell, and ui.Table only ever orders the rows it was handed, so
|
||||||
* headers into a per-page sort while the count below still speaks
|
* paging first would turn the headers into a per-page sort while the
|
||||||
* for every measurement. */
|
* count below still speaks for every measurement. */
|
||||||
let rows = entries.slice().reverse();
|
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();
|
const sorting = this.table.getActiveSortState();
|
||||||
|
|
||||||
if (sorting)
|
if (sorting)
|
||||||
@@ -263,17 +273,8 @@ return view.extend({
|
|||||||
this.page = pages - 1;
|
this.page = pages - 1;
|
||||||
|
|
||||||
this.renderPager(rows.length, pages);
|
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
|
/* 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_NAME:=luci-app-passwall
|
||||||
PKG_VERSION:=26.8.19
|
PKG_VERSION:=26.8.19
|
||||||
PKG_RELEASE:=232
|
PKG_RELEASE:=233
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -778,7 +778,7 @@ function gen_config_server(node)
|
|||||||
u.username = user.username
|
u.username = user.username
|
||||||
u.password = user.password
|
u.password = user.password
|
||||||
end
|
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.name = user.username
|
||||||
u.password = user.password
|
u.password = user.password
|
||||||
end
|
end
|
||||||
@@ -801,10 +801,6 @@ function gen_config_server(node)
|
|||||||
u.password = user.password
|
u.password = user.password
|
||||||
u.uuid = user.uuid
|
u.uuid = user.uuid
|
||||||
end
|
end
|
||||||
if node.protocol == "hysteria2" then
|
|
||||||
u.name = user.username
|
|
||||||
u.password = user.password
|
|
||||||
end
|
|
||||||
if node.protocol == "wireguard" then
|
if node.protocol == "wireguard" then
|
||||||
u.public_key = user.wireguard_public_key
|
u.public_key = user.wireguard_public_key
|
||||||
u.pre_shared_key = user.wireguard_pre_shared_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
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-passwall2
|
PKG_NAME:=luci-app-passwall2
|
||||||
PKG_VERSION:=26.8.17
|
PKG_VERSION:=26.8.20
|
||||||
PKG_RELEASE:=93
|
PKG_RELEASE:=94
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -800,7 +800,7 @@ function gen_config_server(node)
|
|||||||
u.username = user.username
|
u.username = user.username
|
||||||
u.password = user.password
|
u.password = user.password
|
||||||
end
|
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.name = user.username
|
||||||
u.password = user.password
|
u.password = user.password
|
||||||
end
|
end
|
||||||
@@ -823,10 +823,6 @@ function gen_config_server(node)
|
|||||||
u.password = user.password
|
u.password = user.password
|
||||||
u.uuid = user.uuid
|
u.uuid = user.uuid
|
||||||
end
|
end
|
||||||
if node.protocol == "hysteria2" then
|
|
||||||
u.name = user.username
|
|
||||||
u.password = user.password
|
|
||||||
end
|
|
||||||
if node.protocol == "wireguard" then
|
if node.protocol == "wireguard" then
|
||||||
u.public_key = user.wireguard_public_key
|
u.public_key = user.wireguard_public_key
|
||||||
u.pre_shared_key = user.wireguard_pre_shared_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
|
end
|
||||||
|
|
||||||
function get_headers()
|
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
|
if fs.access(cache_file) then
|
||||||
return luci.sys.exec("cat " .. cache_file)
|
return luci.sys.exec("cat " .. cache_file)
|
||||||
end
|
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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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 |
|
| 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 |
|
| 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 baseclass";
|
||||||
"require ui";
|
"require ui";
|
||||||
|
|
||||||
|
const PALETTE_RECENTS_KEY = "aurora.paletteRecents";
|
||||||
|
|
||||||
return baseclass.extend({
|
return baseclass.extend({
|
||||||
__init__() {
|
__init__() {
|
||||||
ui.menu.load().then((tree) => this.render(tree));
|
ui.menu.load().then((tree) => this.render(tree));
|
||||||
@@ -599,8 +601,12 @@ return baseclass.extend({
|
|||||||
if (!toggle || this.paletteIndex) return;
|
if (!toggle || this.paletteIndex) return;
|
||||||
|
|
||||||
this.paletteIndex = [];
|
this.paletteIndex = [];
|
||||||
|
let logout = null;
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
if (item.isLogout) return;
|
if (item.isLogout) {
|
||||||
|
logout = item;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!item.hasChildren) {
|
if (!item.hasChildren) {
|
||||||
this.paletteIndex.push({
|
this.paletteIndex.push({
|
||||||
title: item.title,
|
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
|
// 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
|
// config app's as above) are used — the theme intentionally ships no
|
||||||
// translations of its own.
|
// translations of its own.
|
||||||
@@ -785,8 +803,15 @@ return baseclass.extend({
|
|||||||
// the clicked anchor is replaced by the re-render, which would strand
|
// the clicked anchor is replaced by the re-render, which would strand
|
||||||
// focus on <body> with the dialog still open.
|
// focus on <body> with the dialog still open.
|
||||||
results.addEventListener("click", (e) => {
|
results.addEventListener("click", (e) => {
|
||||||
const mode = e.target?.closest?.(".cmdk-row")?.dataset.mode;
|
const row = e.target?.closest?.(".cmdk-row");
|
||||||
if (!mode) return;
|
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();
|
e.preventDefault();
|
||||||
setTheme(mode);
|
setTheme(mode);
|
||||||
syncSwitchers();
|
syncSwitchers();
|
||||||
@@ -876,6 +901,46 @@ return baseclass.extend({
|
|||||||
|
|
||||||
// Empty query matches everything at score 0 (the browse list); title hits
|
// Empty query matches everything at score 0 (the browse list); title hits
|
||||||
// outrank name/path and group hits and are the only ones highlighted.
|
// 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) {
|
matchPaletteEntry(q, page) {
|
||||||
if (!q) return { score: 0, ranges: null };
|
if (!q) return { score: 0, ranges: null };
|
||||||
|
|
||||||
@@ -888,23 +953,37 @@ return baseclass.extend({
|
|||||||
return rest && { score: rest.score, ranges: null };
|
return rest && { score: rest.score, ranges: null };
|
||||||
},
|
},
|
||||||
|
|
||||||
renderPaletteResults(value) {
|
collectPaletteMatches(value) {
|
||||||
let q = value.trim().toLowerCase();
|
let q = value.trim().toLowerCase();
|
||||||
// A leading ">" scopes the list to the command rows (the prototype's
|
// A leading ">" scopes the list to the command rows (the prototype's
|
||||||
// command-only mode); ">" covers CJK IMEs emitting the full-width form.
|
// command-only mode); ">" covers CJK IMEs emitting the full-width form.
|
||||||
const cmdOnly = q[0] === ">" || q[0] === ">";
|
const cmdOnly = q[0] === ">" || q[0] === ">";
|
||||||
if (cmdOnly) q = q.slice(1).trim();
|
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 = [];
|
const matches = [];
|
||||||
|
|
||||||
for (const page of this.paletteIndex) {
|
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);
|
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
|
if (q || recents.length) matches.sort((a, b) => b.score - a.score);
|
||||||
// list stays in menu order by skipping the sort entirely).
|
return matches;
|
||||||
if (q) matches.sort((a, b) => b.score - a.score);
|
},
|
||||||
|
|
||||||
|
renderPaletteResults(value) {
|
||||||
|
const matches = this.collectPaletteMatches(value);
|
||||||
|
const theme = localStorage.getItem("aurora.theme") || "device";
|
||||||
|
|
||||||
this.paletteInput.removeAttribute("aria-activedescendant");
|
this.paletteInput.removeAttribute("aria-activedescendant");
|
||||||
this.paletteList.replaceChildren();
|
this.paletteList.replaceChildren();
|
||||||
@@ -926,6 +1005,7 @@ return baseclass.extend({
|
|||||||
href: page.href || "#",
|
href: page.href || "#",
|
||||||
};
|
};
|
||||||
if (page.mode) attributes["data-mode"] = page.mode;
|
if (page.mode) attributes["data-mode"] = page.mode;
|
||||||
|
else if (!page.isLogout) attributes["data-name"] = page.name;
|
||||||
if (current) attributes["aria-current"] = "true";
|
if (current) attributes["aria-current"] = "true";
|
||||||
|
|
||||||
this.paletteList.appendChild(
|
this.paletteList.appendChild(
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ test("production assets stay within raw-transfer budgets", () => {
|
|||||||
// binds.
|
// binds.
|
||||||
assert.ok(main <= 193_000, "main.css exceeds 193 KB");
|
assert.ok(main <= 193_000, "main.css exceeds 193 KB");
|
||||||
assert.ok(login <= 12_000, "login.css exceeds 12 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
|
// 15K: the expiry gate, readonly folding, menu.d node css, wildcard
|
||||||
// actions, progress bar, visibility gate and contract check added ~3 KB
|
// actions, progress bar, visibility gate and contract check added ~3 KB
|
||||||
// over the first cut; the total-transfer budget below moved by the same
|
// 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(router <= 15_000, "router-aurora.js exceeds 15 KB");
|
||||||
assert.ok(logo <= 16_000, "logo.svg exceeds 16 KB");
|
assert.ok(logo <= 16_000, "logo.svg exceeds 16 KB");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
main + menu + router + font + logo <= 267_000,
|
main + menu + router + font + logo <= 267_500,
|
||||||
"admin assets exceed 267 KB",
|
"admin assets exceed 267.5 KB",
|
||||||
);
|
);
|
||||||
assert.ok(login + font + logo <= 55_000, "login assets exceed 55 KB");
|
assert.ok(login + font + logo <= 55_000, "login assets exceed 55 KB");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ const E = (tagName, attributes, children) => ({
|
|||||||
children,
|
children,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadMenuModule = () => {
|
const loadMenuModule = (
|
||||||
|
localStorage = {},
|
||||||
|
{ document = {}, navigator = { platform: "" } } = {},
|
||||||
|
) => {
|
||||||
const baseclass = {
|
const baseclass = {
|
||||||
extend(module) {
|
extend(module) {
|
||||||
return module;
|
return module;
|
||||||
@@ -34,8 +37,9 @@ const loadMenuModule = () => {
|
|||||||
"document",
|
"document",
|
||||||
"window",
|
"window",
|
||||||
"localStorage",
|
"localStorage",
|
||||||
|
"navigator",
|
||||||
source,
|
source,
|
||||||
)(baseclass, ui, E, L, (value) => value, {}, {}, {});
|
)(baseclass, ui, E, L, (value) => value, document, {}, localStorage, navigator);
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu = loadMenuModule();
|
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", () => {
|
test("renders the plain title when there are no ranges", () => {
|
||||||
assert.deepEqual(menu.highlightPaletteMatch("Status", null), ["Status"]);
|
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_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
|
||||||
LUCI_DEPENDS:=+luci-base
|
LUCI_DEPENDS:=+luci-base
|
||||||
|
|
||||||
PKG_VERSION:=1.2.3
|
PKG_VERSION:=1.2.4
|
||||||
PKG_RELEASE:=69
|
PKG_RELEASE:=70
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
|
||||||
LUCI_MINIFY_CSS:=
|
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?=
|
FOOTSTRAP_VERSION?=
|
||||||
ifneq ($(FOOTSTRAP_VERSION),)
|
ifneq ($(FOOTSTRAP_VERSION),)
|
||||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||||
PKG_RELEASE:=29
|
PKG_RELEASE:=30
|
||||||
endif
|
endif
|
||||||
|
|
||||||
LUCI_TITLE:=Footstrap Theme
|
LUCI_TITLE:=Footstrap Theme
|
||||||
|
|||||||
@@ -86,11 +86,53 @@ function fittersEnabled() {
|
|||||||
function run() {
|
function run() {
|
||||||
if (!fittersEnabled()) return;
|
if (!fittersEnabled()) return;
|
||||||
runAll(_fitters, 'fitter');
|
runAll(_fitters, 'fitter');
|
||||||
/* the page is settled now, so this is the position the next mutation must be measured against —
|
/* the page is settled now: this is the height the NEXT tick may not go below, and the position
|
||||||
* unless a correction is already on its way, which would make this reference the drifted one */
|
* 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();
|
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 ----
|
/* ---- 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
|
* 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; }
|
if (scrolling()) { requestAnimationFrame(sampleMotion); return; }
|
||||||
_sampling = false;
|
_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();
|
rememberRest();
|
||||||
/* the page has held still for SCROLL_IDLE: whatever was put off may run now */
|
/* the page has held still for SCROLL_IDLE: whatever was put off may run now */
|
||||||
if (_deferred) {
|
if (_deferred) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=opera-proxy
|
PKG_NAME:=opera-proxy
|
||||||
PKG_VERSION:=1.28.0
|
PKG_VERSION:=1.29.0
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
|
|
||||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
||||||
PKG_LICENSE:=MIT
|
PKG_LICENSE:=MIT
|
||||||
|
|||||||
Reference in New Issue
Block a user