mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 20:34:19 +08:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dee375ce66 | ||
|
|
7c496d5738 | ||
|
|
d3321f4c3c | ||
|
|
f5aa63986c | ||
|
|
a2faaa2a01 | ||
|
|
570b248f8b | ||
|
|
87d47fbcb2 | ||
|
|
380ee71a08 | ||
|
|
8ca86b3a5e | ||
|
|
dbdd70b00d | ||
|
|
e70e8fddee | ||
|
|
02c6c789ba |
@@ -394,7 +394,7 @@ jobs:
|
|||||||
git_sparse_clone master "https://github.com/immortalwrt/packages" net/n2n net/qbittorrent \
|
git_sparse_clone master "https://github.com/immortalwrt/packages" net/n2n net/qbittorrent \
|
||||||
net/amule net/cdnspeedtest net/minieap net/ddns-go net/sysuh3c net/3proxy utils/filebrowser net/cloudreve \
|
net/amule net/cdnspeedtest net/minieap net/ddns-go net/sysuh3c net/3proxy utils/filebrowser net/cloudreve \
|
||||||
net/go-nats net/go-wol net/bitsrunlogin-go net/transfer net/udp2raw net/msd_lite \
|
net/go-nats net/go-wol net/bitsrunlogin-go net/transfer net/udp2raw net/msd_lite \
|
||||||
net/subconverter net/ngrokc net/scutclient net/gost net/ua2f net/dufs net/frp net/qBittorrent-Enhanced-Edition \
|
net/subconverter net/ngrokc net/scutclient net/gost net/ua2f net/dufs net/qBittorrent-Enhanced-Edition \
|
||||||
net/tinyportmapper net/nexttrace net/rustdesk-server net/tuic-server net/transmission-web-control \
|
net/tinyportmapper net/nexttrace net/rustdesk-server net/tuic-server net/transmission-web-control \
|
||||||
net/ipset-lists net/ShadowVPN net/nps net/dnsforwarder \
|
net/ipset-lists net/ShadowVPN net/nps net/dnsforwarder \
|
||||||
net/ps3netsrv net/brook net/q \
|
net/ps3netsrv net/brook net/q \
|
||||||
@@ -405,7 +405,7 @@ jobs:
|
|||||||
utils/phicomm-k3screenctrl utils/joker utils/7z utils/dhrystone utils/supervisor utils/tinymembench utils/pcat-mgr utils/fan2go \
|
utils/phicomm-k3screenctrl utils/joker utils/7z utils/dhrystone utils/supervisor utils/tinymembench utils/pcat-mgr utils/fan2go \
|
||||||
utils/coremark utils/watchcat multimedia/you-get multimedia/lux multimedia/gmediarender multimedia/ykdl multimedia/gallery-dl \
|
utils/coremark utils/watchcat multimedia/you-get multimedia/lux multimedia/gmediarender multimedia/ykdl multimedia/gallery-dl \
|
||||||
sound/spotifyd devel/go-rice devel/rust-bindgen admin/gotop \
|
sound/spotifyd devel/go-rice devel/rust-bindgen admin/gotop \
|
||||||
lang/lua/lua-periphery lang/lua/lua-neturl lang/lua/lua-maxminddb lang/golang devel/gn
|
lang/lua/lua-periphery lang/lua/lua-neturl lang/lua/lua-maxminddb lang/golang devel/gn lang/node/node-pnpm
|
||||||
git_clone https://github.com/koshev-msk/modemfeed && mv -n modemfeed/*/!(telephony)/!(ookla-speedtest) ./; rm -rf modemfeed
|
git_clone https://github.com/koshev-msk/modemfeed && mv -n modemfeed/*/!(telephony)/!(ookla-speedtest) ./; rm -rf modemfeed
|
||||||
git_sparse_clone openwrt-25.12 "https://github.com/immortalwrt/luci" applications collections/luci-nginx protocols/luci-proto-minieap protocols/luci-proto-quectel
|
git_sparse_clone openwrt-25.12 "https://github.com/immortalwrt/luci" applications collections/luci-nginx protocols/luci-proto-minieap protocols/luci-proto-quectel
|
||||||
mv -f applications luciapp && rm -rf luciapp/luci-app-firewall
|
mv -f applications luciapp && rm -rf luciapp/luci-app-firewall
|
||||||
|
|||||||
+52
-35
@@ -1,47 +1,43 @@
|
|||||||
#
|
|
||||||
# Copyright (C) 2017-2024
|
|
||||||
#
|
|
||||||
# This is free software, licensed under the GNU General Public License v2.
|
|
||||||
#
|
|
||||||
|
|
||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=filebrowser-q
|
PKG_NAME:=filebrowser-q
|
||||||
PKG_VERSION:=1.5.6-stable
|
PKG_VERSION:=1.5.6-stable
|
||||||
PKG_RELEASE=1
|
PKG_RELEASE:=5
|
||||||
|
|
||||||
ifeq ($(ARCH),aarch64)
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_ARCH:=arm64
|
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
else ifeq ($(ARCH),arm)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/filebrowser-$(PKG_VERSION)
|
||||||
PKG_ARCH:=armv7
|
|
||||||
PKG_HASH:=skip
|
PKG_LICENSE:=Apache-2.0
|
||||||
else ifeq ($(ARCH),x86_64)
|
PKG_LICENSE_FILES:=LICENSE
|
||||||
PKG_ARCH:=amd64
|
PKG_MAINTAINER:=kiddin9
|
||||||
PKG_HASH:=skip
|
|
||||||
endif
|
PKG_BUILD_DEPENDS:=golang/host node/host
|
||||||
|
PKG_BUILD_PARALLEL:=1
|
||||||
|
PKG_BUILD_FLAGS:=no-mips16
|
||||||
|
|
||||||
|
GO_PKG:=github.com/gtsteffaniak/filebrowser/backend
|
||||||
|
GO_PKG_BUILD_DIR:=$(PKG_BUILD_DIR)/backend
|
||||||
|
GO_PKG_LDFLAGS_X:= \
|
||||||
|
$(GO_PKG)/internal/version.Version=v$(PKG_VERSION) \
|
||||||
|
$(GO_PKG)/internal/version.CommitSHA=$(PKG_VERSION)
|
||||||
|
|
||||||
include $(INCLUDE_DIR)/package.mk
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||||
|
|
||||||
define Package/filebrowser-q
|
define Package/filebrowser-q
|
||||||
SECTION:=net
|
SECTION:=utils
|
||||||
CATEGORY:=Network
|
CATEGORY:=Utilities
|
||||||
DEPENDS:=@(arm||aarch64||x86_64)
|
SUBMENU:=Filesystem
|
||||||
TITLE:=FileBrowser Quantum
|
TITLE:=FileBrowser Quantum - Modern Web File Manager
|
||||||
|
URL:=https://github.com/gtsteffaniak/filebrowser
|
||||||
|
DEPENDS:=$(GO_ARCH_DEPENDS)
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/filebrowser-q/description
|
define Package/filebrowser-q/description
|
||||||
The best free self-hosted web-based file manager.
|
FileBrowser Quantum provides a modern, responsive web-based file management
|
||||||
endef
|
interface with multi-source, real-time search, and enhanced preview features.
|
||||||
|
|
||||||
PKG_SOURCE:=linux-$(PKG_ARCH)-filebrowser
|
|
||||||
PKG_SOURCE_URL:=https://github.com/gtsteffaniak/filebrowser/releases/download/v$(PKG_VERSION)
|
|
||||||
|
|
||||||
define Build/Prepare
|
|
||||||
$(call Build/Prepare/Default)
|
|
||||||
endef
|
|
||||||
|
|
||||||
define Build/Compile
|
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/filebrowser-q/conffiles
|
define Package/filebrowser-q/conffiles
|
||||||
@@ -49,13 +45,34 @@ define Package/filebrowser-q/conffiles
|
|||||||
/etc/config/filebrowser-q
|
/etc/config/filebrowser-q
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
define Build/Prepare
|
||||||
|
$(call Build/Prepare/Default)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
( \
|
||||||
|
pushd $(PKG_BUILD_DIR)/frontend && \
|
||||||
|
npm install && \
|
||||||
|
npm run build ; \
|
||||||
|
)
|
||||||
|
( \
|
||||||
|
cd $(PKG_BUILD_DIR)/backend && \
|
||||||
|
$(GO_PKG_VARS) \
|
||||||
|
go build \
|
||||||
|
-trimpath \
|
||||||
|
-ldflags="-w -s" \
|
||||||
|
-o $(PKG_BUILD_DIR)/filebrowser . ; \
|
||||||
|
)
|
||||||
|
endef
|
||||||
|
|
||||||
define Package/filebrowser-q/install
|
define Package/filebrowser-q/install
|
||||||
$(INSTALL_DIR) $(1)/etc/init.d
|
$(INSTALL_DIR) $(1)/usr/bin
|
||||||
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser-q
|
$(INSTALL_BIN) $(PKG_BUILD_DIR)/filebrowser $(1)/usr/bin/filebrowser-q
|
||||||
|
|
||||||
$(INSTALL_DIR) $(1)/etc/config
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser-q
|
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser-q
|
||||||
$(INSTALL_DIR) $(1)/usr/bin/
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
$(INSTALL_BIN) $(DL_DIR)/linux-$(PKG_ARCH)-filebrowser $(1)/usr/bin/filebrowser-q
|
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser-q
|
||||||
endef
|
endef
|
||||||
|
|
||||||
$(eval $(call BuildPackage,filebrowser-q))
|
$(eval $(call BuildPackage,filebrowser-q))
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
server:
|
|
||||||
port: 8989
|
|
||||||
database: "/etc/filebrowser-q/database.db"
|
|
||||||
sources:
|
|
||||||
- path: "/" # Do not use a root "/" directory or include the "/var" folder
|
|
||||||
config:
|
|
||||||
defaultEnabled: true
|
|
||||||
auth:
|
|
||||||
adminUsername: admin
|
|
||||||
adminPassword: "admin"
|
|
||||||
userDefaults:
|
|
||||||
ui:
|
|
||||||
locale: "zhCN"
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
config filebrowser 'config'
|
config filebrowser 'config'
|
||||||
option enabled '0'
|
option enabled '0'
|
||||||
option listen_port '8989'
|
option listen_port '8787'
|
||||||
option root_path '/'
|
option root_path '/'
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ start() {
|
|||||||
[ "$enabled" -eq "1" ] || return 1
|
[ "$enabled" -eq "1" ] || return 1
|
||||||
|
|
||||||
local listen_port root_path root_name
|
local listen_port root_path root_name
|
||||||
config_get listen_port "config" "listen_port" "8989"
|
config_get listen_port "config" "listen_port" "8787"
|
||||||
config_get root_path "config" "root_path" "/"
|
config_get root_path "config" "root_path" "/"
|
||||||
|
|
||||||
# 路径为 / 时 name 设为 root,否则留空
|
# 路径为 / 时 name 设为 root,否则留空
|
||||||
@@ -43,9 +43,9 @@ userDefaults:
|
|||||||
EOF
|
EOF
|
||||||
else
|
else
|
||||||
grep -q " name:" "$CONF_PATH" || sed -i "s,.*- path:.*,&\n name: \"\"," "$CONF_PATH"
|
grep -q " name:" "$CONF_PATH" || sed -i "s,.*- path:.*,&\n name: \"\"," "$CONF_PATH"
|
||||||
sed -e "s/.*port:.*/ port: $listen_port/" \
|
sed -e "s/ port:.*/ port: $listen_port/" \
|
||||||
-e "s,.*- path:.*, - path: \"$root_path\"," \
|
-e "s, - path:.*, - path: \"$root_path\"," \
|
||||||
-e "s/.*name:.*/ name: \"$root_name\"/" \
|
-e "s/ name: \".*\"/ name: \"$root_name\"/" \
|
||||||
-i "$CONF_PATH"
|
-i "$CONF_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=filebrowser
|
PKG_NAME:=filebrowser
|
||||||
PKG_VERSION:=2.63.23
|
PKG_VERSION:=2.63.23
|
||||||
PKG_RELEASE:=21
|
PKG_RELEASE:=23
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=frp
|
PKG_NAME:=frp
|
||||||
PKG_VERSION:=0.70.1
|
PKG_VERSION:=0.71.0
|
||||||
PKG_RELEASE:=5
|
PKG_RELEASE:=6
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/fatedier/frp/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/fatedier/frp/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ return view.extend({
|
|||||||
|
|
||||||
o = s.option(form.Value, 'listen_port', _('Listen port'));
|
o = s.option(form.Value, 'listen_port', _('Listen port'));
|
||||||
o.datatype = 'port';
|
o.datatype = 'port';
|
||||||
o.default = '8989';
|
o.default = '8787';
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
|
|
||||||
o = s.option(form.Value, 'root_path', _('Root directory'));
|
o = s.option(form.Value, 'root_path', _('Root directory'));
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall2
|
PKG_NAME:=luci-app-passwall2
|
||||||
PKG_VERSION:=26.9.2
|
PKG_VERSION:=26.9.2
|
||||||
PKG_RELEASE:=111
|
PKG_RELEASE:=112
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -272,9 +272,17 @@ o = s:option(ListValue, "remote_dns_protocol", translate("Remote DNS Protocol"))
|
|||||||
o:value("tcp", "TCP")
|
o:value("tcp", "TCP")
|
||||||
o:value("doh", "DoH")
|
o:value("doh", "DoH")
|
||||||
o:value("udp", "UDP")
|
o:value("udp", "UDP")
|
||||||
|
if m.is_js_luci then
|
||||||
|
if current_node.type == "sing-box" then
|
||||||
|
o:value("tls", "TLS(DoT)")
|
||||||
|
o:value("quic", "QUIC(DoQ)")
|
||||||
|
o:value("http3", "HTTP3(DoH3)")
|
||||||
|
end
|
||||||
|
else
|
||||||
o:value("tls", "TLS(DoT)", { _is_singbox = "1" })
|
o:value("tls", "TLS(DoT)", { _is_singbox = "1" })
|
||||||
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" })
|
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" })
|
||||||
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" })
|
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" })
|
||||||
|
end
|
||||||
o:depends("_show_dns_option", "1")
|
o:depends("_show_dns_option", "1")
|
||||||
|
|
||||||
---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ)
|
---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ)
|
||||||
@@ -382,9 +390,8 @@ for k, v in pairs(nodes_table) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--m:appendTemplate("/acl/options", {section = arg[1]})
|
--[[
|
||||||
|
-- Shunt
|
||||||
-- Shunt Start
|
|
||||||
if current_node.protocol == "_shunt" then
|
if current_node.protocol == "_shunt" then
|
||||||
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua")
|
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua")
|
||||||
setfenv(shunt_lua, getfenv(1))(m, s, {
|
setfenv(shunt_lua, getfenv(1))(m, s, {
|
||||||
@@ -396,5 +403,6 @@ if current_node.protocol == "_shunt" then
|
|||||||
end
|
end
|
||||||
|
|
||||||
m:appendTemplate("/acl/shunt", { shunt_list = api.jsonc.stringify(shunt_list), section = s.section })
|
m:appendTemplate("/acl/shunt", { shunt_list = api.jsonc.stringify(shunt_list), section = s.section })
|
||||||
|
]]--
|
||||||
|
|
||||||
return api.return_map(m)
|
return api.return_map(m)
|
||||||
|
|||||||
@@ -201,9 +201,17 @@ o = s:taboption("DNS", ListValue, "remote_dns_protocol", translate("Remote DNS P
|
|||||||
o:value("tcp", "TCP")
|
o:value("tcp", "TCP")
|
||||||
o:value("doh", "DoH")
|
o:value("doh", "DoH")
|
||||||
o:value("udp", "UDP")
|
o:value("udp", "UDP")
|
||||||
|
if m.is_js_luci then
|
||||||
|
if current_node.type == "sing-box" then
|
||||||
|
o:value("tls", "TLS(DoT)")
|
||||||
|
o:value("quic", "QUIC(DoQ)")
|
||||||
|
o:value("http3", "HTTP3(DoH3)")
|
||||||
|
end
|
||||||
|
else
|
||||||
o:value("tls", "TLS(DoT)", { _is_singbox = "1" })
|
o:value("tls", "TLS(DoT)", { _is_singbox = "1" })
|
||||||
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" })
|
o:value("quic", "QUIC(DoQ)", { _is_singbox = "1" })
|
||||||
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" })
|
o:value("http3", "HTTP3(DoH3)", { _is_singbox = "1" })
|
||||||
|
end
|
||||||
|
|
||||||
---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ)
|
---- DNS over TCP or UDP or TLS (DoT) or QUIC (DoQ)
|
||||||
o = s:taboption("DNS", Value, "remote_dns", translate("Remote DNS"))
|
o = s:taboption("DNS", Value, "remote_dns", translate("Remote DNS"))
|
||||||
@@ -259,11 +267,10 @@ o:value("UseIP")
|
|||||||
o:value("UseIPv4")
|
o:value("UseIPv4")
|
||||||
o:value("UseIPv6")
|
o:value("UseIPv6")
|
||||||
|
|
||||||
if current_node.type == "sing-box" then
|
|
||||||
o = s:taboption("DNS", Value, "remote_rewrite_ttl", translate("Remote DNS") .. " TTL")
|
o = s:taboption("DNS", Value, "remote_rewrite_ttl", translate("Remote DNS") .. " TTL")
|
||||||
o.datatype = "min(1)"
|
o.datatype = "min(1)"
|
||||||
o.default = "30"
|
o.default = "30"
|
||||||
end
|
o:depends("_is_singbox", "1")
|
||||||
|
|
||||||
o = s:taboption("DNS", TextValue, "dns_hosts", translate("Domain Override"))
|
o = s:taboption("DNS", TextValue, "dns_hosts", translate("Domain Override"))
|
||||||
o.rows = 5
|
o.rows = 5
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
<%
|
|
||||||
local map = self.map
|
|
||||||
local api = map.api
|
|
||||||
local config = map.config
|
|
||||||
local section = self.section
|
|
||||||
-%>
|
|
||||||
<script type="text/javascript">
|
|
||||||
//<![CDATA[
|
|
||||||
function setOption(option, val) {
|
|
||||||
const dom = document.getElementById('cbid.<%=config%>.<%=section%>.' + option);
|
|
||||||
if (dom) {
|
|
||||||
dom.value = val;
|
|
||||||
}
|
|
||||||
const combobox_dom = document.getElementById('cbi.combobox.cbid.<%=config%>.<%=section%>.' + option);
|
|
||||||
if (combobox_dom) {
|
|
||||||
combobox_dom.value = val;
|
|
||||||
//combobox_dom.selectedIndex = val;
|
|
||||||
//combobox_dom.dispatchEvent(new Event("change", { bubbles: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
function dom_event() {
|
|
||||||
waitForElementId('cbi.combobox.cbid.<%=config%>.<%=section%>.tcp_no_redir_ports', function(el) {
|
|
||||||
const o_val = el.value;
|
|
||||||
el.addEventListener("change", () => {
|
|
||||||
const udp_no_redir_ports = getOption("<%=config%>", "<%=section%>", "udp_no_redir_ports");
|
|
||||||
if (el.value == "1:65535" && udp_no_redir_ports && udp_no_redir_ports.value == "1:65535") {
|
|
||||||
setOption("tcp_no_redir_ports", "");
|
|
||||||
setOption("udp_no_redir_ports", "");
|
|
||||||
setOption("mode", 0);
|
|
||||||
cbi_d_update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
waitForElementId('cbi.combobox.cbid.<%=config%>.<%=section%>.udp_no_redir_ports', function(el) {
|
|
||||||
const o_val = el.value;
|
|
||||||
el.addEventListener("change", () => {
|
|
||||||
const tcp_no_redir_ports = getOption("<%=config%>", "<%=section%>", "tcp_no_redir_ports");
|
|
||||||
if (el.value == "1:65535" && tcp_no_redir_ports && tcp_no_redir_ports.value == "1:65535") {
|
|
||||||
setOption("tcp_no_redir_ports", "");
|
|
||||||
setOption("udp_no_redir_ports", "");
|
|
||||||
setOption("mode", 0);
|
|
||||||
cbi_d_update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const ori_cbi_d_update = cbi_d_update;
|
|
||||||
cbi_d_update = function() {
|
|
||||||
ori_cbi_d_update();
|
|
||||||
dom_event();
|
|
||||||
};
|
|
||||||
dom_event();
|
|
||||||
});
|
|
||||||
//]]>
|
|
||||||
</script>
|
|
||||||
@@ -1507,15 +1507,17 @@ table td, .table .td {
|
|||||||
|
|
||||||
//Node list option saving logic
|
//Node list option saving logic
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
function onChange(option, value) {
|
function onChange(option, value, refresh) {
|
||||||
ajax.abortAll();
|
ajax.abortAll();
|
||||||
XHR.get('<%=api.url("save_node_list_opt")%>', {
|
XHR.get('<%=api.url("save_node_list_opt")%>', {
|
||||||
option: option,
|
option: option,
|
||||||
value: value
|
value: value
|
||||||
}, function(x) {
|
}, function(x) {
|
||||||
if (x && x.status == 200) {
|
if (x && x.status == 200) {
|
||||||
|
if (refresh) {
|
||||||
document.getElementById("node_list").innerHTML = "";
|
document.getElementById("node_list").innerHTML = "";
|
||||||
loadNodeList();
|
loadNodeList();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
alert("<%:Error%>");
|
alert("<%:Error%>");
|
||||||
}
|
}
|
||||||
@@ -1527,7 +1529,7 @@ table td, .table .td {
|
|||||||
el.addEventListener("change", () => {
|
el.addEventListener("change", () => {
|
||||||
el.blur();
|
el.blur();
|
||||||
show_node_info = el.checked ? "1" : "0";
|
show_node_info = el.checked ? "1" : "0";
|
||||||
onChange("show_node_info", show_node_info);
|
onChange("show_node_info", show_node_info, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1535,32 +1537,47 @@ table td, .table .td {
|
|||||||
el.addEventListener("change", () => {
|
el.addEventListener("change", () => {
|
||||||
el.blur();
|
el.blur();
|
||||||
auto_detection_time = el.value;
|
auto_detection_time = el.value;
|
||||||
onChange("auto_detection_time", auto_detection_time);
|
onChange("auto_detection_time", auto_detection_time, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
<% if api.is_js_luci() then -%>
|
||||||
|
waitForElement('div[id*="cbid.<%=appname%>"][id*="url_test_url"]', function(el) {
|
||||||
|
el.addEventListener("cbi-dropdown-change", () => {
|
||||||
|
if (el.value && (!el.new_val || el.new_val != el.value)) {
|
||||||
|
onChange("url_test_url", el.value, false);
|
||||||
|
el.new_val = el.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
<% else -%>
|
||||||
waitForElement('select[id*="<%=appname%>"][id*="url_test_url"]', function(el) {
|
waitForElement('select[id*="<%=appname%>"][id*="url_test_url"]', function(el) {
|
||||||
el.addEventListener("change", () => {
|
el.addEventListener("change", () => {
|
||||||
if (el.value) {
|
if (el.value && (!el.new_val || el.new_val != el.value)) {
|
||||||
onChange("url_test_url", el.value);
|
onChange("url_test_url", el.value, false);
|
||||||
|
el.new_val = el.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
waitForElement('input[id*="<%=appname%>"][id*="url_test_url"]', function(el) {
|
waitForElement('input[id*="<%=appname%>"][id*="url_test_url"]', function(el) {
|
||||||
el.addEventListener("change", () => {
|
el.addEventListener("change", () => {
|
||||||
if (el.value) {
|
if (el.value && (!el.new_val || el.new_val != el.value)) {
|
||||||
onChange("url_test_url", el.value);
|
onChange("url_test_url", el.value, false);
|
||||||
|
el.new_val = el.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
<% end -%>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<% if not api.is_js_luci() then -%>
|
||||||
const ori_cbi_d_update = cbi_d_update;
|
const ori_cbi_d_update = cbi_d_update;
|
||||||
cbi_d_update = function() {
|
cbi_d_update = function() {
|
||||||
ori_cbi_d_update();
|
ori_cbi_d_update();
|
||||||
dom_event();
|
dom_event();
|
||||||
};
|
};
|
||||||
|
<% end -%>
|
||||||
dom_event();
|
dom_event();
|
||||||
|
|
||||||
const links = document.querySelectorAll('a');
|
const links = document.querySelectorAll('a');
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-wwand
|
PKG_NAME:=luci-app-wwand
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
|
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
|
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
|
||||||
PKG_SOURCE_VERSION:=88a711251296810ea3428d32a3da9c5b635cea0c
|
PKG_SOURCE_VERSION:=bd773d76198d2a92063b349ed4f1e1f75475857c
|
||||||
PKG_SOURCE_DATE:=2026-08-31
|
PKG_SOURCE_DATE:=2026-09-05
|
||||||
PKG_MIRROR_HASH:=skip
|
PKG_MIRROR_HASH:=skip
|
||||||
|
|
||||||
PKG_LICENSE:=GPL-2.0-only
|
PKG_LICENSE:=GPL-2.0-only
|
||||||
|
|||||||
@@ -82,6 +82,24 @@ function buildIwinfoDeviceLookup(devices) {
|
|||||||
return lookup;
|
return lookup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getQcaIwinfoDevicesFromConfig() {
|
||||||
|
const radios = uci.sections('wireless', 'wifi-device');
|
||||||
|
const devices = [];
|
||||||
|
|
||||||
|
if (!radios.length || !radios.every((radio) => isQcaWifiHwtype(radio.type)))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
for (const iface of uci.sections('wireless', 'wifi-iface')) {
|
||||||
|
if (isConfigWifiIfaceDisabled(iface) ||
|
||||||
|
!isQcaWifiHwtype(uci.get('wireless', iface.device, 'type')))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
pushUnique(devices, iface.ifname || getQcaFallbackIfname(iface.device, iface['.name']));
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
function getWifiNetIdBySection(section) {
|
function getWifiNetIdBySection(section) {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
@@ -130,7 +148,7 @@ function parseIwDevInfoTxPower(stdout) {
|
|||||||
return (!isNaN(value) && value > 0) ? value : null;
|
return (!isNaN(value) && value > 0) ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildIwinfoResolver(devices) {
|
function buildIwinfoResolver(devices, configOnly) {
|
||||||
const deviceLookup = buildIwinfoDeviceLookup(devices);
|
const deviceLookup = buildIwinfoDeviceLookup(devices);
|
||||||
const aliasMap = Object.create(null);
|
const aliasMap = Object.create(null);
|
||||||
const queryTargets = [];
|
const queryTargets = [];
|
||||||
@@ -190,7 +208,8 @@ function buildIwinfoResolver(devices) {
|
|||||||
return {
|
return {
|
||||||
deviceLookup,
|
deviceLookup,
|
||||||
aliasMap,
|
aliasMap,
|
||||||
queryTargets
|
queryTargets,
|
||||||
|
configOnly
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,8 +220,13 @@ function loadIwinfoResolver(force) {
|
|||||||
if (!force && cachedIwinfoResolver != null)
|
if (!force && cachedIwinfoResolver != null)
|
||||||
return Promise.resolve(cachedIwinfoResolver);
|
return Promise.resolve(cachedIwinfoResolver);
|
||||||
|
|
||||||
cachedIwinfoResolverPromise = L.resolveDefault(callIwinfoDevices(), {}).then((res) => {
|
const qcaDevices = getQcaIwinfoDevicesFromConfig();
|
||||||
cachedIwinfoResolver = buildIwinfoResolver(res?.devices);
|
const deviceRequest = qcaDevices != null
|
||||||
|
? Promise.resolve({ devices: qcaDevices })
|
||||||
|
: L.resolveDefault(callIwinfoDevices(), {});
|
||||||
|
|
||||||
|
cachedIwinfoResolverPromise = deviceRequest.then((res) => {
|
||||||
|
cachedIwinfoResolver = buildIwinfoResolver(res?.devices, qcaDevices != null);
|
||||||
cachedIwinfoResolverPromise = null;
|
cachedIwinfoResolverPromise = null;
|
||||||
return cachedIwinfoResolver;
|
return cachedIwinfoResolver;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
@@ -214,7 +238,8 @@ function loadIwinfoResolver(force) {
|
|||||||
cachedIwinfoResolver = {
|
cachedIwinfoResolver = {
|
||||||
deviceLookup: Object.create(null),
|
deviceLookup: Object.create(null),
|
||||||
aliasMap: Object.create(null),
|
aliasMap: Object.create(null),
|
||||||
queryTargets: []
|
queryTargets: [],
|
||||||
|
configOnly: false
|
||||||
};
|
};
|
||||||
|
|
||||||
return cachedIwinfoResolver;
|
return cachedIwinfoResolver;
|
||||||
@@ -231,7 +256,9 @@ function loadIwinfoInfoMap(force) {
|
|||||||
return Promise.resolve(cachedIwinfoInfoMap);
|
return Promise.resolve(cachedIwinfoInfoMap);
|
||||||
|
|
||||||
cachedIwinfoInfoPromise = loadIwinfoResolver(force).then((resolver) => {
|
cachedIwinfoInfoPromise = loadIwinfoResolver(force).then((resolver) => {
|
||||||
const queryTargets = resolver.queryTargets.length ? resolver.queryTargets : getLegacyIwinfoProbeTargets();
|
const queryTargets = (resolver.configOnly || resolver.queryTargets.length)
|
||||||
|
? resolver.queryTargets
|
||||||
|
: getLegacyIwinfoProbeTargets();
|
||||||
|
|
||||||
return Promise.all(queryTargets.map((name) =>
|
return Promise.all(queryTargets.map((name) =>
|
||||||
L.resolveDefault(callIwinfoInfoCompat(name), null).then((info) => [ name, info ])
|
L.resolveDefault(callIwinfoInfoCompat(name), null).then((info) => [ name, info ])
|
||||||
|
|||||||
@@ -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:=60
|
PKG_RELEASE:=61
|
||||||
endif
|
endif
|
||||||
|
|
||||||
LUCI_TITLE:=Footstrap Theme
|
LUCI_TITLE:=Footstrap Theme
|
||||||
|
|||||||
@@ -480,6 +480,35 @@ function wireRail() {
|
|||||||
* stays in the label for screen readers, and in `title` for the pointer. */
|
* stays in the label for screen readers, and in `title` for the pointer. */
|
||||||
const IND_DOT = '•';
|
const IND_DOT = '•';
|
||||||
|
|
||||||
|
/* Idempotent attribute write, so a poll tick that finds nothing changed touches no DOM — same
|
||||||
|
* shape as `fsSyncAttr` in menu-footstrap-common.js, restated rather than imported (that file does
|
||||||
|
* not export it). */
|
||||||
|
function syncIndAttr(el, name, value) {
|
||||||
|
if (value === null) {
|
||||||
|
if (el.hasAttribute(name)) el.removeAttribute(name);
|
||||||
|
} else if (el.getAttribute(name) !== value) {
|
||||||
|
el.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A CLICKABLE `[data-indicator]` (the poll pill, "Unsaved Changes: N", …) ships as a bare
|
||||||
|
* span: no role, name or tabindex, so Tab skips it and a screen reader
|
||||||
|
* announces a run of text with no name, role or state (WCAG 2.1.1, 4.1.2). ui.showIndicator's own
|
||||||
|
* click handler already lives on this exact element — this only adds the second, W3C-APG way to
|
||||||
|
* reach it; it does not add a competing one. The name is the pill's own prose ("Refreshing"),
|
||||||
|
* never invented. Enter/Space call el.click() because a <span>, unlike an <a>, gets neither key
|
||||||
|
* for free (contrast fs-widgets.js's wireSpaceKey, written for an <a role="button">); calling
|
||||||
|
* click() cannot double-fire the mouse handler, since a keydown is not a click. */
|
||||||
|
function wireIndicatorKeyboard(el) {
|
||||||
|
if (el.dataset.fsWired) return;
|
||||||
|
el.dataset.fsWired = '1';
|
||||||
|
el.addEventListener('keydown', (ev) => {
|
||||||
|
if (ev.key !== 'Enter' && ev.key !== ' ' && ev.key !== 'Spacebar') return;
|
||||||
|
ev.preventDefault();
|
||||||
|
el.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function wireIndicatorCounts() {
|
function wireIndicatorCounts() {
|
||||||
const box = document.getElementById('indicators');
|
const box = document.getElementById('indicators');
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
@@ -491,6 +520,18 @@ function wireIndicatorCounts() {
|
|||||||
el.setAttribute('data-fs-badge', m ? m[1] : IND_DOT);
|
el.setAttribute('data-fs-badge', m ? m[1] : IND_DOT);
|
||||||
/* the rail hides the prose; the tooltip is where it stays reachable by pointer */
|
/* the rail hides the prose; the tooltip is where it stays reachable by pointer */
|
||||||
el.setAttribute('title', txt);
|
el.setAttribute('title', txt);
|
||||||
|
/* Only a CLICKABLE indicator becomes a control. `ui.showIndicator` sets
|
||||||
|
* `data-clickable` only when it was handed a handler, and a status-only pill is
|
||||||
|
* given none (luci-base ui.js; `battstatus.js` calls it with a null handler) — a
|
||||||
|
* `role="button"` tabstop with nothing behind it is worse than the bare span it
|
||||||
|
* replaces. The ring and the pointer cursor in theme/20-shell.css are scoped to
|
||||||
|
* `[data-clickable]` for the same reason; the two must not disagree. */
|
||||||
|
if (el.hasAttribute('data-clickable')) {
|
||||||
|
syncIndAttr(el, 'role', 'button');
|
||||||
|
syncIndAttr(el, 'tabindex', '0');
|
||||||
|
syncIndAttr(el, 'aria-label', txt.trim() || null);
|
||||||
|
wireIndicatorKeyboard(el);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
'require dom';
|
'require dom';
|
||||||
'require network';
|
'require network';
|
||||||
'require fs-fit as fit';
|
'require fs-fit as fit';
|
||||||
|
'require menu-footstrap-common as common';
|
||||||
|
|
||||||
/* Overview layout only: renders nothing of its own, it re-arranges the STOCK System / Memory /
|
/* Overview layout only: renders nothing of its own, it re-arranges the STOCK System / Memory /
|
||||||
* Storage sections into a grid. Content, data and styling stay luci-mod-status's — rendering a
|
* Storage sections into a grid. Content, data and styling stay luci-mod-status's — rendering a
|
||||||
@@ -23,11 +24,15 @@ const ROLES = { [_('System')]: 'sys', [_('Memory')]: 'mem', [_('Storage')]: 'sto
|
|||||||
/* the data-page value four call sites compare against; a string literal is not mangled, so a
|
/* the data-page value four call sites compare against; a string literal is not mangled, so a
|
||||||
* repeat is paid in full on flash every time (measured: 24 B x4 -> 37 B, 59 B saved) */
|
* repeat is paid in full on flash every time (measured: 24 B x4 -> 37 B, 59 B saved) */
|
||||||
|
|
||||||
function sectionTitle(sec) {
|
function headerEl(sec) {
|
||||||
/* two title markups, one per release: 25.12 wraps the heading (`.cbi-title > h3`), 24.10 emits
|
/* two title markups, one per release: 25.12 wraps the heading (`.cbi-title > h3`), 24.10 emits
|
||||||
* a bare `<h3>` as the section's first child. Matching only one silently disables the grid on
|
* a bare `<h3>` as the section's first child. Matching only one silently disables the grid on
|
||||||
* the other. */
|
* the other. */
|
||||||
const h = sec.querySelector('.cbi-title h3, :scope > h3');
|
return sec.querySelector('.cbi-title h3, :scope > h3');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionTitle(sec) {
|
||||||
|
const h = headerEl(sec);
|
||||||
if (!h) return '';
|
if (!h) return '';
|
||||||
/* the first non-empty TEXT node, not `firstChild`: 25.12 appends a hide/show <span> inside the
|
/* the first non-empty TEXT node, not `firstChild`: 25.12 appends a hide/show <span> inside the
|
||||||
* same <h3>, so `firstChild` depends on upstream keeping the words first */
|
* same <h3>, so `firstChild` depends on upstream keeping the words first */
|
||||||
@@ -39,6 +44,101 @@ function sectionTitle(sec) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- keyboard disclosure: the card header becomes the toggle, the pill becomes its glyph ----
|
||||||
|
*
|
||||||
|
* Live on owrt2512 (25.12.4), Status -> Overview carries 14 `[data-clickable]` elements — the
|
||||||
|
* topbar poll pill plus one Hide/Show toggle per card — and every one is a bare <span>: no
|
||||||
|
* tabindex, no role, no aria-expanded. Tab never reaches one and a screen reader announces a run
|
||||||
|
* of text with no name, role or state. WCAG 2.1.1 Keyboard (A), 4.1.2 Name, Role, Value (A).
|
||||||
|
* docs/findings.md, "A card cannot be collapsed from the keyboard".
|
||||||
|
*
|
||||||
|
* index.js's own pill keeps doing the actual show/hide — untouched, so mouse behaviour for anyone
|
||||||
|
* clicking it does not change. The header becomes a second, W3C-APG way to reach the SAME handler,
|
||||||
|
* not a competing one. */
|
||||||
|
|
||||||
|
/* Idempotent attribute write, so a poll tick that finds nothing changed touches no DOM and fires
|
||||||
|
* no mutation record. Same shape as `fsSyncAttr` in menu-footstrap-common.js — restated rather than
|
||||||
|
* imported, since that file does not export it. */
|
||||||
|
function syncAttr(el, name, value) {
|
||||||
|
if (value === null) {
|
||||||
|
if (el.hasAttribute(name)) el.removeAttribute(name);
|
||||||
|
} else if (el.getAttribute(name) !== value) {
|
||||||
|
el.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* index.js's own attribute: "inactive" is expanded (the pill reads "Hide"), "active" is collapsed
|
||||||
|
* (it reads "Show") — the chevron mirror in pages/20-overview.css reads the same attribute the
|
||||||
|
* same way. */
|
||||||
|
function pillExpanded(label) {
|
||||||
|
return label.getAttribute('data-style') !== 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The panel `data-style` shows or hides is `.cbi-title`'s next sibling in the 25.12 markup this was
|
||||||
|
* measured on. 24.10 emits no `.cbi-title` wrapper (headerEl() above) and is left without
|
||||||
|
* aria-controls rather than guessed at — role, tabindex, aria-expanded and the keyboard below still
|
||||||
|
* apply there; aria-controls is the one piece this function cannot state with confidence. */
|
||||||
|
function panelFor(h) {
|
||||||
|
const holder = h.closest('.cbi-title');
|
||||||
|
return holder ? holder.nextElementSibling : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _panelSeq = 0;
|
||||||
|
|
||||||
|
/* Promote one card's header to a disclosure control. Idempotent: a tick that finds the header
|
||||||
|
* already wired touches only aria-expanded, and only when it actually changed; the click/keydown
|
||||||
|
* listeners are added once (`dataset.fsWired`), never re-added. */
|
||||||
|
function wireDisclosure(sec) {
|
||||||
|
const h = headerEl(sec);
|
||||||
|
const label = h && h.querySelector('.label[data-indicator="poll-status"]');
|
||||||
|
if (!h || !label) return; /* not this card's shape — left alone rather than guessed at */
|
||||||
|
|
||||||
|
const panel = panelFor(h);
|
||||||
|
if (panel && !panel.id) panel.id = 'fs-ovl-panel-' + (_panelSeq++);
|
||||||
|
|
||||||
|
syncAttr(h, 'role', 'button');
|
||||||
|
syncAttr(h, 'tabindex', '0');
|
||||||
|
syncAttr(h, 'aria-controls', panel ? panel.id : null);
|
||||||
|
syncAttr(h, 'aria-expanded', pillExpanded(label) ? 'true' : 'false');
|
||||||
|
/* the header now carries the pill's name, role and state; a screen reader user tabbing past it
|
||||||
|
* to a second, unlabelled clickable span would hear an unexplained duplicate control — same
|
||||||
|
* reasoning as the aria-hidden on svgIcon()'s output, fs-widgets.js:12 */
|
||||||
|
syncAttr(label, 'aria-hidden', 'true');
|
||||||
|
|
||||||
|
if (h.dataset.fsWired) return;
|
||||||
|
h.dataset.fsWired = '1';
|
||||||
|
|
||||||
|
h.addEventListener('click', (ev) => {
|
||||||
|
/* a click landing on the pill itself already ran index.js's own handler; forwarding here
|
||||||
|
* too would toggle the card twice */
|
||||||
|
if (ev.target.closest?.('[data-indicator="poll-status"]') !== label) label.click();
|
||||||
|
syncAttr(h, 'aria-expanded', pillExpanded(label) ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
/* the pill is an <a>-less <span>, so neither key is native here — contrast
|
||||||
|
* fs-widgets.js's wireSpaceKey, written for an <a role="button">, which gets Enter for free
|
||||||
|
* and needs only Space added */
|
||||||
|
h.addEventListener('keydown', (ev) => {
|
||||||
|
if (ev.key !== 'Enter' && ev.key !== ' ' && ev.key !== 'Spacebar') return;
|
||||||
|
ev.preventDefault();
|
||||||
|
label.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A `.cbi-section` LuCI still renders when a stock include has nothing to show this tick: title
|
||||||
|
* "-", the poll pill its only content, 72px tall on the live Overview — seventh there (a fixture is
|
||||||
|
* not proof of position: docs/playground.html puts it first). Real and cosmetic, so it is
|
||||||
|
* suppressed rather than left as a rung in the tab order with nothing behind its own name. */
|
||||||
|
function hideEmptyCard(sec) {
|
||||||
|
if (sectionTitle(sec) === '-') sec.classList.add('fs-ovl-empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tidyCards(view) {
|
||||||
|
view.querySelectorAll('.cbi-section').forEach((sec) => {
|
||||||
|
hideEmptyCard(sec);
|
||||||
|
wireDisclosure(sec);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* the wrapper we built, so the poll-tick fast path costs one property read */
|
/* the wrapper we built, so the poll-tick fast path costs one property read */
|
||||||
let _wrapEl = null;
|
let _wrapEl = null;
|
||||||
|
|
||||||
@@ -75,6 +175,18 @@ function arrange() {
|
|||||||
if (!view) return;
|
if (!view) return;
|
||||||
|
|
||||||
nameTooltips(view);
|
nameTooltips(view);
|
||||||
|
/* 20_memory.js, 25_storage.js and 30_network.js each call their OWN local progressbar(), not
|
||||||
|
* the theme's window.progressbar (menu-footstrap-common.js), so the reading and colour those
|
||||||
|
* bars need are stamped on here instead — on the same poll-tick callback nameTooltips() already
|
||||||
|
* runs on, ahead of the fast-path return below: dom.content() rewrites a section's title every
|
||||||
|
* tick even when the wrapper survives, so a bar's reading is stale on every tick this line is
|
||||||
|
* skipped. */
|
||||||
|
common.annotateMeters(view);
|
||||||
|
/* Same reasoning, same placement: dom.content() rewrites a card's body every tick even when the
|
||||||
|
* wrapper survives, so a card that is only now getting its title text needs wiring on this
|
||||||
|
* pass, not just the first one. tidyCards() is its own idempotent write (wireDisclosure(),
|
||||||
|
* hideEmptyCard() above), so a tick that changes nothing here touches no DOM either. */
|
||||||
|
tidyCards(view);
|
||||||
|
|
||||||
/* Fast path: the poll lands here on every tick, forever, and the stock poll never rebuilds
|
/* Fast path: the poll lands here on every tick, forever, and the stock poll never rebuilds
|
||||||
* the .cbi-section wrappers, so the grid survives. Deliberately not a disconnect() — if a
|
* the .cbi-section wrappers, so the grid survives. Deliberately not a disconnect() — if a
|
||||||
@@ -276,7 +388,10 @@ function patchOverview() {
|
|||||||
|
|
||||||
/* `progressbar`, `renderBox` and `renderBadge` are defined in menu-footstrap-common.js, not here:
|
/* `progressbar`, `renderBox` and `renderBadge` are defined in menu-footstrap-common.js, not here:
|
||||||
* a stock include calls them bare from its own render(), so they must exist before the view class
|
* a stock include calls them bare from its own render(), so they must exist before the view class
|
||||||
* does, and this page module is required during the navigation that races it. */
|
* does, and this page module is required during the navigation that races it. `annotateMeters` is
|
||||||
|
* required from that same module (see arrange()) rather than duplicated: it is not a template
|
||||||
|
* global, so nothing forces it to live there, but the threshold split and label lookup it shares
|
||||||
|
* with `window.progressbar` would drift into two copies otherwise. */
|
||||||
|
|
||||||
return baseclass.extend({
|
return baseclass.extend({
|
||||||
/* called once by menu-footstrap-common's init; everything route-dependent hangs off the
|
/* called once by menu-footstrap-common's init; everything route-dependent hangs off the
|
||||||
|
|||||||
@@ -316,6 +316,66 @@ function labelCells(t, head) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- WCAG 1.3.1: a `display` override must not cost a table its own roles ----
|
||||||
|
*
|
||||||
|
* theme/*.css restyles `display` on `.table`/`.thead`/`.tbody`/`.tfoot`/`.tr`/`.th`/`.td` in over
|
||||||
|
* twenty places — `.fs-stacked`, `.fs-rowstack`, the config table's own `@container`, the phone
|
||||||
|
* `@media`, the Overview meter row, the software page's carded description column, the 560px
|
||||||
|
* key/value card added alongside this file — scattered across selectors this module cannot see the
|
||||||
|
* live state of (a media query, a container query, a class some other pass toggled). A `display`
|
||||||
|
* other than `table`/`table-row`/`table-cell`/… drops the IMPLICIT `table`/`row`/`cell` role
|
||||||
|
* HTML-AAM derives from it: measured on Overview's key/value cards, `display: block`/`grid`/`block`
|
||||||
|
* (table/row/cell) at both 1440 and 390px, and on the 390px card stack, `block`/`none`/`block` —
|
||||||
|
* neither carries a role, so the column a value belongs to is never announced.
|
||||||
|
*
|
||||||
|
* Rather than pair each of those twenty-odd rules with a role write timed to its own trigger — a
|
||||||
|
* race with every one of them — the role is written UNCONDITIONALLY, matching what a plain
|
||||||
|
* `<table>` already carries implicitly. That costs nothing while the table IS a table and survives
|
||||||
|
* the moment any of those rules land. tools/table-contract.mjs holds the pairing: a future
|
||||||
|
* `display` rule on one of these classes with no role to match is what it is written to catch.
|
||||||
|
*
|
||||||
|
* The whole chain, every pass — a `role="cell"` with nothing above it saying `row`, or a `row` with
|
||||||
|
* nothing above IT saying `table`/`rowgroup`, reads worse than no roles at all. One flat query per
|
||||||
|
* class keeps it whole without walking each table by hand: `.tr`/`.th`/`.td` are LuCI's own names,
|
||||||
|
* always written together with `.table` (adoptMarkup() above never adds one without the others), so
|
||||||
|
* a class query alone reaches the whole subtree. `.thead`/`.tbody`/`.tfoot` are the same for a
|
||||||
|
* div-based table; a REAL `<thead>`/`<tbody>`/`<tfoot>` this file never classes (20_lan.js's and
|
||||||
|
* 30_wifi.js's `<tfoot>`, theme/30-tables.css:570/634) is reached by tag name instead. */
|
||||||
|
const TABLE_ROLE_CLASSES = [
|
||||||
|
[ '.table', 'table' ],
|
||||||
|
[ '.thead', 'rowgroup' ],
|
||||||
|
[ '.tbody', 'rowgroup' ],
|
||||||
|
[ '.tfoot', 'rowgroup' ],
|
||||||
|
[ '.tr', 'row' ],
|
||||||
|
[ '.th', 'columnheader' ],
|
||||||
|
[ '.td', 'cell' ],
|
||||||
|
];
|
||||||
|
const TABLE_ROLE_TAGS = [
|
||||||
|
[ 'thead', 'rowgroup' ],
|
||||||
|
[ 'tbody', 'rowgroup' ],
|
||||||
|
[ 'tfoot', 'rowgroup' ],
|
||||||
|
];
|
||||||
|
|
||||||
|
/* idempotent, the same idiom as menu-footstrap-common.js's fsSyncAttr: a node that already carries
|
||||||
|
* the right role is a no-op read, so a poll tick re-rendering the same table on every tick touches
|
||||||
|
* no DOM and wakes no attribute observer. Not imported for one line — this file has none of that
|
||||||
|
* module's other exports to justify the dependency. */
|
||||||
|
function setRole(el, role) {
|
||||||
|
if (el.getAttribute('role') !== role) el.setAttribute('role', role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A hidden row/cell (`.tr.cbi-section-table-descr`, `.td.hide-xs`/`.hide-sm` under `.fs-stacked`/
|
||||||
|
* `.fs-drop-xs`) drops out of the accessibility tree on `display: none` alone — the role written
|
||||||
|
* here changes nothing about that, since a role is inert on a box the engine never generates. */
|
||||||
|
function roleTables() {
|
||||||
|
TABLE_ROLE_CLASSES.forEach(([ sel, role ]) => {
|
||||||
|
document.querySelectorAll(inRoots(sel, liveRoots())).forEach((el) => setRole(el, role));
|
||||||
|
});
|
||||||
|
TABLE_ROLE_TAGS.forEach(([ sel, role ]) => {
|
||||||
|
document.querySelectorAll(inRoots(sel, liveRoots())).forEach((el) => setRole(el, role));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- card-stack a data table that no longer fits ----
|
/* ---- card-stack a data table that no longer fits ----
|
||||||
*
|
*
|
||||||
* Measuring, scheduling and the observers are fs-fit.js; this file supplies only the decision.
|
* Measuring, scheduling and the observers are fs-fit.js; this file supplies only the decision.
|
||||||
@@ -890,12 +950,14 @@ return baseclass.extend({
|
|||||||
fit.armGate();
|
fit.armGate();
|
||||||
|
|
||||||
/* A table must be tagged .fs-dt before it can be fitted, and re-tagged whenever the poll
|
/* A table must be tagged .fs-dt before it can be fitted, and re-tagged whenever the poll
|
||||||
* brings a fresh one back, so tagging leads.
|
* brings a fresh one back, so tagging leads; roleTables() follows it directly so a class
|
||||||
|
* adoptMarkup() just added carries its role in the same pass rather than waiting a tick.
|
||||||
*
|
*
|
||||||
* Five registrations, not one callback: fs-fit catches per registered fitter, and the first
|
* Six registrations, not one callback: fs-fit catches per registered fitter, and the first
|
||||||
* of these walks third-party markup. Bundled, one throw in `tagDataTables()` leaves no table
|
* of these walks third-party markup. Bundled, one throw in `tagDataTables()` leaves no table
|
||||||
* tagged and — with the gate above raised — a page with no tables on it at all. */
|
* tagged and — with the gate above raised — a page with no tables on it at all. */
|
||||||
fit.add(tagDataTables);
|
fit.add(tagDataTables);
|
||||||
|
fit.add(roleTables);
|
||||||
fit.add(fitTables);
|
fit.add(fitTables);
|
||||||
fit.add(fitScrollables);
|
fit.add(fitScrollables);
|
||||||
fit.add(unpinActionColumn);
|
fit.add(unpinActionColumn);
|
||||||
|
|||||||
@@ -153,6 +153,88 @@ function loadPlugins() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* warn/danger split for the meter fill (theme/25-progressbar.css): a DECISION, not a measurement —
|
||||||
|
* 20% of an 84 MiB overlay and 20% of an 8 GiB disk are different news at the same reading, and a
|
||||||
|
* plain percentage is kept anyway so every meter on the page reads the same way. */
|
||||||
|
const FS_METER_WARN = 80;
|
||||||
|
const FS_METER_DANGER = 92;
|
||||||
|
|
||||||
|
/* Write an attribute only when the value actually changes, so a poll tick that reads the same
|
||||||
|
* numbers back touches no DOM and fires no attribute-mutation observer. `value === null` removes
|
||||||
|
* the attribute instead of writing the string "null". */
|
||||||
|
function fsSyncAttr(el, name, value) {
|
||||||
|
if (value === null) {
|
||||||
|
if (el.hasAttribute(name)) el.removeAttribute(name);
|
||||||
|
} else if (el.getAttribute(name) !== value) {
|
||||||
|
el.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The meter's name, if the markup already states one — never invented. A `.cbi-value` row's own
|
||||||
|
* label (the RSSI/RSRP gallery shape) or a key/value table row's first cell (Memory, Storage, CPU
|
||||||
|
* load on Overview) each stand for the whole row; a bare meter with neither (Software's disk-space
|
||||||
|
* bar, the package manager) gets no `aria-label` written at all, and Chromium's accessible-name
|
||||||
|
* computation then falls back to `title` — which is the reading, not a name ("95 / 100 (95%)"),
|
||||||
|
* measured with `Accessibility.getPartialAXTree`. That fallback cannot be closed from here: `title`
|
||||||
|
* is the ONLY source `::after { content: attr(title) }` has for the visible percentage
|
||||||
|
* (styles/theme/25-progressbar.css), so removing it would blank the bar for a sighted reader too. */
|
||||||
|
function findProgressbarLabel(pg) {
|
||||||
|
const row = pg.closest('.cbi-value');
|
||||||
|
let label = row ? row.querySelector('.cbi-value-title') : null;
|
||||||
|
if (!label) {
|
||||||
|
const tr = pg.closest('.tr');
|
||||||
|
if (tr) {
|
||||||
|
const own = pg.closest('.td, td');
|
||||||
|
const cell = tr.querySelector('.td, td');
|
||||||
|
if (cell && cell !== own) label = cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The percentage inside a meter's `title`, in either shape a caller writes it in: `window.progressbar`
|
||||||
|
* below composes `'%s / %s (%d%%)'`, parenthesised and preceded by the byte/localised reading, but a
|
||||||
|
* bare meter markup may carry just `'97%'` with nothing around it (docs/gallery.html). Both are
|
||||||
|
* anchored at the end of the string so neither can match a stray "%" earlier in a localised reading;
|
||||||
|
* an empty title or one with no percentage at all yields null, on purpose — nothing to annotate.
|
||||||
|
* `%d` is the unclamped percentage, so the result can still read past 100 or under 0 and is clamped
|
||||||
|
* by the caller, the same way `window.progressbar` clamps its own `level`, below. Exported for
|
||||||
|
* tests/meter.test.mjs, which is the only caller that needs the parse on its own. */
|
||||||
|
function parseMeterPercent(title) {
|
||||||
|
if (title == null) return null;
|
||||||
|
const m = (/\((-?\d+)%\)\s*$/).exec(title) || (/(-?\d+)%\s*$/).exec(title);
|
||||||
|
return m ? parseInt(m[1], 10) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The attribute work `window.progressbar` and a bare `.cbi-progressbar` node both need — Status ->
|
||||||
|
* Overview's own 20_memory.js, 25_storage.js and 30_network.js (luci-mod-status) declare and call
|
||||||
|
* their OWN local `progressbar()`, a straight copy of the same upstream body, so this theme's
|
||||||
|
* global never runs on the meters those stock includes actually draw. `annotateMeters()` below
|
||||||
|
* walks the DOM after the fact instead of owning every writer; the threshold split and the name
|
||||||
|
* logic stay in this one function either way, so the two callers cannot drift apart. */
|
||||||
|
function annotateMeter(pg) {
|
||||||
|
const title = pg.getAttribute('title');
|
||||||
|
const pc = parseMeterPercent(title);
|
||||||
|
if (pc == null) return;
|
||||||
|
const level = pc < 0 ? 0 : (pc > 100 ? 100 : pc);
|
||||||
|
fsSyncAttr(pg, 'role', 'progressbar');
|
||||||
|
fsSyncAttr(pg, 'aria-valuemin', '0');
|
||||||
|
fsSyncAttr(pg, 'aria-valuemax', '100');
|
||||||
|
fsSyncAttr(pg, 'aria-valuenow', String(level));
|
||||||
|
fsSyncAttr(pg, 'aria-valuetext', title);
|
||||||
|
const label = findProgressbarLabel(pg);
|
||||||
|
fsSyncAttr(pg, 'aria-label', label ? label.textContent.trim() : null);
|
||||||
|
fsSyncAttr(pg, 'data-fs-level',
|
||||||
|
level >= FS_METER_DANGER ? 'danger' : (level >= FS_METER_WARN ? 'warn' : null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Every `.cbi-progressbar[title]` under `root` — the markup itself, not who last drew it. Called
|
||||||
|
* from fs-overview.js's own poll-tick observer (see there for why this file does not run a second
|
||||||
|
* one); `fsSyncAttr` above makes a re-run over an unchanged bar a no-op read. */
|
||||||
|
function annotateMeters(root) {
|
||||||
|
(root || document).querySelectorAll('.cbi-progressbar[title]').forEach(annotateMeter);
|
||||||
|
}
|
||||||
|
|
||||||
/* The three template globals Status -> Overview needs, defined where ordering is guaranteed.
|
/* The three template globals Status -> Overview needs, defined where ordering is guaranteed.
|
||||||
*
|
*
|
||||||
* `admin_status/index.ut` defines `progressbar`, `renderBox` and `renderBadge` in an inline script
|
* `admin_status/index.ut` defines `progressbar`, `renderBox` and `renderBadge` in an inline script
|
||||||
@@ -165,27 +247,43 @@ function loadPlugins() {
|
|||||||
* ordering the two. This file is required by the footer on every page and evaluates before the
|
* ordering the two. This file is required by the footer on every page and evaluates before the
|
||||||
* router exists.
|
* router exists.
|
||||||
*
|
*
|
||||||
* Bodies are verbatim from upstream except for two deltas: L.itemlist -> window.L.itemlist (the
|
* Bodies are verbatim from upstream except for three deltas: L.itemlist -> window.L.itemlist (the
|
||||||
* two-L trap, docs/spa-router.md), and renderBox's `[title]` — dom.append parses a scalar child as
|
* two-L trap, docs/spa-router.md); renderBox's `[title]` — dom.append parses a scalar child as
|
||||||
* innerHTML (luci.js:1395) and an array member as text (:1383), and this file defines the global on
|
* innerHTML (luci.js:1395) and an array member as text (:1383), and this file defines the global on
|
||||||
* every admin page where upstream defines it on Status -> Overview alone. Same output: nothing in
|
* every admin page where upstream defines it on Status -> Overview alone; and progressbar's
|
||||||
* 24.10, 25.12 or master calls renderBox. The typeof guards make each a no-op on a full page load,
|
* accessibility attributes (role, aria-value*, data-fs-level) — upstream's copy writes the reading
|
||||||
* where the template's own copies win the race. */
|
* into `title` alone, which nothing reads back for a screen reader off a generated `::after`. Same
|
||||||
|
* output on the other two: nothing in 24.10, 25.12 or master calls renderBox, and progressbar's
|
||||||
|
* `title` and fill width are untouched.
|
||||||
|
*
|
||||||
|
* `renderBox`/`renderBadge` keep the `typeof` guard: a full page load runs the template's inline
|
||||||
|
* `<script>` first, in document order, before this module evaluates, so upstream has already
|
||||||
|
* declared both — the guard just avoids overwriting an identical body. `progressbar` CANNOT reuse
|
||||||
|
* that guard: upstream's copy of the same name wins that exact race, and it is the one WITHOUT the
|
||||||
|
* accessibility attributes below. Guarded, the theme's version only ever won when a page other than
|
||||||
|
* Status -> Overview loaded first and the SPA router then navigated here without a reload — which
|
||||||
|
* is a real gap, not the common case. The assignment is unconditional so the accessible copy always
|
||||||
|
* wins, on a full load and on an SPA arrival alike. */
|
||||||
function ensureOverviewHelpers() {
|
function ensureOverviewHelpers() {
|
||||||
/* eslint-disable no-var -- these three bodies are copies of LuCI's admin_status/index.ut so
|
/* eslint-disable no-var -- these three bodies are copies of LuCI's admin_status/index.ut so
|
||||||
they can be diffed against upstream when it changes. Modernising the `var`s would break
|
they can be diffed against upstream when it changes. Modernising the `var`s would break
|
||||||
that property, which is what makes carrying the copies safe. */
|
that property, which is what makes carrying the copies safe. */
|
||||||
if (typeof window.progressbar !== 'function')
|
|
||||||
window.progressbar = function(query, value, max, byte) {
|
window.progressbar = function(query, value, max, byte) {
|
||||||
var pg = document.querySelector(query),
|
var pg = document.querySelector(query),
|
||||||
vn = parseInt(value) || 0,
|
vn = parseInt(value) || 0,
|
||||||
mn = parseInt(max) || 100,
|
mn = parseInt(max) || 100,
|
||||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||||
pc = Math.floor((100 / mn) * vn);
|
pc = Math.floor((100 / mn) * vn),
|
||||||
|
reading = '%s / %s (%d%%)'.format(fv, fm, pc);
|
||||||
if (pg) {
|
if (pg) {
|
||||||
pg.firstElementChild.style.width = pc + '%';
|
pg.firstElementChild.style.width = pc + '%';
|
||||||
pg.setAttribute('title', '%s / %s (%d%%)'.format(fv, fm, pc));
|
pg.setAttribute('title', reading);
|
||||||
|
/* Accessible value, colour and name: annotateMeter() re-parses `reading` back out
|
||||||
|
* of `title` rather than reusing `pc` here directly, so a bare `.cbi-progressbar`
|
||||||
|
* this function never touched is annotated the identical way — the clamp, the
|
||||||
|
* label lookup and the warn/danger split stay in that one function. */
|
||||||
|
annotateMeter(pg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (typeof window.renderBox !== 'function')
|
if (typeof window.renderBox !== 'function')
|
||||||
@@ -235,6 +333,15 @@ return baseclass.extend({
|
|||||||
* remember() for what a key is */
|
* remember() for what a key is */
|
||||||
remember,
|
remember,
|
||||||
|
|
||||||
|
/* the seam fs-overview.js calls, on its own already-coalesced poll-tick observer, to annotate
|
||||||
|
* the meters a stock Status -> Overview include draws with its own local progressbar() */
|
||||||
|
annotateMeters,
|
||||||
|
|
||||||
|
/* module-private otherwise: tests/meter.test.mjs drives the percent parse and the clamp/threshold
|
||||||
|
* split directly rather than through a live `.cbi-progressbar[title]` walk */
|
||||||
|
parseMeterPercent, /* fs:probe */
|
||||||
|
annotateMeter, /* fs:probe */
|
||||||
|
|
||||||
init(renderMainMenu) {
|
init(renderMainMenu) {
|
||||||
/* First, and outside the promise: a third-party sheet that outranks the chrome is already
|
/* First, and outside the promise: a third-party sheet that outranks the chrome is already
|
||||||
* painting (fs-sheets: openclash's `* { margin: 0; padding: 0 }`). Deferring this to
|
* painting (fs-sheets: openclash's `* { margin: 0; padding: 0 }`). Deferring this to
|
||||||
|
|||||||
@@ -12,16 +12,24 @@
|
|||||||
* drifted). Only the overview-specific chevron toggle stays.
|
* drifted). Only the overview-specific chevron toggle stays.
|
||||||
* index.js emits a span with a raw "Hide"/"Show" text node + data-style (expanded =
|
* index.js emits a span with a raw "Hide"/"Show" text node + data-style (expanded =
|
||||||
* inactive, collapsed = active); data-indicator='poll-status' is unique to these
|
* inactive, collapsed = active); data-indicator='poll-status' is unique to these
|
||||||
* toggles. font-size:0 hides the word, ::after borders draw the chevron. */
|
* toggles. font-size:0 hides the word, ::after borders draw the chevron.
|
||||||
|
*
|
||||||
|
* The card HEADER is the disclosure control now (fs-overview.js wireDisclosure()); this pill is
|
||||||
|
* marked aria-hidden there and stays only as its glyph, so the border and fill that made it read
|
||||||
|
* as its own 26px button are dropped — 16px, matching the rail toggle's own chevron glyph
|
||||||
|
* (theme/20-shell.css `.fs-railtoggle .fs-ico`), and --fs-faint rather than the pill's --fs-dim,
|
||||||
|
* a step quieter since it is decoration rather than a target now. */
|
||||||
.cbi-title .label[data-indicator="poll-status"] {
|
.cbi-title .label[data-indicator="poll-status"] {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
width: 26px;
|
width: 16px;
|
||||||
height: 26px;
|
height: 16px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: var(--fs-radius-sm);
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
color: var(--fs-faint);
|
||||||
}
|
}
|
||||||
/* The glyph itself is the Appearance fold's, to the byte — one chevron for "this opens", drawn
|
/* The glyph itself is the Appearance fold's, to the byte — one chevron for "this opens", drawn
|
||||||
* in two page files because the two hosts are a stock element keyed by attribute and one of
|
* in two page files because the two hosts are a stock element keyed by attribute and one of
|
||||||
@@ -47,6 +55,37 @@
|
|||||||
/* @endmirror */
|
/* @endmirror */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The header itself (fs-overview.js wireDisclosure() stamps role="button"): a hand cursor for a
|
||||||
|
* pointer user, a focus ring for a keyboard one — the same ring every other interactive control
|
||||||
|
* in this theme draws (theme/35-alerts.css `.cbi-title .label:focus-visible`, the pill's own
|
||||||
|
* former ring, now the header's).
|
||||||
|
*
|
||||||
|
* Two selectors, not one: fs-overview.js's headerEl() reads `.cbi-title h3, :scope > h3` because
|
||||||
|
* the wrapper itself differs by release — 25.12 renders `.cbi-title > h3`, 24.10 a bare `<h3>`
|
||||||
|
* straight inside `.cbi-section`, no `.cbi-title` at all. `[role="button"]` is the anchor either
|
||||||
|
* way, so a heading wireDisclosure() left untouched — one with nothing to collapse — is never
|
||||||
|
* matched. Measured on a 24.10 FORM: without the second arm the attributes and the keyboard
|
||||||
|
* worked but `cursor` stayed `auto`, `box-shadow` `none`, and focus drew only the UA outline.
|
||||||
|
* On a live 24.10 the arm does not fire at all today, and that is not a fault in it: OpenWrt
|
||||||
|
* 24.10.8 renders no hide/show control on an Overview card, so wireDisclosure() stamps nothing
|
||||||
|
* there and 0 of the page 14 `<h3>`s carry the role (measured on owlab owrt2410). The arm
|
||||||
|
* guards a SHAPE of markup, not a release — it is what keeps the styling with the behaviour on
|
||||||
|
* whichever release grows the control next. */
|
||||||
|
:is(.cbi-title h3, .cbi-section > h3)[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--fs-radius-sm);
|
||||||
|
}
|
||||||
|
:is(.cbi-title h3, .cbi-section > h3)[role="button"]:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--fs-focus-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An empty `.cbi-section` LuCI still renders when a stock include has nothing to show: title
|
||||||
|
* "-", the poll pill its only content, 72px tall on the live Overview (seventh there — a
|
||||||
|
* fixture is not proof of position, docs/findings.md "An empty card with `-` as its title").
|
||||||
|
* Real and cosmetic, so fs-overview.js's hideEmptyCard() marks it and this drops it. */
|
||||||
|
body[data-page="admin-status-overview"] .cbi-section.fs-ovl-empty { display: none; }
|
||||||
|
|
||||||
/* Stock overview layout: fs-overview.js wraps the stock System / Memory / Storage
|
/* Stock overview layout: fs-overview.js wraps the stock System / Memory / Storage
|
||||||
* sections in .fs-ovl; this is the grid only — System left across both rows, Memory +
|
* sections in .fs-ovl; this is the grid only — System left across both rows, Memory +
|
||||||
* Storage right. Stock styling and data untouched. */
|
* Storage right. Stock styling and data untouched. */
|
||||||
|
|||||||
@@ -192,8 +192,13 @@
|
|||||||
}
|
}
|
||||||
.fs-ap-verrow { display: flex; align-items: center; justify-content: space-between; gap: var(--fs-space-2-5); }
|
.fs-ap-verrow { display: flex; align-items: center; justify-content: space-between; gap: var(--fs-space-2-5); }
|
||||||
/* links to the project repo — reads as muted text until hovered */
|
/* links to the project repo — reads as muted text until hovered */
|
||||||
.fs-ap-version { font-size: var(--fs-type-xs); color: var(--fs-dim); font-variant-numeric: tabular-nums; text-decoration: none; }
|
.fs-ap-version { position: relative; font-size: var(--fs-type-xs); color: var(--fs-dim); font-variant-numeric: tabular-nums; text-decoration: none; }
|
||||||
.fs-ap-version:hover { color: var(--fs-accent); text-decoration: underline; }
|
.fs-ap-version:hover { color: var(--fs-accent); text-decoration: underline; }
|
||||||
|
/* the link's own box was 84x17 (owrt2512/390x844, Appearance tab), under the 24px floor of
|
||||||
|
* WCAG 2.2 SC 2.5.8 on height. Same fix as `.cbi-checkbox > label[for]::before` in
|
||||||
|
* theme/60-inputs.css: an invisible, absolutely-positioned overlay grows the hit box to
|
||||||
|
* 88x25 without moving the text or growing `.fs-ap-verrow`, since the overlay is out of flow. */
|
||||||
|
.fs-ap-version::before { content: ""; position: absolute; inset: -4px -2px; }
|
||||||
|
|
||||||
/* The line that names the missing catalogue package. Same weight as the version beside it —
|
/* The line that names the missing catalogue package. Same weight as the version beside it —
|
||||||
* it is a fact about this router, not a warning: nothing is broken, one package is absent.
|
* it is a fact about this router, not a warning: nothing is broken, one package is absent.
|
||||||
|
|||||||
@@ -782,4 +782,14 @@
|
|||||||
* so "Refreshing" showed a text caret. */
|
* so "Refreshing" showed a text caret. */
|
||||||
.fs-indicators [data-indicator][data-clickable],
|
.fs-indicators [data-indicator][data-clickable],
|
||||||
#indicators [data-indicator][data-clickable] { cursor: pointer; }
|
#indicators [data-indicator][data-clickable] { cursor: pointer; }
|
||||||
|
/* fs-chrome.js promotes a clickable indicator to role="button" with Enter and Space, so
|
||||||
|
* it is a control now and takes the control ring the buttons beside it take (55-buttons.css)
|
||||||
|
* rather than the UA outline it was left with. `:focus-visible`, not `:focus`: a pointer press
|
||||||
|
* on the pill should not draw a ring, the same rule every other control here follows. */
|
||||||
|
.fs-indicators [data-indicator][data-clickable]:focus-visible,
|
||||||
|
#indicators [data-indicator][data-clickable]:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--fs-accent);
|
||||||
|
box-shadow: var(--fs-focus-ring);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,16 @@
|
|||||||
border-radius: var(--fs-radius-pill);
|
border-radius: var(--fs-radius-pill);
|
||||||
transition: width var(--fs-dur-fill);
|
transition: width var(--fs-dur-fill);
|
||||||
}
|
}
|
||||||
|
/* Colour carries the reading too, not length alone: `data-fs-level` is set from JS
|
||||||
|
* (menu-footstrap-common.js, FS_METER_WARN/FS_METER_DANGER — 80/92%, a decision, not a
|
||||||
|
* measurement) on every poll tick. Solid tokens, matching the accent fill above — no
|
||||||
|
* color-mix, so no static twin is owed to 04-nocolormix.css. */
|
||||||
|
.cbi-progressbar[data-fs-level="warn"] > div {
|
||||||
|
background: var(--fs-warn);
|
||||||
|
}
|
||||||
|
.cbi-progressbar[data-fs-level="danger"] > div {
|
||||||
|
background: var(--fs-danger);
|
||||||
|
}
|
||||||
.cbi-progressbar::after {
|
.cbi-progressbar::after {
|
||||||
content: attr(title);
|
content: attr(title);
|
||||||
/* the gap is a scale step, not a literal 4: every other gap in the theme scales with the
|
/* the gap is a scale step, not a literal 4: every other gap in the theme scales with the
|
||||||
|
|||||||
@@ -303,6 +303,43 @@
|
|||||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:not(:first-child) {
|
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:not(:first-child) {
|
||||||
font-family: var(--fs-font-mono); color: var(--fs-text); text-align: end;
|
font-family: var(--fs-font-mono); color: var(--fs-text); text-align: end;
|
||||||
}
|
}
|
||||||
|
/* Below ~560px of container width the 40%/nowrap column above starves the value: measured on
|
||||||
|
* System -> System at 390px content width, Firmware Version wrapped onto 5 lines and Kernel
|
||||||
|
* Version/Architecture onto 2 each, most of the 130px label column sitting empty. Stack it like
|
||||||
|
* the other card idioms in this file — label as an eyebrow above its value — instead of holding
|
||||||
|
* a fixed-fraction column at any width. A width win, not a height one: measured on the gallery's
|
||||||
|
* key/value specimen (a 340px .cbi-map, docs/gallery.html), stacking cost the card +44.9% height
|
||||||
|
* (202.6 -> 293.5px) for the value column widening x1.67 (183.6 -> 306px) and Firmware Version's
|
||||||
|
* line count dropping from two to one. (The five lines above are the live 390px page, where the
|
||||||
|
* value is longer than the specimen carries — the shape is the same, the count is not.)
|
||||||
|
*
|
||||||
|
* 560 is the second threshold named on fs-content; the config table's card (theme/65-dropdown.css)
|
||||||
|
* is the first, at 960, for a row full of widgets that bake in a layout width. This one holds two
|
||||||
|
* plain text cells, so it can stay two-column down to a narrower width before the label needs the
|
||||||
|
* row to itself.
|
||||||
|
*
|
||||||
|
* `:first-child:first-child` / `:last-child:last-child` double the tail pseudo-class to outrank
|
||||||
|
* the two rules above at identical specificity otherwise — the `.hidden.hidden` idiom
|
||||||
|
* (theme/60-inputs.css) — rather than win on source order, which the next edit to either rule
|
||||||
|
* could invert.
|
||||||
|
*
|
||||||
|
* The value's right-flush alignment above is a two-column device: with the label gone from the
|
||||||
|
* row, right-flushing floats the value away from its own caption. Left-aligned instead, so the
|
||||||
|
* card reads top-to-bottom as caption then value, matching every other stacked cell in this file. */
|
||||||
|
@container fs-content (max-width: 560px) {
|
||||||
|
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .tr {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:first-child:first-child {
|
||||||
|
display: block; width: auto; white-space: normal;
|
||||||
|
font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight);
|
||||||
|
letter-spacing: var(--fs-eyebrow-tracking); color: var(--fs-eyebrow-color);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:last-child:not(:first-child) {
|
||||||
|
display: block; width: auto; text-align: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- the realtime graph legend (status/realtime/*) ---
|
/* --- the realtime graph legend (status/realtime/*) ---
|
||||||
* The one no-id `.table` LuCI renders with inline `table-layout:fixed;width:100%` and six cells
|
* The one no-id `.table` LuCI renders with inline `table-layout:fixed;width:100%` and six cells
|
||||||
|
|||||||
@@ -110,8 +110,8 @@
|
|||||||
* a container-type, so giving container-type to any new component would silently re-target every
|
* a container-type, so giving container-type to any new component would silently re-target every
|
||||||
* unnamed query underneath it:
|
* unnamed query underneath it:
|
||||||
* fs-view the overview's grid (pages/20-overview.css, @container 800)
|
* fs-view the overview's grid (pages/20-overview.css, @container 800)
|
||||||
* fs-content "whatever content column I am in", carried by both .cbi-map and #view, so a query
|
* fs-content "whatever content column I am in", carried by .cbi-map, #view and .modal (below),
|
||||||
* resolves to the nearer of the two. */
|
* so a query resolves to the nearest of the three. */
|
||||||
.cbi-map { container-type: inline-size; container-name: fs-content; }
|
.cbi-map { container-type: inline-size; container-name: fs-content; }
|
||||||
#view { container-type: inline-size; container-name: fs-view fs-content; }
|
#view { container-type: inline-size; container-name: fs-view fs-content; }
|
||||||
|
|
||||||
|
|||||||
@@ -782,32 +782,78 @@
|
|||||||
* `!important` would do it too, and would invert the layer order for every future override. */
|
* `!important` would do it too, and would invert the layer order for every future override. */
|
||||||
.hidden.hidden { display: none; }
|
.hidden.hidden { display: none; }
|
||||||
/* RangeSlider (UIRangeSlider) — the one LuCI widget base leaves to native browser rendering.
|
/* RangeSlider (UIRangeSlider) — the one LuCI widget base leaves to native browser rendering.
|
||||||
* Token-driven track/thumb. */
|
* Token-driven track/thumb.
|
||||||
|
* The INPUT box is the WCAG 2.2 SC 2.5.8 target (24px min): it used to be the 6px track
|
||||||
|
* itself, measured 258x6 on owrt2512/390x844 (System > System). Box and paint split here —
|
||||||
|
* a transparent 24px box, the 6px bar drawn by the track pseudo-element inside it.
|
||||||
|
*
|
||||||
|
* A flat `height: 24px` DOES move the row, and not through `.cbi-range-slider`'s own cross
|
||||||
|
* size — that stayed 29.5px throughout, already pinned by the value chip beside it. It moves
|
||||||
|
* through the enclosing LINE BOX: `.cbi-value-field` lays this element out as ordinary inline
|
||||||
|
* content, and the line's height is anchored on where `.cbi-range-slider` (inline-flex, no
|
||||||
|
* baseline-participating child) reports ITS baseline to the ancestor line — which tracks the
|
||||||
|
* first flex item's own edge and moved with the input's box. Measured before either fix below:
|
||||||
|
* `.cbi-value-field` +2.75px (Chromium) / +2.5px (Firefox), `.cbi-value` +1.5px / +1.0px,
|
||||||
|
* track/thumb ~1.25px lower.
|
||||||
|
*
|
||||||
|
* First half of the fix is the checkbox's own trick (`--hit`/`--box` below): grow the box for
|
||||||
|
* the hit target, then pull the extra area back out of flow with a symmetric negative margin
|
||||||
|
* so the box overflows itself around its old centre instead of pushing something taller. 6px
|
||||||
|
* was the box before this rule; -9px is (6px - 24px) / 2. This is what keeps a BARE range
|
||||||
|
* input (no `.cbi-range-slider` wrapper, no taller sibling — a third-party app's own markup)
|
||||||
|
* from growing its own row, the same way the checkbox's margin protects a row with no toggle
|
||||||
|
* label beside it. Measured on its own, though, it left THIS row's growth above unchanged: the
|
||||||
|
* line-box placement of `.cbi-range-slider` tracks the input's own edge, not the flow space the
|
||||||
|
* margin trick frees up, so the second half below is still needed on top of it. */
|
||||||
input[type="range"] {
|
input[type="range"] {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 100%; height: 6px; min-height: 0; padding: 0;
|
width: 100%; height: 24px; min-height: 0; padding: 0;
|
||||||
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
|
margin-block: -9px;
|
||||||
|
background: transparent; border: 0; border-radius: 0;
|
||||||
outline: none; box-shadow: none; cursor: pointer;
|
outline: none; box-shadow: none; cursor: pointer;
|
||||||
}
|
}
|
||||||
|
input[type="range"]::-webkit-slider-runnable-track {
|
||||||
|
width: 100%; height: 6px;
|
||||||
|
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
|
||||||
|
}
|
||||||
|
input[type="range"]::-moz-range-track {
|
||||||
|
width: 100%; height: 6px;
|
||||||
|
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
|
||||||
|
}
|
||||||
input[type="range"]::-webkit-slider-thumb {
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 16px; height: 16px; border-radius: 50%;
|
width: 16px; height: 16px; border-radius: 50%;
|
||||||
background: var(--fs-accent); border: 2px solid var(--fs-panel);
|
background: var(--fs-accent); border: 2px solid var(--fs-panel);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
/* WebKit centres the thumb on the INPUT's box, not the track: (6 - 16) / 2 = -5px pulls
|
||||||
|
* it back onto the 6px track. Firefox centres ::-moz-range-thumb on the track itself. */
|
||||||
|
margin-top: -5px;
|
||||||
}
|
}
|
||||||
input[type="range"]::-moz-range-thumb {
|
input[type="range"]::-moz-range-thumb {
|
||||||
width: 16px; height: 16px; border-radius: 50%;
|
width: 16px; height: 16px; border-radius: 50%;
|
||||||
background: var(--fs-accent); border: 2px solid var(--fs-panel); cursor: pointer;
|
background: var(--fs-accent); border: 2px solid var(--fs-panel); cursor: pointer;
|
||||||
}
|
}
|
||||||
input[type="range"]::-moz-range-track { background: var(--fs-track); height: 6px; border-radius: var(--fs-radius-pill); }
|
|
||||||
/* -solo: the track sets `border: 0` and `outline: none`, so there is no second channel here
|
/* -solo: the track sets `border: 0` and `outline: none`, so there is no second channel here
|
||||||
* either — the tinted ring was the whole indicator on a 6px bar. */
|
* either — the tinted ring was the whole indicator on a 6px bar. Set on the track pseudo,
|
||||||
input[type="range"]:focus { box-shadow: var(--fs-focus-ring-solo); }
|
* not the input, so the ring still hugs the 6px bar rather than the 24px hit box. */
|
||||||
|
input[type="range"]:focus::-webkit-slider-runnable-track { box-shadow: var(--fs-focus-ring-solo); }
|
||||||
|
input[type="range"]:focus::-moz-range-track { box-shadow: var(--fs-focus-ring-solo); }
|
||||||
/* THE SAME CEILING THE ELASTIC TEXT INPUTS TAKE, and the reason is the whole point of the
|
/* THE SAME CEILING THE ELASTIC TEXT INPUTS TAKE, and the reason is the whole point of the
|
||||||
* token: a `.cbi-value-field` is `flex: 1`, so `width: 100%` here meant the slider ate the
|
* token: a `.cbi-value-field` is `flex: 1`, so `width: 100%` here meant the slider ate the
|
||||||
* entire card while the select two rows above stopped at 210px and the readout chip was flung
|
* entire card while the select two rows above stopped at 210px and the readout chip was flung
|
||||||
* to the card's right edge. Every stretching control in a form row now ends at one x. */
|
* to the card's right edge. Every stretching control in a form row now ends at one x.
|
||||||
.cbi-range-slider { display: inline-flex; align-items: center; gap: var(--fs-space-3); width: 100%; max-width: var(--fs-field-max); }
|
*
|
||||||
|
* `vertical-align: middle` answers the second half of the 24px-target growth explained above
|
||||||
|
* `input[type="range"]`. Default (`baseline`) places this inline-flex box in its line by the
|
||||||
|
* edge of its first flex item — the range input — so that edge moving 6px -> 24px still pushed
|
||||||
|
* `.cbi-value-field`'s line taller even with the margin trick already in place. `middle` places
|
||||||
|
* the box by its own total height instead, which the margin trick already held at its old
|
||||||
|
* 29.5px (pinned by the value chip, unaffected by the input's growth), decoupling the two.
|
||||||
|
* Measured on the gallery slider fixture, both engines, this rule plus the margin trick against
|
||||||
|
* HEAD: `.cbi-value-field` +0.000px in both, `.cbi-value` -0.187px (Chromium) / -0.183px
|
||||||
|
* (Firefox) — under a fifth of a CSS px, not a pixel a screen can render differently. Box
|
||||||
|
* unchanged at 258x24. */
|
||||||
|
.cbi-range-slider { display: inline-flex; align-items: center; vertical-align: middle; gap: var(--fs-space-3); width: 100%; max-width: var(--fs-field-max); }
|
||||||
.cbi-range-slider-value {
|
.cbi-range-slider-value {
|
||||||
background: var(--fs-panel2); border: var(--fs-hairline); border-radius: var(--fs-radius-sm);
|
background: var(--fs-panel2); border: var(--fs-hairline); border-radius: var(--fs-radius-sm);
|
||||||
padding: var(--fs-space-1) var(--fs-space-2); color: var(--fs-accent); font-family: var(--fs-font-mono);
|
padding: var(--fs-space-1) var(--fs-space-2); color: var(--fs-accent); font-family: var(--fs-font-mono);
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# This is free software, licensed under the GNU General Public License v2.
|
||||||
|
# See /LICENSE for more information.
|
||||||
|
#
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NPM_NAME:=pnpm
|
||||||
|
PKG_NAME:=node-$(PKG_NPM_NAME)
|
||||||
|
PKG_VERSION:=11.15.1
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_SOURCE:=$(PKG_NPM_NAME)-$(PKG_VERSION).tgz
|
||||||
|
PKG_SOURCE_URL:=https://mirrors.tencent.com/npm/$(PKG_NPM_NAME)/-/ \
|
||||||
|
https://registry.npmjs.org/$(PKG_NPM_NAME)/-/
|
||||||
|
PKG_HASH:=skip
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
|
||||||
|
PKG_LICENSE:=MIT
|
||||||
|
PKG_LICENSE_FILES:=LICENSE
|
||||||
|
|
||||||
|
HOST_BUILD_DEPENDS:=node/host
|
||||||
|
HOST_BUILD_PARALLEL:=1
|
||||||
|
|
||||||
|
PKG_BUILD_DEPENDS:=node/host
|
||||||
|
PKG_BUILD_PARALLEL:=1
|
||||||
|
PKG_BUILD_FLAGS:=no-mips16
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
include $(INCLUDE_DIR)/host-build.mk
|
||||||
|
|
||||||
|
define Package/node-pnpm
|
||||||
|
SUBMENU:=Node.js
|
||||||
|
SECTION:=lang
|
||||||
|
CATEGORY:=Languages
|
||||||
|
TITLE:=Fast, disk space efficient package manager
|
||||||
|
URL:=https://pnpm.io/
|
||||||
|
DEPENDS:=+node
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/node-pnpm/description
|
||||||
|
Fast, disk space efficient package manager
|
||||||
|
endef
|
||||||
|
|
||||||
|
TAR_OPTIONS+= --strip-components 1
|
||||||
|
TAR_CMD=$(HOST_TAR) -C $(1) $(TAR_OPTIONS)
|
||||||
|
|
||||||
|
NODEJS_CPU:=$(subst powerpc,ppc,$(subst aarch64,arm64,$(subst x86_64,x64,$(subst i386,ia32,$(ARCH)))))
|
||||||
|
TMPNPM:=$(shell mktemp -u XXXXXXXXXX)
|
||||||
|
HOSTTMPNPM:=$(shell mktemp -u XXXXXXXXXX)
|
||||||
|
NPM_CACHE_DIR:=$(if $(call qstrip,$(CONFIG_NODEJS_NPM_CACHE_DIR)),$(call qstrip,$(CONFIG_NODEJS_NPM_CACHE_DIR)),$(TMP_DIR))
|
||||||
|
|
||||||
|
TARGET_CFLAGS+=$(FPIC)
|
||||||
|
TARGET_CPPFLAGS+=$(FPIC)
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
cd $(PKG_BUILD_DIR); \
|
||||||
|
$(MAKE_VARS) \
|
||||||
|
$(MAKE_FLAGS) \
|
||||||
|
npm_config_nodedir=$(STAGING_DIR)/usr/ \
|
||||||
|
$(if $(CONFIG_NODEJS_NPM_KEEP_CACHE), npm_config_cache=$(NPM_CACHE_DIR)/npm-cache-$(PKG_NPM_NAME),npm_config_cache=$(TMP_DIR)/npm-cache-$(TMPNPM)) \
|
||||||
|
npm install --prefer-offline --no-audit --global-style --install-strategy=shallow --no-save --omit=dev --no-package-lock
|
||||||
|
rm -rf $(TMP_DIR)/npm-cache-$(TMPNPM)
|
||||||
|
rm -f $(PKG_BUILD_DIR)/node_modules/.package-lock.json
|
||||||
|
find $(PKG_BUILD_DIR)/node_modules -type d -empty -print0 | xargs -0 -r rmdir || true
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/node-pnpm/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/lib/node/$(PKG_NPM_NAME)
|
||||||
|
$(CP) $(PKG_BUILD_DIR)/{package.json,LICENSE} \
|
||||||
|
$(1)/usr/lib/node/$(PKG_NPM_NAME)/
|
||||||
|
$(CP) $(PKG_BUILD_DIR)/README.md \
|
||||||
|
$(1)/usr/lib/node/$(PKG_NPM_NAME)/
|
||||||
|
$(CP) $(PKG_BUILD_DIR)/bin \
|
||||||
|
$(1)/usr/lib/node/$(PKG_NPM_NAME)/
|
||||||
|
$(INSTALL_DIR) $(1)/usr/lib/node/$(PKG_NPM_NAME)/dist
|
||||||
|
$(CP) $(PKG_BUILD_DIR)/dist/{pnpm.mjs,worker.js} \
|
||||||
|
$(1)/usr/lib/node/$(PKG_NPM_NAME)/dist/
|
||||||
|
$(INSTALL_DIR) $(1)/usr/bin
|
||||||
|
$(LN) ../lib/node/$(PKG_NPM_NAME)/bin/pnpm.mjs $(1)/usr/bin/pnpm
|
||||||
|
$(LN) ../lib/node/$(PKG_NPM_NAME)/bin/pnpx.mjs $(1)/usr/bin/pnpx
|
||||||
|
$(INSTALL_DIR) $(1)/usr/lib/node_modules
|
||||||
|
$(LN) ../node/$(PKG_NPM_NAME) $(1)/usr/lib/node_modules/$(PKG_NPM_NAME)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/node-pnpm/postrm
|
||||||
|
#!/bin/sh
|
||||||
|
rm /usr/lib/node_modules/pnpm || true
|
||||||
|
rm -rf /usr/lib/node/pnpm || true
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Host/Compile
|
||||||
|
cd $(HOST_BUILD_DIR); \
|
||||||
|
$(HOST_MAKE_VARS) \
|
||||||
|
npm_config_nodedir=$(STAGING_DIR)/usr/ \
|
||||||
|
$(if $(CONFIG_NODEJS_NPM_KEEP_CACHE), npm_config_cache=$(NPM_CACHE_DIR)/host-npm-cache-$(PKG_NPM_NAME),npm_config_cache=$(TMP_DIR)/npm-cache-$(HOSTTMPNPM)) \
|
||||||
|
npm install --prefer-offline --no-audit --global-style --install-strategy=shallow --no-save --omit=dev --no-package-lock
|
||||||
|
rm -rf $(TMP_DIR)/npm-cache-$(HOSTTMPNPM)
|
||||||
|
rm -f $(HOST_BUILD_DIR)/node_modules/.package-lock.json
|
||||||
|
find $(HOST_BUILD_DIR)/node_modules -type d -empty -print0 | xargs -0 -r rmdir || true
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Host/Install
|
||||||
|
$(INSTALL_DIR) $(1)/lib/node_modules/$(PKG_NPM_NAME)
|
||||||
|
$(CP) $(HOST_BUILD_DIR)/{package.json,LICENSE} \
|
||||||
|
$(1)/lib/node_modules/$(PKG_NPM_NAME)/
|
||||||
|
$(CP) $(HOST_BUILD_DIR)/README.md \
|
||||||
|
$(1)/lib/node_modules/$(PKG_NPM_NAME)/
|
||||||
|
$(CP) $(HOST_BUILD_DIR)/bin \
|
||||||
|
$(1)/lib/node_modules/$(PKG_NPM_NAME)/
|
||||||
|
$(INSTALL_DIR) $(1)/lib/node_modules/$(PKG_NPM_NAME)/dist
|
||||||
|
$(CP) $(HOST_BUILD_DIR)/dist/{pnpm.mjs,worker.js} \
|
||||||
|
$(1)/lib/node_modules/$(PKG_NPM_NAME)/dist/
|
||||||
|
$(INSTALL_DIR) $(1)/bin
|
||||||
|
$(LN) ../lib/node_modules/$(PKG_NPM_NAME)/bin/pnpm.mjs $(1)/bin/pnpm
|
||||||
|
$(LN) ../lib/node_modules/$(PKG_NPM_NAME)/bin/pnpx.mjs $(1)/bin/pnpx
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call HostBuild))
|
||||||
|
$(eval $(call BuildPackage,node-pnpm))
|
||||||
@@ -1767,6 +1767,9 @@ static int __init fast_classifier_init(void)
|
|||||||
printk(KERN_ALERT "fast-classifier: starting up\n");
|
printk(KERN_ALERT "fast-classifier: starting up\n");
|
||||||
DEBUG_INFO("SFE CM init\n");
|
DEBUG_INFO("SFE CM init\n");
|
||||||
|
|
||||||
|
/* Initialize state before registering callbacks that can use it. */
|
||||||
|
spin_lock_init(&sc->lock);
|
||||||
|
|
||||||
hash_init(fc_conn_ht);
|
hash_init(fc_conn_ht);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1882,8 +1885,6 @@ static int __init fast_classifier_init(void)
|
|||||||
|
|
||||||
printk(KERN_ALERT "fast-classifier: registered\n");
|
printk(KERN_ALERT "fast-classifier: registered\n");
|
||||||
|
|
||||||
spin_lock_init(&sc->lock);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Hook the receive path in the network stack.
|
* Hook the receive path in the network stack.
|
||||||
*/
|
*/
|
||||||
@@ -2005,4 +2006,3 @@ module_exit(fast_classifier_exit)
|
|||||||
|
|
||||||
MODULE_DESCRIPTION("Shortcut Forwarding Engine - Connection Manager");
|
MODULE_DESCRIPTION("Shortcut Forwarding Engine - Connection Manager");
|
||||||
MODULE_LICENSE("Dual BSD/GPL");
|
MODULE_LICENSE("Dual BSD/GPL");
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -43,12 +43,12 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=wwand
|
PKG_NAME:=wwand
|
||||||
PKG_RELEASE:=21
|
PKG_RELEASE:=22
|
||||||
|
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||||
PKG_SOURCE_VERSION:=6ed882e3b53544c9e42f2cc681780d99205141eb
|
PKG_SOURCE_VERSION:=1aff5270ef08b380e9c07ad5c62b79704212b779
|
||||||
PKG_SOURCE_DATE:=2026-09-03
|
PKG_SOURCE_DATE:=2026-09-05
|
||||||
PKG_MIRROR_HASH:=skip
|
PKG_MIRROR_HASH:=skip
|
||||||
|
|
||||||
PKG_LICENSE:=GPL-2.0-only
|
PKG_LICENSE:=GPL-2.0-only
|
||||||
|
|||||||
Reference in New Issue
Block a user