🐶 Sync 2026-11-08 23:51:11

This commit is contained in:
github-actions[bot] committed 2026-11-08 23:51:11 +08:00
1 parent 5a5196e2d6
commit 534c8d6ab2
73 files changed
+12872 -2580

No files matched your search

Binary file not shown.
+2 -2
View File
@@ -6,13 +6,13 @@ PKG_UPSTREAM_TAG:=v$(PKG_UPSTREAM_VERSION)
PKG_SHORT_SHA:=3f27ef3 PKG_SHORT_SHA:=3f27ef3
PKG_VERSION:=2026.11.04.$(PKG_UPSTREAM_VERSION) PKG_VERSION:=2026.11.04.$(PKG_UPSTREAM_VERSION)
PKG_RELEASE:=26 PKG_RELEASE:=27
PKG_SOURCE:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA).tar.gz PKG_SOURCE:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=7cfb805e17bdb39e47de36296a41660d2027e954 PKG_SOURCE_VERSION:=205f65fc18f4dd0579b421a2271e9d46ba8cb6f3
PKG_HASH:=skip PKG_HASH:=skip
PKG_BUILD_VERSION:=$(PKG_UPSTREAM_TAG) PKG_BUILD_VERSION:=$(PKG_UPSTREAM_TAG)
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=gately PKG_NAME:=gately
PKG_VERSION:=0.6.5.1 PKG_VERSION:=0.6.5.2
PKG_RELEASE:=28 PKG_RELEASE:=29
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)?
+3 -3
View File
@@ -5,11 +5,11 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=honk PKG_NAME:=honk
PKG_VERSION:=2026.11.07~73b1077 PKG_VERSION:=2026.11.08~62b1390
PKG_RELEASE:=32 PKG_RELEASE:=33
HONK_SRC_URL:=https://github.com/daeuniverse/honk.git HONK_SRC_URL:=https://github.com/daeuniverse/honk.git
HONK_SRC_VERSION:=73b1077bf9cdb639899f0447994cf8980abc0752 HONK_SRC_VERSION:=62b1390401d9b5eb00dfaddad3e11db4a2539fdf
PKG_LICENSE:=AGPL-3.0-only PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILES:=LICENSE PKG_LICENSE_FILES:=LICENSE
+2 -2
View File
@@ -11,8 +11,8 @@ 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.8.8 PKG_SOURCE_DATE:=0.9.0
PKG_RELEASE:=15 PKG_RELEASE:=16
ARCH_HEXCODE:= ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64) ifeq ($(ARCH),x86_64)
ARCH_HEXCODE=8664 ARCH_HEXCODE=8664
+2 -2
View File
@@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_KS_CLI:=$(ARCH) PKG_ARCH_KS_CLI:=$(ARCH)
PKG_NAME:=ks-cli PKG_NAME:=ks-cli
PKG_VERSION:=0.8.9 PKG_VERSION:=0.9.0
PKG_RELEASE:=6 PKG_RELEASE:=7
PKG_SOURCE:=ks-cli-binary-$(PKG_VERSION).tar.gz PKG_SOURCE:=ks-cli-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$(PKG_VERSION)/ PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$(PKG_VERSION)/
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for KSpeeder LUCI_TITLE:=LuCI support for KSpeeder
LUCI_DEPENDS:=+istoreenhance +luci-lib-linkeaseauth +linkease-app-entry LUCI_DEPENDS:=+istoreenhance +luci-lib-linkeaseauth +linkease-app-entry
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_VERSION:=0.8.8-r1 PKG_VERSION:=0.9.0-r1
PKG_RELEASE:=9 PKG_RELEASE:=10
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
LUCI_TITLE:=KaiPlus LUCI_TITLE:=KaiPlus
PKG_VERSION:=1.0.9-r9 PKG_VERSION:=1.0.11-r1
PKG_RELEASE:=15 PKG_RELEASE:=16
LUCI_DEPENDS:=+kaiplus +luci-compat +luci-lib-taskd +luci-lib-linkeaseauth +linkease-app-entry LUCI_DEPENDS:=+kaiplus +luci-compat +luci-lib-taskd +luci-lib-linkeaseauth +linkease-app-entry
LUCI_MINIFY_CSS:=0 LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0 LUCI_MINIFY_JS:=0
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.414 PKG_VERSION:=0.1.420
PKG_RELEASE:=69 PKG_RELEASE:=70
PKG_MAINTAINER:=liandu2024 PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.11.3 PKG_VERSION:=26.11.3
PKG_RELEASE:=370 PKG_RELEASE:=371
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -336,6 +336,17 @@ local com = self.com
font-weight: normal; font-weight: normal;
opacity: .7; opacity: .7;
} }
/* Keep action controls out of theme help-text folding. */
#app_version .app-update-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .5rem;
}
#app_version .app-update-actions > span {
min-width: 0;
overflow-wrap: anywhere;
}
</style> </style>
<div class="cbi-value"> <div class="cbi-value">
@@ -343,13 +354,13 @@ local com = self.com
<span class="app-version-sub"><%=api.get_version()%></span> <span class="app-version-sub"><%=api.get_version()%></span>
</label> </label>
<div class="cbi-value-field"> <div class="cbi-value-field">
<!--div class="cbi-value-description"--> <div class="app-update-actions">
<input class="btn cbi-button cbi-button-apply" type="button" id="<%=api.appname%>-check_btn" data-update-app="<%=api.appname%>" <input class="btn cbi-button cbi-button-apply" type="button" id="<%=api.appname%>-check_btn" data-update-app="<%=api.appname%>"
onclick="onBtnClick(this, appname);" value="<%:Check update%>" /> onclick="onBtnClick(this, appname);" value="<%:Check update%>" />
<input class="btn cbi-button cbi-button-apply" type="button" id="_<%=api.appname%>-force_btn" data-update-app="<%=api.appname%>" <input class="btn cbi-button cbi-button-apply" type="button" id="_<%=api.appname%>-force_btn" data-update-app="<%=api.appname%>"
onclick="doUpdate(this, appname);" value="<%:Force update%>" style="display:none" /> onclick="doUpdate(this, appname);" value="<%:Force update%>" style="display:none" />
<span id="<%=api.appname%>-check_btn-detail"></span> <span id="<%=api.appname%>-check_btn-detail"></span>
<!--/div--> </div>
</div> </div>
</div> </div>
@@ -362,13 +373,13 @@ local com = self.com
<span class="app-version-sub"><span class="com_version" key="<%=k%>"><%=translate("Null")%></span></span> <span class="app-version-sub"><span class="com_version" key="<%=k%>"><%=translate("Null")%></span></span>
</label> </label>
<div class="cbi-value-field"> <div class="cbi-value-field">
<!--div class="cbi-value-description"--> <div class="app-update-actions">
<input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-check_btn" data-update-app="<%=k%>" <input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-check_btn" data-update-app="<%=k%>"
onclick="onBtnClick(this,'<%=k%>');" value="<%:Check update%>" /> onclick="onBtnClick(this,'<%=k%>');" value="<%:Check update%>" />
<input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-force_btn" data-update-app="<%=k%>" <input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-force_btn" data-update-app="<%=k%>"
onclick="doUpdate(this,'<%=k%>');" value="<%:Force update%>" style="display:none"/> onclick="doUpdate(this,'<%=k%>');" value="<%:Force update%>" style="display:none"/>
<span id="_<%=k%>-check_btn-detail"></span> <span id="_<%=k%>-check_btn-detail"></span>
<!--/div--> </div>
</div> </div>
</div> </div>
<%end%> <%end%>
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.11.3 PKG_VERSION:=26.11.3
PKG_RELEASE:=172 PKG_RELEASE:=173
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -344,6 +344,17 @@ local com = self.com
font-weight: normal; font-weight: normal;
opacity: .7; opacity: .7;
} }
/* Keep action controls out of theme help-text folding. */
#app_version .app-update-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .5rem;
}
#app_version .app-update-actions > span {
min-width: 0;
overflow-wrap: anywhere;
}
</style> </style>
<div class="cbi-value"> <div class="cbi-value">
@@ -351,7 +362,7 @@ local com = self.com
<span class="app-version-sub"><%=api.get_version()%></span> <span class="app-version-sub"><%=api.get_version()%></span>
</label> </label>
<div class="cbi-value-field"> <div class="cbi-value-field">
<div class="cbi-value-description"> <div class="app-update-actions">
<input class="btn cbi-button cbi-button-apply" type="button" id="<%=api.appname%>-check_btn" data-update-app="<%=api.appname%>" <input class="btn cbi-button cbi-button-apply" type="button" id="<%=api.appname%>-check_btn" data-update-app="<%=api.appname%>"
onclick="onBtnClick(this, appname);" value="<%:Check update%>" /> onclick="onBtnClick(this, appname);" value="<%:Check update%>" />
<input class="btn cbi-button cbi-button-apply" type="button" id="_<%=api.appname%>-force_btn" data-update-app="<%=api.appname%>" <input class="btn cbi-button cbi-button-apply" type="button" id="_<%=api.appname%>-force_btn" data-update-app="<%=api.appname%>"
@@ -370,7 +381,7 @@ local com = self.com
<span class="app-version-sub"><span class="com_version" key="<%=k%>"><%=translate("Null")%></span></span> <span class="app-version-sub"><span class="com_version" key="<%=k%>"><%=translate("Null")%></span></span>
</label> </label>
<div class="cbi-value-field"> <div class="cbi-value-field">
<div class="cbi-value-description"> <div class="app-update-actions">
<input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-check_btn" data-update-app="<%=k%>" <input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-check_btn" data-update-app="<%=k%>"
onclick="onBtnClick(this,'<%=k%>');" value="<%:Check update%>" /> onclick="onBtnClick(this,'<%=k%>');" value="<%:Check update%>" />
<input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-force_btn" data-update-app="<%=k%>" <input class="btn cbi-button cbi-button-apply" type="button" id="_<%=k%>-force_btn" data-update-app="<%=k%>"
+1 -1
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=198 PKG_VERSION:=198
PKG_RELEASE:=101 PKG_RELEASE:=102
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -20,8 +20,7 @@ local CLASH_RULES_DIR = "/etc/ssrplus/clash"
local SUPPORTED_COMPONENTS = { local SUPPORTED_COMPONENTS = {
mainprogram = true, mainprogram = true,
xray = true, xray = true,
mihomo = true, mihomo = true
naiveproxy = true
} }
local SUPPORTED_GEO_COMPONENTS = { local SUPPORTED_GEO_COMPONENTS = {
{ name = "country_mmdb", require = nil }, { name = "country_mmdb", require = nil },
@@ -12,10 +12,6 @@
mihomo: { mihomo: {
label: '<%:Mihomo%>', label: '<%:Mihomo%>',
help: '<%:The online upgrade downloads and installs the matching Mihomo IPK or APK package for this system from fw876/helloworld releases.%>' help: '<%:The online upgrade downloads and installs the matching Mihomo IPK or APK package for this system from fw876/helloworld releases.%>'
},
naiveproxy: {
label: '<%:NaiveProxy%>',
help: '<%:The online upgrade downloads and installs the matching NaiveProxy package for this system from fw876/helloworld releases.%>'
} }
}; };
@@ -53,7 +49,6 @@
'Extract failed': '<%:Extract failed%>', 'Extract failed': '<%:Extract failed%>',
'xray binary not found in archive': '<%:xray binary not found in archive%>', 'xray binary not found in archive': '<%:xray binary not found in archive%>',
'mihomo binary not found in archive': '<%:mihomo binary not found in archive%>', 'mihomo binary not found in archive': '<%:mihomo binary not found in archive%>',
'naive binary not found in archive': '<%:naive binary not found in archive%>',
'Install failed': '<%:Install failed%>', 'Install failed': '<%:Install failed%>',
'Installed binary failed to run': '<%:Installed binary failed to run%>', 'Installed binary failed to run': '<%:Installed binary failed to run%>',
'Unsupported package manager': '<%:Unsupported package manager%>', 'Unsupported package manager': '<%:Unsupported package manager%>',
@@ -468,7 +463,6 @@
<option value="mainprogram"><%:SSR Plus+ Main Program%></option> <option value="mainprogram"><%:SSR Plus+ Main Program%></option>
<option value="xray"><%:Xray-core%></option> <option value="xray"><%:Xray-core%></option>
<option value="mihomo"><%:Mihomo%></option> <option value="mihomo"><%:Mihomo%></option>
<option value="naiveproxy"><%:NaiveProxy%></option>
</select> </select>
</div> </div>
</div> </div>
@@ -2677,12 +2677,6 @@ msgid ""
"package for this system from fw876/helloworld releases." "package for this system from fw876/helloworld releases."
msgstr "" msgstr ""
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:18
msgid ""
"The online upgrade downloads and installs the matching NaiveProxy package "
"for this system from fw876/helloworld releases."
msgstr ""
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:816 #: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:816
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:871 #: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:871
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:89 #: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:89
@@ -3265,10 +3259,6 @@ msgstr ""
msgid "mihomo binary not found in archive" msgid "mihomo binary not found in archive"
msgstr "" msgstr ""
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:56
msgid "naive binary not found in archive"
msgstr ""
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:911 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:911
msgid "native UDP characteristics" msgid "native UDP characteristics"
msgstr "" msgstr ""
-11
View File
@@ -2701,13 +2701,6 @@ msgid ""
msgstr "" msgstr ""
"在线升级会从 fw876/helloworld 发布页下载并安装适配当前系统的 Mihomo IPK 或 APK 软件包。" "在线升级会从 fw876/helloworld 发布页下载并安装适配当前系统的 Mihomo IPK 或 APK 软件包。"
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:18
msgid ""
"The online upgrade downloads and installs the matching NaiveProxy package "
"for this system from fw876/helloworld releases."
msgstr ""
"在线升级会从 fw876/helloworld 发布页下载并安装适配当前系统的 NaiveProxy 软件包。"
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:816 #: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:816
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:871 #: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:871
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:89 #: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:89
@@ -3292,10 +3285,6 @@ msgstr "mKCP 伪装(混淆)类型"
msgid "mihomo binary not found in archive" msgid "mihomo binary not found in archive"
msgstr "压缩包中未找到 mihomo 二进制文件" msgstr "压缩包中未找到 mihomo 二进制文件"
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:56
msgid "naive binary not found in archive"
msgstr "压缩包中未找到 naive 二进制文件"
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:911 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:911
msgid "native UDP characteristics" msgid "native UDP characteristics"
msgstr "原生 UDP 特性" msgstr "原生 UDP 特性"
@@ -103,17 +103,6 @@ version_gt() {
[ "$first" = "$left" ] [ "$first" = "$left" ]
} }
naiveproxy_versions_equal() {
local left right
left="$(trim_version "${1:-}")"
right="$(trim_version "${2:-}")"
[ -n "$left" ] || return 1
[ -n "$right" ] || return 1
[ "$left" = "$right" ] && return 0
[ "${left%%-*}" = "${right%%-*}" ]
}
get_openwrt_arch() { get_openwrt_arch() {
local arch local arch
@@ -163,22 +152,6 @@ find_mihomo_binary() {
return 1 return 1
} }
find_naiveproxy_binary() {
if command -v naive >/dev/null 2>&1; then
command -v naive
return 0
fi
for path in /usr/bin/naive /usr/libexec/naive /etc/ssrplus/bin/naive; do
if [ -x "$path" ]; then
printf '%s' "$path"
return 0
fi
done
return 1
}
get_xray_current_version() { get_xray_current_version() {
if [ ! -x "$XRAY_BINARY" ]; then if [ ! -x "$XRAY_BINARY" ]; then
return 1 return 1
@@ -211,29 +184,6 @@ get_mihomo_current_version() {
return 0 return 0
} }
get_naiveproxy_current_version() {
local pm version binary
pm="$(detect_package_manager 2>/dev/null || true)"
case "$pm" in
apk)
version="$(apk list -I naiveproxy 2>/dev/null | sed -n 's/^naiveproxy-\([^[:space:]]*\).*$/\1/p' | sed -n '1p')"
;;
opkg)
version="$(opkg status naiveproxy 2>/dev/null | sed -n 's/^Version:[[:space:]]*//p' | sed -n '1p')"
;;
esac
if [ -n "${version:-}" ]; then
printf '%s' "$version"
return 0
fi
binary="$(find_naiveproxy_binary)" || return 1
"$binary" --version 2>&1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?' | sed -n '1p'
return 0
}
require_cmd() { require_cmd() {
command -v "$1" >/dev/null 2>&1 command -v "$1" >/dev/null 2>&1
} }
@@ -1130,71 +1080,6 @@ get_mihomo_latest_info() {
return 0 return 0
} }
select_naiveproxy_asset() {
local asset_list="$1"
local pm="$2"
local arch="$3"
local candidate
case "$pm" in
apk)
candidate="$(printf '%s\n' "$asset_list" | grep -E '^naiveproxy-[^[:space:]]+\.apk$' | grep -F "_${arch}.apk" | sed -n '1p')"
;;
opkg)
candidate="$(printf '%s\n' "$asset_list" | grep -E '^naiveproxy_[^[:space:]]+\.ipk$' | grep -F "_${arch}.ipk" | sed -n '1p')"
;;
*)
return 1
;;
esac
[ -n "$candidate" ] || return 1
printf '%s' "$candidate"
}
naiveproxy_asset_version() {
local asset="$1"
local arch="$2"
local version
case "$asset" in
naiveproxy_*_"$arch".ipk)
version="${asset#naiveproxy_}"
version="${version%_${arch}.ipk}"
;;
naiveproxy-*_"$arch".apk)
version="${asset#naiveproxy-}"
version="${version%_${arch}.apk}"
;;
*)
return 1
;;
esac
[ -n "$version" ] || return 1
printf '%s' "$version"
}
get_naiveproxy_latest_info() {
local pm arch tag version asset asset_list release_html url
pm="$(detect_package_manager)" || return 2
arch="$(get_openwrt_arch)"
tag="$(get_helloworld_latest_tag)" || return 3
release_html="$(fetch_text "https://github.com/fw876/helloworld/releases/expanded_assets/$tag")" || return 3
asset_list="$(printf '%s\n' "$release_html" | sed -n 's#.*href="/fw876/helloworld/releases/download/[^/]*/\([^"]*\)".*#\1#p')"
asset="$(select_naiveproxy_asset "$asset_list" "$pm" "$arch")" || return 4
version="$(naiveproxy_asset_version "$asset" "$arch")" || return 4
url="$(mirror_wrap_url "https://github.com/fw876/helloworld/releases/download/$tag/$asset")"
log_kv package_manager "$pm"
log_kv arch "$arch"
log_kv asset "$asset"
log_kv latest_version "$version"
log_kv download_url "$url"
return 0
}
get_mainprogram_current_version() { get_mainprogram_current_version() {
local pm="$1" local pm="$1"
local version local version
@@ -1522,54 +1407,6 @@ mihomo_info() {
log_kv error '' log_kv error ''
} }
naiveproxy_info() {
local pm current installed latest_output latest_rc latest_version arch asset can_upgrade
installed=0
current=""
pm="$(detect_package_manager 2>/dev/null || true)"
arch="$(get_openwrt_arch)"
if current="$(get_naiveproxy_current_version)" && [ -n "$current" ]; then
installed=1
fi
latest_output="$(get_naiveproxy_latest_info 2>/dev/null)"
latest_rc=$?
log_kv component naiveproxy
log_kv installed "$installed"
log_kv current_version "$current"
log_kv package_manager "$pm"
log_kv arch "$arch"
log_kv asset ''
if [ $latest_rc -ne 0 ]; then
log_kv can_upgrade 0
case "$latest_rc" in
2) log_kv error 'unsupported_package_manager' ;;
3) log_kv error 'fetch_failed' ;;
4) log_kv error 'asset_not_found' ;;
*) log_kv error 'unknown_error' ;;
esac
return 0
fi
latest_version="$(printf '%s\n' "$latest_output" | sed -n 's/^latest_version=//p' | sed -n '1p')"
pm="$(printf '%s\n' "$latest_output" | sed -n 's/^package_manager=//p' | sed -n '1p')"
arch="$(printf '%s\n' "$latest_output" | sed -n 's/^arch=//p' | sed -n '1p')"
asset="$(printf '%s\n' "$latest_output" | sed -n 's/^asset=//p' | sed -n '1p')"
can_upgrade=0
if [ -z "$current" ]; then
can_upgrade=1
elif ! naiveproxy_versions_equal "$latest_version" "$current" && version_gt "$latest_version" "$current"; then
can_upgrade=1
fi
printf '%s\n' "$latest_output" | sed '/^download_url=/d'
log_kv can_upgrade "$can_upgrade"
log_kv error ''
}
xray_local_info() { xray_local_info() {
local pm current installed arch local pm current installed arch
@@ -1614,28 +1451,6 @@ mihomo_local_info() {
log_kv error '' log_kv error ''
} }
naiveproxy_local_info() {
local pm current installed arch
installed=0
current=""
pm="$(detect_package_manager 2>/dev/null || true)"
arch="$(get_openwrt_arch)"
if current="$(get_naiveproxy_current_version)" && [ -n "$current" ]; then
installed=1
fi
log_kv component naiveproxy
log_kv installed "$installed"
log_kv current_version "$current"
log_kv latest_version ''
log_kv package_manager "$pm"
log_kv arch "$arch"
log_kv asset ''
log_kv can_upgrade 0
log_kv error ''
}
xray_upgrade() { xray_upgrade() {
local latest_output latest_rc latest_version download_url package_manager asset tmp_dir package_file current_before current_after local latest_output latest_rc latest_version download_url package_manager asset tmp_dir package_file current_before current_after
@@ -1784,80 +1599,6 @@ mihomo_upgrade() {
return 0 return 0
} }
naiveproxy_upgrade() {
local latest_output latest_rc latest_version download_url package_manager asset tmp_dir package_file current_before current_after
latest_output="$(get_naiveproxy_latest_info 2>/dev/null)"
latest_rc=$?
if [ $latest_rc -ne 0 ]; then
log_kv success 0
case "$latest_rc" in
2) log_kv message 'Unsupported package manager' ;;
3) log_kv message 'Failed to fetch release metadata' ;;
4) log_kv message 'Matching release asset not found' ;;
*) log_kv message 'Unknown error' ;;
esac
return 0
fi
latest_version="$(printf '%s\n' "$latest_output" | sed -n 's/^latest_version=//p' | sed -n '1p')"
download_url="$(printf '%s\n' "$latest_output" | sed -n 's/^download_url=//p' | sed -n '1p')"
package_manager="$(printf '%s\n' "$latest_output" | sed -n 's/^package_manager=//p' | sed -n '1p')"
asset="$(printf '%s\n' "$latest_output" | sed -n 's/^asset=//p' | sed -n '1p')"
current_before="$(get_naiveproxy_current_version 2>/dev/null || true)"
if [ -n "$current_before" ] && ! version_gt "$latest_version" "$current_before"; then
log_kv success 1
log_kv previous_version "$current_before"
log_kv current_version "$current_before"
log_kv latest_version "$latest_version"
log_kv message 'Already up to date'
return 0
fi
tmp_dir="$(mktemp -d /tmp/ssrplus-naiveproxy.XXXXXX)"
if [ -z "$tmp_dir" ] || [ ! -d "$tmp_dir" ]; then
log_kv success 0
log_kv message 'Failed to create temp directory'
return 0
fi
package_file="$tmp_dir/$asset"
trap "rm -rf '$tmp_dir'" EXIT INT TERM
if ! download_file "$download_url" "$package_file"; then
log_kv success 0
log_kv message 'Download failed'
return 0
fi
if [ ! -s "$package_file" ] || ! mainprogram_install_package "$package_manager" "$package_file"; then
log_kv success 0
log_kv message 'Install failed'
return 0
fi
current_after="$(get_naiveproxy_current_version 2>/dev/null || true)"
if [ -z "$current_after" ]; then
log_kv success 0
log_kv message 'Installed package check failed'
return 0
fi
if [ -x /etc/init.d/shadowsocksr ]; then
/etc/init.d/shadowsocksr restart >/dev/null 2>&1 || true
fi
log_kv success 1
log_kv previous_version "$current_before"
log_kv current_version "$current_after"
log_kv latest_version "$latest_version"
log_kv package_manager "$package_manager"
log_kv arch "$(get_openwrt_arch)"
log_kv asset "$asset"
log_kv message 'Upgrade completed'
return 0
}
# ==================== 主程序入口 ==================== # ==================== 主程序入口 ====================
case "${1:-}" in case "${1:-}" in
# 主程序相关 # 主程序相关
@@ -1890,16 +1631,6 @@ case "${1:-}" in
mihomo_upgrade) mihomo_upgrade)
mihomo_upgrade mihomo_upgrade
;; ;;
# Naiveproxy 相关
naiveproxy_info)
naiveproxy_info
;;
naiveproxy_local_info)
naiveproxy_local_info
;;
naiveproxy_upgrade)
naiveproxy_upgrade
;;
# Country MMDB 相关 # Country MMDB 相关
country_mmdb_info) country_mmdb_info)
country_mmdb_info country_mmdb_info
@@ -1942,7 +1673,7 @@ case "${1:-}" in
;; ;;
*) *)
log_kv success 0 log_kv success 0
log_kv message 'Usage: update_components.sh mainprogram_info|mainprogram_local_info|mainprogram_upgrade|xray_info|xray_local_info|xray_upgrade|mihomo_info|mihomo_local_info|mihomo_upgrade|naiveproxy_info|naiveproxy_local_info|naiveproxy_upgrade|country_mmdb_info|country_mmdb_local_info|country_mmdb_upgrade|geosite_info|geosite_local_info|geosite_upgrade|v2ray_geoip_info|v2ray_geoip_local_info|v2ray_geoip_upgrade|v2ray_geosite_info|v2ray_geosite_local_info|v2ray_geosite_upgrade' log_kv message 'Usage: update_components.sh mainprogram_info|mainprogram_local_info|mainprogram_upgrade|xray_info|xray_local_info|xray_upgrade|mihomo_info|mihomo_local_info|mihomo_upgrade|country_mmdb_info|country_mmdb_local_info|country_mmdb_upgrade|geosite_info|geosite_local_info|geosite_upgrade|v2ray_geoip_info|v2ray_geoip_local_info|v2ray_geoip_upgrade|v2ray_geosite_info|v2ray_geosite_local_info|v2ray_geosite_upgrade'
return 1 2>/dev/null || exit 1 return 1 2>/dev/null || exit 1
;; ;;
esac esac
+1
View File
@@ -1,3 +1,4 @@
.deploy/target.env .deploy/target.env
.deploy/.ssh/ .deploy/.ssh/
.private .private
/artifacts/
+5 -2
View File
@@ -9,8 +9,11 @@ LUCI_TITLE:=LuCI for Tailscale (NG)
LUCI_DEPENDS:=+tailscale LUCI_DEPENDS:=+tailscale
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_VERSION:=0.1.1 PKG_VERSION:=0.1.2
PKG_RELEASE:=3
# match the i18n pkg version to the main pkg version
PKG_RELEASE:=4
PKG_PO_VERSION:=$(PKG_VERSION)-r$(PKG_RELEASE)
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk
+5
View File
@@ -101,9 +101,14 @@ Download the latest `.apk` package from [Releases](https://github.com/vad-b/luci
apk add --allow-untrusted --upgrade /tmp/luci-app-tailscale-ng_*.apk apk add --allow-untrusted --upgrade /tmp/luci-app-tailscale-ng_*.apk
``` ```
Simplified Chinese translation is also available as a separate `luci-i18n-tailscale-ng-zh-cn` package in both `.apk` and `.ipk` formats in [Releases](https://github.com/vad-b/luci-app-tailscale-ng/releases).
For local development builds and testing, see [Local package builds](docs/local-build.md).
## Credits ## Credits
- [asvow/luci-app-tailscale](https://github.com/asvow/luci-app-tailscale) - the project that inspired this work - [asvow/luci-app-tailscale](https://github.com/asvow/luci-app-tailscale) - the project that inspired this work
- [GFander](https://github.com/GFander) - Simplified Chinese translation
## License ## License
+65
View File
@@ -0,0 +1,65 @@
# Local package builds
Build locally with the same script used by GitHub Actions. No push or release
is needed.
## Prepare
Use Ubuntu 24.04 x86_64, including under WSL2, as a regular user. Run the
commands below in the Linux shell. Install dependencies once:
```sh
sudo apt-get update
sudo apt-get install -y git build-essential flex bison gawk gcc-multilib g++-multilib gettext libncurses-dev libssl-dev python3-setuptools rsync unzip zlib1g-dev file wget curl zstd
```
## Build
Commit the changes to test locally: the script builds the current commit and
requires a clean working tree, including untracked files. From the repository
root, build APK packages for OpenWrt 25.12:
```sh
JOBS=2 bash scripts/build-packages.sh apk
```
For IPK packages targeting OpenWrt 24.10, use:
```sh
JOBS=2 bash scripts/build-packages.sh ipk
```
Outputs go to `artifacts/apk/` or `artifacts/ipk/`: the application package,
Chinese translation, `SHA256SUMS` and `build-info.txt` with the source commit
and SDK details. SDK versions and checksums are fixed in the build script.
The build host must be x86_64; the two output packages are architecture
independent. Obtain native dependencies such as Tailscale from the router's
own feeds.
## Check and test
For an APK build, verify the outputs and source commit:
```sh
cd artifacts/apk
sha256sum --check SHA256SUMS
cat build-info.txt
cd ../..
```
Back up the router's Tailscale configuration and authorization state before
testing. Transfer both packages and install them using the
[manual installation instructions](../README.md#manual-installation).
Local APK packages are unsigned and require `--allow-untrusted`.
Check installed versions, preserved settings and login, service operation,
and the translated LuCI page. If Software hides the translation package,
select **Display LuCI translation packages → all**.
## Cache
The first build is slower; later builds reuse `~/.cache/luci-app-tailscale-ng`.
Keep this cache on a Linux filesystem at a path without spaces. Delete that
directory for a fresh SDK, or `artifacts/` to discard generated packages.
Optional overrides are `BUILD_CACHE_DIR`, `OUTPUT_DIR` and `JOBS`.
@@ -0,0 +1,314 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:199
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:618
msgid "-- None --"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:587
msgid "Accept DNS"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:588
msgid "Accept DNS configuration from the control server."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:601
msgid "Accept Subnet Routes"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:602
msgid "Accept subnet routes that other nodes advertise."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:648
msgid "Additional Flags"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:596
msgid "Advertise Exit Node"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:606
msgid "Advertised Routes"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:164
msgid "Are you sure you want to log out and clean the local state?"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:522
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:528
msgid "Authentication"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:534
msgid "Base URL of login (control) server."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:283
msgid "Checking login..."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:368
msgid "Checking status..."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:355
msgid "Click here to log in"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:131
msgid "Command exited with code"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:585
msgid "DNS"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:552
msgid ""
"Disconnect from login server, expire current login, and flush local state "
"cache."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:173
msgid "Error:"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:151
msgid "Error: unable to load status data."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:616
msgid "Exit Node"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:646
msgid "Extra"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:650
msgid "Extra flags in format --flag=value. See %s for available options."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:575
msgid "Firewall Mode"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:560
msgid "General"
msgstr ""
#: root/usr/share/rpcd/acl.d/luci-app-tailscale-ng.json:3
msgid "Grant access to Tailscale NG configuration"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:562
msgid "Listen Port"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:387
msgid "Loading..."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:279
msgid "Logged in"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:281
msgid "Logged out"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:630
msgid "Logging"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:167
msgid "Logging out and cleaning state..."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:634
msgid "Logging program activities."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:640
msgid "Logging program errors and exceptions."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:533
msgid "Login Server"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:530
msgid "Login Status"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:549
msgid "Logout"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:546
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:550
msgid "Logout and Clean"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:363
msgid "Manage in Admin Console"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:514
msgid "N/A"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:391
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:707
msgid "No output available. Service may not be running."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:270
msgid "Not running"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:597
msgid "Offer to be an exit node for outbound internet traffic."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:571
msgid "Path to state file. WARNING: Changing this requires reauthentication."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:538
msgid "Pre-authentication Key"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:313
msgid "Restart"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:315
msgid "Restarting Tailscale service..."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:594
msgid "Routing"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:273
msgid "Running"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:617
msgid "Select an online machine to use as an exit node."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:577
msgid "Select the firewall backend to use."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:366
msgid "Service not running"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:539
msgid "Set the machine authorization key."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:523
msgid "Settings"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:326
msgid "Start"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:504
msgid "Start Tailscale service on boot."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:328
msgid "Starting Tailscale service..."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:569
msgid "State File"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:524
msgid "Status"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:638
msgid "StdErr Logging"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:632
msgid "StdOut Logging"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:319
msgid "Stop"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:321
msgid "Stopping Tailscale service..."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:607
msgid ""
"Subnets to advertise to other Tailscale nodes, e.g. <code>192.168.1.0/24</"
"code>."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:681
msgid "Tailscale DNS Status"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:503
msgid "Tailscale Enabled"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:663
msgid "Tailscale IP"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:669
msgid "Tailscale Interface"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:492
#: root/usr/share/luci/menu.d/luci-app-tailscale-ng.json:3
msgid "Tailscale NG"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:675
msgid "Tailscale Netcheck"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:508
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:657
msgid "Tailscale Status"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:512
msgid "Tailscale Version"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:651
msgid "Tailscale documentation"
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:493
msgid "Tailscale is a cross-platform and easy to use virtual LAN."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:564
msgid "UDP port to listen on for WireGuard and peer-to-peer traffic."
msgstr ""
#: htdocs/luci-static/resources/view/tailscale-ng/index.js:540
msgid ""
"Use a non-reusable auth key and disable key expiry for trusted machines via "
"the provider admin console."
msgstr ""
@@ -3,16 +3,16 @@
# LuCI for Tailscale (NG) - Simplified Chinese (简体中文) translation # LuCI for Tailscale (NG) - Simplified Chinese (简体中文) translation
# #
# Extracted with: build/i18n-scan.pl from the openwrt/luci repository # Extracted with: build/i18n-scan.pl from the openwrt/luci repository
# Language: zh_CN / zh-cn # Language: zh_Hans (Simplified Chinese)
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: luci-app-tailscale-ng 0.1.1\n" "Project-Id-Version: luci-app-tailscale-ng 0.1.2\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-15 00:00+0000\n" "POT-Creation-Date: 2026-08-15 00:00+0000\n"
"PO-Revision-Date: 2026-08-15 00:00+0000\n" "PO-Revision-Date: 2026-08-15 00:00+0000\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Chinese (Simplified) <zh-cn>\n" "Language-Team: Chinese (Simplified)\n"
"Language: zh_CN\n" "Language: zh_Hans\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-only
set -euo pipefail
# Use Linux tools only, including when invoked from WSL with Windows PATH entries.
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export LC_ALL=C
format="${1:-apk}"
case "$format" in
apk)
sdk_version=25.12.6
sdk_file=openwrt-sdk-25.12.6-x86-64_gcc-14.3.0_musl.Linux-x86_64.tar.zst
sdk_url="https://downloads.openwrt.org/releases/$sdk_version/targets/x86/64/$sdk_file"
sdk_sha256=1864152619092e781fa36a401960c8e9ae42d6a364c44ee5f0b197ca5f3c0fcc
;;
ipk)
sdk_version=24.10.4
sdk_file=openwrt-sdk-24.10.4-x86-64_gcc-13.3.0_musl.Linux-x86_64.tar.zst
sdk_url="https://archive.openwrt.org/releases/$sdk_version/targets/x86/64/$sdk_file"
sdk_sha256=229e871f734a2cee5ce3ad6a3e98d3836b0899bfdeaea4d9c2c5cc7b1fce1407
;;
*) printf 'Usage: %s [apk|ipk]\n' "$0" >&2; exit 1 ;;
esac
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cache_dir="${BUILD_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/luci-app-tailscale-ng}"
output_dir="${OUTPUT_DIR:-$source_dir/artifacts/$format}"
jobs="${JOBS:-$(nproc)}"
if [[ "$(id -u)" == 0 ]]; then
printf 'Run the SDK build as a regular Linux user.\n' >&2
exit 1
fi
if [[ -n "$(git -C "$source_dir" status --porcelain)" ]]; then
printf 'Commit the source changes first; the build uses an exact Git snapshot.\n' >&2
exit 1
fi
source_commit="$(git -C "$source_dir" rev-parse HEAD)"
mkdir -p "$cache_dir/downloads" "$output_dir"
cache_dir="$(realpath "$cache_dir")"
output_dir="$(realpath "$output_dir")"
sdk_dir="$cache_dir/sdk-$sdk_version"
archive="$cache_dir/downloads/$sdk_file"
if [[ "$sdk_dir" == *[[:space:]]* ]]; then
printf 'The SDK cache path must not contain whitespace.\n' >&2
exit 1
fi
if [[ ! -f "$archive" ]]; then
curl --fail --location --retry 3 "$sdk_url" --output "$archive.part"
mv "$archive.part" "$archive"
fi
printf '%s %s\n' "$sdk_sha256" "$archive" | sha256sum --check
if [[ ! -f "$sdk_dir/.sdk-ready" ]]; then
mkdir -p "$sdk_dir"
tar --zstd -xf "$archive" -C "$sdk_dir" --strip-components=1
touch "$sdk_dir/.sdk-ready"
fi
# Export only committed source: no deployment credentials or private notes.
package_dir="$sdk_dir/package/luci-app-tailscale-ng"
rm -rf -- "$package_dir"
mkdir -p "$package_dir"
git -C "$source_dir" archive "$source_commit" | tar -xf - -C "$package_dir"
package_version="$(sed -n 's/^PKG_VERSION:=//p' "$package_dir/Makefile")"
package_release="$(sed -n 's/^PKG_RELEASE:=//p' "$package_dir/Makefile")"
cd "$sdk_dir"
# Keep the release SDK's pinned feed revisions instead of tracking moving branches.
if [[ ! -f .feeds-ready ]]; then
./scripts/feeds update -a
touch .feeds-ready
fi
./scripts/feeds uninstall -a
./scripts/feeds install tailscale luci-base
cat > .config <<EOF
CONFIG_ALL_NONSHARED=n
CONFIG_ALL_KMODS=n
CONFIG_ALL=n
CONFIG_AUTOREMOVE=n
CONFIG_SIGNED_PACKAGES=n
CONFIG_PACKAGE_luci-app-tailscale-ng=m
CONFIG_LUCI_LANG_zh_Hans=y
CONFIG_PACKAGE_luci-i18n-tailscale-ng-zh-cn=m
EOF
make defconfig
grep -qx 'CONFIG_PACKAGE_luci-i18n-tailscale-ng-zh-cn=m' .config
make package/luci-app-tailscale-ng/clean
make -j"$jobs" package/luci-app-tailscale-ng/compile V=s
# Require both outputs; a successful app build alone does not deliver a translation.
find "$output_dir" -maxdepth 1 -type f \( \
-name "luci-app-tailscale-ng[-_]*.$format" -o \
-name "luci-i18n-tailscale-ng-zh-cn[-_]*.$format" \) -delete
for package in luci-app-tailscale-ng luci-i18n-tailscale-ng-zh-cn; do
mapfile -t files < <(find bin/packages -type f -name "${package}[-_]${package_version}-r${package_release}[._]*${format}")
if [[ "${#files[@]}" != 1 ]]; then
printf 'Expected one %s package, found %s.\n' "$package" "${#files[@]}" >&2
exit 1
fi
cp "${files[0]}" "$output_dir/"
done
{
printf 'source_commit=%s\n' "$source_commit"
printf 'sdk_version=%s\nsdk_sha256=%s\nformat=%s\n' "$sdk_version" "$sdk_sha256" "$format"
cat feeds.conf.default
} > "$output_dir/build-info.txt"
(cd "$output_dir" && sha256sum -- *."$format" > SHA256SUMS)
printf '\nPackages saved to %s\n' "$output_dir"
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-trafficctl PKG_NAME:=luci-app-trafficctl
PKG_VERSION:=1.26.4 PKG_VERSION:=1.28.0
PKG_RELEASE:=22 PKG_RELEASE:=23
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Denis Iusupov <yusdyr@gmail.com> PKG_MAINTAINER:=Denis Iusupov <yusdyr@gmail.com>
@@ -172,8 +172,9 @@ else
# The download rule is listed first; the upload one is the "_ul" name. # The download rule is listed first; the upload one is the "_ul" name.
RL_DUMP=$(iptables -t mangle -L FORWARD -nv 2>/dev/null | grep "rl_ratelimit" \ RL_DUMP=$(iptables -t mangle -L FORWARD -nv 2>/dev/null | grep "rl_ratelimit" \
| awk -v ip="$IP" '{ for (i = 1; i <= NF; i++) if ($i == ip || $i == ip "/32") { print; next } }') | awk -v ip="$IP" '{ for (i = 1; i <= NF; i++) if ($i == ip || $i == ip "/32") { print; next } }')
RATE_LIM=$(echo "$RL_DUMP" | grep -oE '[0-9]+kbit' | head -1 | sed 's/kbit//') RATE_LIM=$(echo "$RL_DUMP" | awk "$TCTL_NFT_RATE_AWK"'{ r = tctl_ipt_first_rate(); if (r >= 0) { print r; exit } }')
RATE_LIM_UP=$(echo "$RL_DUMP" | grep -E 'rl_ratelimit.*_ul' | grep -oE '[0-9]+kbit' | head -1 | sed 's/kbit//') RATE_LIM_UP=$(echo "$RL_DUMP" | grep -E 'rl_ratelimit.*_ul' \
| awk "$TCTL_NFT_RATE_AWK"'{ r = tctl_ipt_first_rate(); if (r >= 0) { print r; exit } }')
fi fi
[ -z "$RATE_LIM" ] && RATE_LIM=0 [ -z "$RATE_LIM" ] && RATE_LIM=0
[ -z "$RATE_LIM_UP" ] && RATE_LIM_UP=0 [ -z "$RATE_LIM_UP" ] && RATE_LIM_UP=0
@@ -279,12 +279,14 @@ tctl_ratelimit_add() {
TCTL_RL_UPLOAD6_OK=0 TCTL_RL_UPLOAD6_OK=0
# shellcheck disable=SC2034 # shellcheck disable=SC2034
TCTL_RL_DOWNLOAD6_OK=0 TCTL_RL_DOWNLOAD6_OK=0
# shellcheck disable=SC2046 # deliberate split into the option words
iptables -t mangle -A FORWARD -d "$ip" -m hashlimit \ iptables -t mangle -A FORWARD -d "$ip" -m hashlimit \
--hashlimit-above "${rate_kbit}kbit/sec" --hashlimit-burst "${rate_kbit}kbit" \ $(tctl_ipt_limit_args "$rate_kbit") \
--hashlimit-mode dstip --hashlimit-name "rl_${comment}" \ --hashlimit-mode dstip --hashlimit-name "rl_${comment}" \
-j DROP -m comment --comment "$comment" 2>/dev/null -j DROP -m comment --comment "$comment" 2>/dev/null
# shellcheck disable=SC2046 # deliberate split into the option words
iptables -t mangle -A FORWARD -s "$ip" -m hashlimit \ iptables -t mangle -A FORWARD -s "$ip" -m hashlimit \
--hashlimit-above "${rate_kbit_up}kbit/sec" --hashlimit-burst "${rate_kbit_up}kbit" \ $(tctl_ipt_limit_args "$rate_kbit_up") \
--hashlimit-mode srcip --hashlimit-name "rl_${comment}_ul" \ --hashlimit-mode srcip --hashlimit-name "rl_${comment}_ul" \
-j DROP -m comment --comment "${comment}_ul" 2>/dev/null -j DROP -m comment --comment "${comment}_ul" 2>/dev/null
fi fi
@@ -382,8 +384,50 @@ function tctl_rate_kbit(n, u, b) {
# Rounded here, so a reader printing %d agrees with one printing %.0f. # Rounded here, so a reader printing %d agrees with one printing %.0f.
return int(b * 8 / 1000 + 0.5) return int(b * 8 / 1000 + 0.5)
} }
# An iptables hashlimit rate as listed, e.g. "624992b/s" or "1213mb/s".
# iptables prints bytes in 1024 steps too. A packet rate ("5000/sec") is not a
# ceiling in kbit/s and reads 0.
function tctl_ipt_rate_kbit(t, n, u) {
if (!match(t, /^[0-9]+/)) return 0
n = substr(t, 1, RLENGTH); u = substr(t, RLENGTH + 1)
if (u ~ /^b\//) return tctl_rate_kbit(n, "bytes")
if (u ~ /^kb\//) return tctl_rate_kbit(n, "kbytes")
if (u ~ /^mb\//) return tctl_rate_kbit(n, "mbytes")
if (u ~ /^gb\//) return tctl_rate_kbit(n, "gbytes")
return 0
}
# The ceiling of the hashlimit rule on the current line: "above X" in an
# "iptables -L" listing, "--hashlimit-above X" in "iptables -S". -1 if none.
function tctl_ipt_first_rate( i) {
for (i = 1; i < NF; i++)
if ($i == "above" || $i == "--hashlimit-above") return tctl_ipt_rate_kbit($(i+1))
return -1
}
' '
# hashlimit arguments for a kbit/s ceiling, in BYTES.
#
# "${kbit}kbit/sec" is not a byte rate to hashlimit: it reads the number as
# PACKETS per second (iptables -S lists it back as "5000/sec"), and the
# "${kbit}kbit" burst written next to it is rejected outright ("bad value for
# option --hashlimit-burst", iptables 1.8.7 and 1.8.10). The 2>/dev/null after
# the rule hid that, so on fw3 no limit was ever installed.
#
# Bytes per second, SI like the nft side (tctl_rate_bytes). The burst is two
# seconds of the rate: hashlimit refuses a byte burst below about one second
# of it ("burst cannot be smaller than 125172064b" at 1 Gbit/s), and it is a
# 32-bit field, hence the cap, which puts the highest ceiling fw3 can
# express near 30 Gbit/s (10 Gbit/s is accepted, 40 is not). Verified on iptables-legacy 1.8.7 with iperf3
# through a forwarding netns: 5, 50 and 1000 Mbit/s held at 4.9-4.97, 49-50.6
# and 990-1003 Mbit/s.
tctl_ipt_limit_args() {
local b burst
b=$(tctl_rate_bytes "$1")
burst=$(( b * 2 ))
[ "$burst" -gt 4000000000 ] && burst=4000000000
echo "--hashlimit-above ${b}b/s --hashlimit-burst ${burst}b"
}
tctl_ratelimit_list() { tctl_ratelimit_list() {
if [ "$TCTL_FW" = "nft" ]; then if [ "$TCTL_FW" = "nft" ]; then
nft list table netdev tm_ratelimit 2>/dev/null nft list table netdev tm_ratelimit 2>/dev/null
@@ -115,6 +115,8 @@ BEGIN {
print "# TYPE trafficctl_device_blocked gauge" print "# TYPE trafficctl_device_blocked gauge"
print "# HELP trafficctl_device_limit_kbit Applied throttle in kbit/s (0 = none)." print "# HELP trafficctl_device_limit_kbit Applied throttle in kbit/s (0 = none)."
print "# TYPE trafficctl_device_limit_kbit gauge" print "# TYPE trafficctl_device_limit_kbit gauge"
print "# HELP trafficctl_device_limit_up_kbit Applied upload throttle in kbit/s (0 = none); equals the download one unless split."
print "# TYPE trafficctl_device_limit_up_kbit gauge"
print "# HELP trafficctl_device_wifi_blocked Whether the device is denied on WiFi." print "# HELP trafficctl_device_wifi_blocked Whether the device is denied on WiFi."
print "# TYPE trafficctl_device_wifi_blocked gauge" print "# TYPE trafficctl_device_wifi_blocked gauge"
print "# HELP trafficctl_device_conntrack_bytes Bytes currently accounted in conntrack (falls as flows expire)." print "# HELP trafficctl_device_conntrack_bytes Bytes currently accounted in conntrack (falls as flows expire)."
@@ -132,6 +134,10 @@ BEGIN {
printf "trafficctl_device_wifi_blocked{ip=\"%s\"} %d\n", ip, (index($0, "\"wifi_blocked\":true") ? 1 : 0) printf "trafficctl_device_wifi_blocked{ip=\"%s\"} %d\n", ip, (index($0, "\"wifi_blocked\":true") ? 1 : 0)
printf "trafficctl_device_limit_kbit{ip=\"%s\",mode=\"limiter\"} %d\n", ip, num($0, "rate_limit_kbit") printf "trafficctl_device_limit_kbit{ip=\"%s\",mode=\"limiter\"} %d\n", ip, num($0, "rate_limit_kbit")
printf "trafficctl_device_limit_kbit{ip=\"%s\",mode=\"shaper\"} %d\n", ip, num($0, "shape_kbit") printf "trafficctl_device_limit_kbit{ip=\"%s\",mode=\"shaper\"} %d\n", ip, num($0, "shape_kbit")
# Its own metric rather than a direction label on the one above: a new
# label would start new series for every existing dashboard and alert.
printf "trafficctl_device_limit_up_kbit{ip=\"%s\",mode=\"limiter\"} %d\n", ip, num($0, "rate_limit_kbit_up")
printf "trafficctl_device_limit_up_kbit{ip=\"%s\",mode=\"shaper\"} %d\n", ip, num($0, "shape_kbit_up")
# Conntrack view: current in-flight totals and the protocol split. # Conntrack view: current in-flight totals and the protocol split.
printf "trafficctl_device_conntrack_bytes{ip=\"%s\",proto=\"all\"} %.0f\n", ip, num($0, "total") printf "trafficctl_device_conntrack_bytes{ip=\"%s\",proto=\"all\"} %.0f\n", ip, num($0, "total")
printf "trafficctl_device_conntrack_bytes{ip=\"%s\",proto=\"tcp\"} %.0f\n", ip, num($0, "tcp") printf "trafficctl_device_conntrack_bytes{ip=\"%s\",proto=\"tcp\"} %.0f\n", ip, num($0, "tcp")
@@ -154,13 +154,15 @@ pfw_add_limit() {
fi fi
else else
if [ "$scope" = "input" ]; then if [ "$scope" = "input" ]; then
# shellcheck disable=SC2046 # deliberate split into the option words
iptables -I INPUT -p "$proto" --dport "$(iptables_dport "$port")" -m hashlimit \ iptables -I INPUT -p "$proto" --dport "$(iptables_dport "$port")" -m hashlimit \
--hashlimit-above "${kbit}kbit/sec" --hashlimit-burst "$kbit" \ $(tctl_ipt_limit_args "$kbit") \
--hashlimit-mode dstport --hashlimit-name "$(hashlimit_name "$proto" "$port")" \ --hashlimit-mode dstport --hashlimit-name "$(hashlimit_name "$proto" "$port")" \
-j DROP -m comment --comment "$cmt" 2>/dev/null -j DROP -m comment --comment "$cmt" 2>/dev/null
else else
# shellcheck disable=SC2046 # deliberate split into the option words
iptables -I FORWARD -d "$ip" -p "$proto" --dport "$(iptables_dport "$port")" -m hashlimit \ iptables -I FORWARD -d "$ip" -p "$proto" --dport "$(iptables_dport "$port")" -m hashlimit \
--hashlimit-above "${kbit}kbit/sec" --hashlimit-burst "$kbit" \ $(tctl_ipt_limit_args "$kbit") \
--hashlimit-mode dstip --hashlimit-name "$(hashlimit_name "$proto" "$port")" \ --hashlimit-mode dstip --hashlimit-name "$(hashlimit_name "$proto" "$port")" \
-j DROP -m comment --comment "$cmt" 2>/dev/null -j DROP -m comment --comment "$cmt" 2>/dev/null
fi fi
@@ -372,7 +374,7 @@ do_list() {
st=$(printf '%s' "$CTL_DUMP" | grep "$(pfw_comment limit "$p" "$cmt_ip" "$port")" | \ st=$(printf '%s' "$CTL_DUMP" | grep "$(pfw_comment limit "$p" "$cmt_ip" "$port")" | \
awk "$TCTL_NFT_RATE_AWK"'{ for (i = 1; i < NF - 1; i++) if ($i == "over") { printf "%.0f\n", tctl_rate_kbit($(i+1), $(i+2)); exit } }') awk "$TCTL_NFT_RATE_AWK"'{ for (i = 1; i < NF - 1; i++) if ($i == "over") { printf "%.0f\n", tctl_rate_kbit($(i+1), $(i+2)); exit } }')
[ -z "$st" ] && st=$(printf '%s' "$CTL_DUMP" | grep "$(pfw_comment limit "$p" "$cmt_ip" "$port")" | \ [ -z "$st" ] && st=$(printf '%s' "$CTL_DUMP" | grep "$(pfw_comment limit "$p" "$cmt_ip" "$port")" | \
grep -oE 'hashlimit-above [0-9]+' | awk '{print $2; exit}') awk "$TCTL_NFT_RATE_AWK"'{ r = tctl_ipt_first_rate(); if (r >= 0) { print r; exit } }')
[ -n "$st" ] && limit_kbit="$st" [ -n "$st" ] && limit_kbit="$st"
fi fi
done done
@@ -65,7 +65,7 @@ else
# hashlimit is keyed on dstip whatever mode was asked for, so on this path # hashlimit is keyed on dstip whatever mode was asked for, so on this path
# every bucket is per-address — "each" is the honest answer, not the # every bucket is per-address — "each" is the honest answer, not the
# requested mode. Documented in docs/API.md: shared needs nftables. # requested mode. Documented in docs/API.md: shared needs nftables.
iptables -t mangle -L FORWARD -nvx 2>/dev/null | grep "rl_ratelimit" | awk ' iptables -t mangle -L FORWARD -nvx 2>/dev/null | grep "rl_ratelimit" | awk "$TCTL_NFT_RATE_AWK"'
{ {
packets = $1 packets = $1
bytes = $2 bytes = $2
@@ -77,14 +77,9 @@ else
} }
if (i == 9) ip = $i if (i == 9) ip = $i
} }
# extract rate from hashlimit-above # the "above X" ceiling, in iptables units (see tctl_ipt_rate_kbit)
for (i = 1; i <= NF; i++) { rate = tctl_ipt_first_rate()
if ($i ~ /^[0-9]+kbit/) { if (rate < 0) rate = 0
gsub(/kbit.*/, "", $i)
rate = $i
break
}
}
if (ip != "" && ip != "0.0.0.0/0") { if (ip != "" && ip != "0.0.0.0/0") {
if (!(ip in seen)) { seen[ip] = 1; order[++n] = ip } if (!(ip in seen)) { seen[ip] = 1; order[++n] = ip }
rate_of[ip] = rate rate_of[ip] = rate
@@ -343,7 +343,7 @@ lookup_rate_limit() {
# way; the upload one is the only "_ul" name. # way; the upload one is the only "_ul" name.
r=$(echo "$RL_DUMP" | grep -E "$pat" \ r=$(echo "$RL_DUMP" | grep -E "$pat" \
| awk -v ip="$ip" '{ for (i = 1; i <= NF; i++) if ($i == ip || $i == ip "/32") { print; next } }' \ | awk -v ip="$ip" '{ for (i = 1; i <= NF; i++) if ($i == ip || $i == ip "/32") { print; next } }' \
| grep -oE '[0-9]+kbit' | head -1 | sed 's/kbit//') | awk "$TCTL_NFT_RATE_AWK"'{ r = tctl_ipt_first_rate(); if (r >= 0) { print r; exit } }')
fi fi
echo "${r:-0}" echo "${r:-0}"
} }
@@ -450,13 +450,34 @@ build_device_keyboard() {
rm -f "$tmpkb" rm -f "$tmpkb"
} }
# A ceiling as "8000 kbit/s", or "8000↓ / 2000↑ kbit/s" when upload has its
# own (#66). An upload figure of 0 or equal to download is not a split.
tg_ceiling() {
if [ "${2:-0}" -gt 0 ] 2>/dev/null && [ "$2" != "$1" ]; then
printf '%s↓ / %s↑ kbit/s' "$1" "$2"
else
printf '%s kbit/s' "$1"
fi
}
# The same, compact, for the /status flags: "8000k" or "8000/2000k".
tg_ceiling_short() {
if [ "${2:-0}" -gt 0 ] 2>/dev/null && [ "$2" != "$1" ]; then
printf '%s/%sk' "$1" "$2"
else
printf '%sk' "$1"
fi
}
build_action_keyboard() { build_action_keyboard() {
local ip="$1" devices="$2" local ip="$1" devices="$2"
local blocked wifi_blocked rl_kbit shape_kbit local blocked wifi_blocked rl_kbit shape_kbit rl_up shape_up
blocked=$(get_device_field "$devices" "$ip" "blocked") blocked=$(get_device_field "$devices" "$ip" "blocked")
wifi_blocked=$(get_device_field "$devices" "$ip" "wifi_blocked") wifi_blocked=$(get_device_field "$devices" "$ip" "wifi_blocked")
rl_kbit=$(get_device_field "$devices" "$ip" "rate_limit_kbit") rl_kbit=$(get_device_field "$devices" "$ip" "rate_limit_kbit")
shape_kbit=$(get_device_field "$devices" "$ip" "shape_kbit") shape_kbit=$(get_device_field "$devices" "$ip" "shape_kbit")
rl_up=$(get_device_field "$devices" "$ip" "rate_limit_kbit_up")
shape_up=$(get_device_field "$devices" "$ip" "shape_kbit_up")
conn_type=$(get_device_field "$devices" "$ip" "conn_type") conn_type=$(get_device_field "$devices" "$ip" "conn_type")
local kb='{"inline_keyboard":[' local kb='{"inline_keyboard":['
@@ -487,7 +508,7 @@ build_action_keyboard() {
# limiter # limiter
if [ "$TG_BTN_LIMIT" = "1" ]; then if [ "$TG_BTN_LIMIT" = "1" ]; then
if [ "${rl_kbit:-0}" -gt 0 ] 2>/dev/null; then if [ "${rl_kbit:-0}" -gt 0 ] 2>/dev/null; then
rows="${rows}[{\"text\":\"⚡ Limit: ${rl_kbit} kbit/s — Remove\",\"callback_data\":\"act:unlimit:${ip}\"}]," rows="${rows}[{\"text\":\"⚡ Limit: $(tg_ceiling "$rl_kbit" "$rl_up") — Remove\",\"callback_data\":\"act:unlimit:${ip}\"}],"
else else
rows="${rows}[{\"text\":\"⚡ 1M\",\"callback_data\":\"act:limit:${ip}:1000\"},{\"text\":\"⚡ 2M\",\"callback_data\":\"act:limit:${ip}:2000\"},{\"text\":\"⚡ 5M\",\"callback_data\":\"act:limit:${ip}:5000\"}]," rows="${rows}[{\"text\":\"⚡ 1M\",\"callback_data\":\"act:limit:${ip}:1000\"},{\"text\":\"⚡ 2M\",\"callback_data\":\"act:limit:${ip}:2000\"},{\"text\":\"⚡ 5M\",\"callback_data\":\"act:limit:${ip}:5000\"}],"
rows="${rows}[{\"text\":\"⚡ 10M\",\"callback_data\":\"act:limit:${ip}:10000\"},{\"text\":\"⚡ 25M\",\"callback_data\":\"act:limit:${ip}:25000\"},{\"text\":\"⚡ 50M\",\"callback_data\":\"act:limit:${ip}:50000\"},{\"text\":\"⚡ 100M\",\"callback_data\":\"act:limit:${ip}:100000\"}]," rows="${rows}[{\"text\":\"⚡ 10M\",\"callback_data\":\"act:limit:${ip}:10000\"},{\"text\":\"⚡ 25M\",\"callback_data\":\"act:limit:${ip}:25000\"},{\"text\":\"⚡ 50M\",\"callback_data\":\"act:limit:${ip}:50000\"},{\"text\":\"⚡ 100M\",\"callback_data\":\"act:limit:${ip}:100000\"}],"
@@ -497,7 +518,7 @@ build_action_keyboard() {
# shaper # shaper
if [ "$TG_BTN_SHAPE" = "1" ]; then if [ "$TG_BTN_SHAPE" = "1" ]; then
if [ "${shape_kbit:-0}" -gt 0 ] 2>/dev/null; then if [ "${shape_kbit:-0}" -gt 0 ] 2>/dev/null; then
rows="${rows}[{\"text\":\"🔧 Shape: ${shape_kbit} kbit/s — Remove\",\"callback_data\":\"act:unshape:${ip}\"}]," rows="${rows}[{\"text\":\"🔧 Shape: $(tg_ceiling "$shape_kbit" "$shape_up") — Remove\",\"callback_data\":\"act:unshape:${ip}\"}],"
else else
rows="${rows}[{\"text\":\"🔧 1M\",\"callback_data\":\"act:shape:${ip}:1000\"},{\"text\":\"🔧 2M\",\"callback_data\":\"act:shape:${ip}:2000\"},{\"text\":\"🔧 5M\",\"callback_data\":\"act:shape:${ip}:5000\"}]," rows="${rows}[{\"text\":\"🔧 1M\",\"callback_data\":\"act:shape:${ip}:1000\"},{\"text\":\"🔧 2M\",\"callback_data\":\"act:shape:${ip}:2000\"},{\"text\":\"🔧 5M\",\"callback_data\":\"act:shape:${ip}:5000\"}],"
rows="${rows}[{\"text\":\"🔧 10M\",\"callback_data\":\"act:shape:${ip}:10000\"},{\"text\":\"🔧 25M\",\"callback_data\":\"act:shape:${ip}:25000\"},{\"text\":\"🔧 50M\",\"callback_data\":\"act:shape:${ip}:50000\"},{\"text\":\"🔧 100M\",\"callback_data\":\"act:shape:${ip}:100000\"}]," rows="${rows}[{\"text\":\"🔧 10M\",\"callback_data\":\"act:shape:${ip}:10000\"},{\"text\":\"🔧 25M\",\"callback_data\":\"act:shape:${ip}:25000\"},{\"text\":\"🔧 50M\",\"callback_data\":\"act:shape:${ip}:50000\"},{\"text\":\"🔧 100M\",\"callback_data\":\"act:shape:${ip}:100000\"}],"
@@ -556,7 +577,7 @@ handle_devices_edit() {
} }
handle_status() { handle_status() {
local devices ip name rl_kbit shape_kbit blocked wifi_blocked local devices ip name rl_kbit shape_kbit rl_up shape_up blocked wifi_blocked
local result="" local result=""
devices=$(get_devices) devices=$(get_devices)
[ -z "$devices" ] || [ "$devices" = "[]" ] && { tg_send "No active devices"; return; } [ -z "$devices" ] || [ "$devices" = "[]" ] && { tg_send "No active devices"; return; }
@@ -566,13 +587,15 @@ handle_status() {
wifi_blocked=$(get_device_field "$devices" "$ip" "wifi_blocked") wifi_blocked=$(get_device_field "$devices" "$ip" "wifi_blocked")
rl_kbit=$(get_device_field "$devices" "$ip" "rate_limit_kbit") rl_kbit=$(get_device_field "$devices" "$ip" "rate_limit_kbit")
shape_kbit=$(get_device_field "$devices" "$ip" "shape_kbit") shape_kbit=$(get_device_field "$devices" "$ip" "shape_kbit")
rl_up=$(get_device_field "$devices" "$ip" "rate_limit_kbit_up")
shape_up=$(get_device_field "$devices" "$ip" "shape_kbit_up")
name=$(get_device_field "$devices" "$ip" "name") name=$(get_device_field "$devices" "$ip" "name")
local flags="" local flags=""
[ "$blocked" = "true" ] || [ "$blocked" = "1" ] && flags="${flags} 🚫inet" [ "$blocked" = "true" ] || [ "$blocked" = "1" ] && flags="${flags} 🚫inet"
[ "$wifi_blocked" = "true" ] || [ "$wifi_blocked" = "1" ] && flags="${flags} 📵wifi" [ "$wifi_blocked" = "true" ] || [ "$wifi_blocked" = "1" ] && flags="${flags} 📵wifi"
[ "${rl_kbit:-0}" -gt 0 ] 2>/dev/null && flags="${flags} ⚡${rl_kbit}k" [ "${rl_kbit:-0}" -gt 0 ] 2>/dev/null && flags="${flags} ⚡$(tg_ceiling_short "$rl_kbit" "$rl_up")"
[ "${shape_kbit:-0}" -gt 0 ] 2>/dev/null && flags="${flags} 🔧${shape_kbit}k" [ "${shape_kbit:-0}" -gt 0 ] 2>/dev/null && flags="${flags} 🔧$(tg_ceiling_short "$shape_kbit" "$shape_up")"
[ -n "$flags" ] && result="${result}${name:-?} (${ip}):${flags}\n" [ -n "$flags" ] && result="${result}${name:-?} (${ip}):${flags}\n"
done done
@@ -616,17 +639,19 @@ handle_callback() {
invalidate_cache invalidate_cache
devices=$(get_devices) devices=$(get_devices)
name=$(get_device_field "$devices" "$ip" "name") name=$(get_device_field "$devices" "$ip" "name")
local blocked wifi_blocked rl_kbit shape_kbit local blocked wifi_blocked rl_kbit shape_kbit rl_up shape_up
blocked=$(get_device_field "$devices" "$ip" "blocked") blocked=$(get_device_field "$devices" "$ip" "blocked")
wifi_blocked=$(get_device_field "$devices" "$ip" "wifi_blocked") wifi_blocked=$(get_device_field "$devices" "$ip" "wifi_blocked")
rl_kbit=$(get_device_field "$devices" "$ip" "rate_limit_kbit") rl_kbit=$(get_device_field "$devices" "$ip" "rate_limit_kbit")
shape_kbit=$(get_device_field "$devices" "$ip" "shape_kbit") shape_kbit=$(get_device_field "$devices" "$ip" "shape_kbit")
rl_up=$(get_device_field "$devices" "$ip" "rate_limit_kbit_up")
shape_up=$(get_device_field "$devices" "$ip" "shape_kbit_up")
local status_line="" local status_line=""
[ "$blocked" = "true" ] || [ "$blocked" = "1" ] && status_line="${status_line}🚫 Internet blocked\n" [ "$blocked" = "true" ] || [ "$blocked" = "1" ] && status_line="${status_line}🚫 Internet blocked\n"
[ "$wifi_blocked" = "true" ] || [ "$wifi_blocked" = "1" ] && status_line="${status_line}📵 WiFi blocked\n" [ "$wifi_blocked" = "true" ] || [ "$wifi_blocked" = "1" ] && status_line="${status_line}📵 WiFi blocked\n"
[ "${rl_kbit:-0}" -gt 0 ] 2>/dev/null && status_line="${status_line}⚡ Limiter: ${rl_kbit} kbit/s\n" [ "${rl_kbit:-0}" -gt 0 ] 2>/dev/null && status_line="${status_line}⚡ Limiter: $(tg_ceiling "$rl_kbit" "$rl_up")\n"
[ "${shape_kbit:-0}" -gt 0 ] 2>/dev/null && status_line="${status_line}🔧 Shaper: ${shape_kbit} kbit/s\n" [ "${shape_kbit:-0}" -gt 0 ] 2>/dev/null && status_line="${status_line}🔧 Shaper: $(tg_ceiling "$shape_kbit" "$shape_up")\n"
[ -z "$status_line" ] && status_line="✅ No restrictions\n" [ -z "$status_line" ] && status_line="✅ No restrictions\n"
local text local text
+2 -2
View File
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-vantage PKG_NAME:=luci-app-vantage
PKG_VERSION:=1.0.16 PKG_VERSION:=1.0.17
PKG_RELEASE:=30 PKG_RELEASE:=31
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage dashboard LUCI_TITLE:=Vantage dashboard
@@ -584,16 +584,21 @@ h2, h3{
--vt-oc: var(--vt-ok); --vt-oc: var(--vt-ok);
background: color-mix(in oklab, var(--vt-oc) 16%, transparent); background: color-mix(in oklab, var(--vt-oc) 16%, transparent);
} }
/* the ring pulses only while something needs attention */ /* the ring always pulses, or it reads as stuck: slowly and softly while
all is fine, at the full rate when something needs attention */
.vt-app .vt-orb::after { .vt-app .vt-orb::after {
content: ""; position: absolute; inset: 0; border-radius: 50%; content: ""; position: absolute; inset: 0; border-radius: 50%;
border: 2px solid var(--vt-oc); opacity: .35; border: 2px solid var(--vt-oc); opacity: .35;
animation: vt-ring-soft 5s ease-out infinite;
} }
.vt-app .vt-orb-warn::after, .vt-app .vt-orb-err::after { animation: vt-ring 2.6s ease-out infinite; } .vt-app .vt-orb-warn::after, .vt-app .vt-orb-err::after { animation: vt-ring 2.6s ease-out infinite; }
/* paused (overview.js setMotion): the ring stands still */
.vt-app.vt-still .vt-orb::after { animation: none; }
.vt-app .vt-orb-core { width: 14px; height: 14px; border-radius: 50%; background: var(--vt-oc); box-shadow: 0 0 0 4px color-mix(in oklab, var(--vt-oc) 22%, transparent); } .vt-app .vt-orb-core { width: 14px; height: 14px; border-radius: 50%; background: var(--vt-oc); box-shadow: 0 0 0 4px color-mix(in oklab, var(--vt-oc) 22%, transparent); }
.vt-app .vt-orb-warn { --vt-oc: var(--vt-warn); } .vt-app .vt-orb-warn { --vt-oc: var(--vt-warn); }
.vt-app .vt-orb-err { --vt-oc: var(--vt-err); } .vt-app .vt-orb-err { --vt-oc: var(--vt-err); }
@keyframes vt-ring { 0% { transform: scale(.7); opacity: .5; } 100% { transform: scale(1.35); opacity: 0; } } @keyframes vt-ring { 0% { transform: scale(.7); opacity: .5; } 100% { transform: scale(1.35); opacity: 0; } }
@keyframes vt-ring-soft { 0% { transform: scale(.85); opacity: .35; } 100% { transform: scale(1.3); opacity: 0; } }
.vt-app .vt-head-text { min-width: 0; flex: 1; } .vt-app .vt-head-text { min-width: 0; flex: 1; }
.vt-app .vt-headline { .vt-app .vt-headline {
@@ -610,8 +615,8 @@ h2, h3{
} }
.vt-app .vt-live-beat { position: relative; width: 8px; height: 8px; border-radius: 50%; background: var(--vt-beat-c, var(--vt-ok)); } .vt-app .vt-live-beat { position: relative; width: 8px; height: 8px; border-radius: 50%; background: var(--vt-beat-c, var(--vt-ok)); }
/* the beat is a ring that scales and fades (compositor only; an animated /* the beat is a ring that scales and fades (compositor only; an animated
box-shadow repainted every frame), only while reconnecting: a live page box-shadow repainted every frame): slowly while live, at the full rate
keeps a still dot. It sits behind the dot (z-index -1 in the pill's while reconnecting, not at all while paused. It sits behind the dot (z-index -1 in the pill's
stacking context) so the dot keeps its colour, and it is transparent stacking context) so the dot keeps its colour, and it is transparent
unless the animation runs: nothing shows without motion. */ unless the animation runs: nothing shows without motion. */
.vt-app .vt-live-beat::after { .vt-app .vt-live-beat::after {
@@ -621,8 +626,10 @@ h2, h3{
} }
.vt-app .vt-live-paused .vt-live-beat { --vt-beat-c: var(--vt-text-3); } .vt-app .vt-live-paused .vt-live-beat { --vt-beat-c: var(--vt-text-3); }
.vt-app .vt-live-err .vt-live-beat { --vt-beat-c: var(--vt-warn); } .vt-app .vt-live-err .vt-live-beat { --vt-beat-c: var(--vt-warn); }
.vt-app .vt-live-ok .vt-live-beat::after { animation: vt-beat-soft 5s ease-out infinite; }
.vt-app .vt-live-err .vt-live-beat::after { animation: vt-beat 2s ease-out infinite; } .vt-app .vt-live-err .vt-live-beat::after { animation: vt-beat 2s ease-out infinite; }
@keyframes vt-beat { 0% { transform: scale(1); opacity: 1; } 70%, 100% { transform: scale(2.75); opacity: 0; } } @keyframes vt-beat { 0% { transform: scale(1); opacity: 1; } 70%, 100% { transform: scale(2.75); opacity: 0; } }
@keyframes vt-beat-soft { 0% { transform: scale(1); opacity: .6; } 70%, 100% { transform: scale(2.2); opacity: 0; } }
.vt-app .vt-health { .vt-app .vt-health {
display: grid; gap: 10px; margin-bottom: var(--vt-sp-4); display: grid; gap: 10px; margin-bottom: var(--vt-sp-4);
@@ -235,7 +235,7 @@ function mount(host, band, style) {
var cv = document.createElement('canvas'), ctx = cv.getContext && cv.getContext('2d'); var cv = document.createElement('canvas'), ctx = cv.getContext && cv.getContext('2d');
var env = { w: 0, h: 0, box: { t: 0, b: 0 }, origin: { x: 0, y: 0 }, count: 2, speed: packetSpeed(0), share: 0.7, frame: frameMs(0), colour: '#7a9a3a', muted: '#8a9099' }; var env = { w: 0, h: 0, box: { t: 0, b: 0 }, origin: { x: 0, y: 0 }, count: 2, speed: packetSpeed(0), share: 0.7, frame: frameMs(0), colour: '#7a9a3a', muted: '#8a9099' };
var down = null, up = null, originEl = null; var down = null, up = null, originEl = null;
var scene = null, cur = 'none', raf = 0, timer = 0, last = 0, dpr = 1, inView = true, hidden = false, still = false, mq = null; var scene = null, cur = 'none', raf = 0, timer = 0, last = 0, dpr = 1, inView = true, hidden = false, still = false, paused = false, mq = null;
var focused = typeof document.hasFocus === 'function' ? document.hasFocus() : true; var focused = typeof document.hasFocus === 'function' ? document.hasFocus() : true;
var ro = null, io = null, onMq = null, stillRated = false, mo = null, scheme = null, onScheme = null; var ro = null, io = null, onMq = null, stillRated = false, mo = null, scheme = null, onScheme = null;
@@ -339,7 +339,7 @@ function mount(host, band, style) {
} }
function again() { timer = 0; if (running() && !raf) raf = requestAnimationFrame(tick); } function again() { timer = 0; if (running() && !raf) raf = requestAnimationFrame(tick); }
function running() { return !!scene && !still && !hidden && focused && inView && usable(); } function running() { return !!scene && !still && !paused && !hidden && focused && inView && usable(); }
function halt() { function halt() {
if (raf) cancelAnimationFrame(raf); if (raf) cancelAnimationFrame(raf);
if (timer) clearTimeout(timer); if (timer) clearTimeout(timer);
@@ -407,6 +407,8 @@ function mount(host, band, style) {
run(); run();
}, },
running: running, running: running,
/* the page's refresh is paused: the last frame stays */
pause: function(on) { paused = !!on; run(); },
stop: function() { stop: function() {
halt(); scene = null; halt(); scene = null;
if (ro) ro.disconnect(); if (ro) ro.disconnect();
@@ -31,6 +31,7 @@ var GONE_KEEP_MS = 5 * 60 * 1000; /* disconnected stations stay listed */
var TALKERS = 3; /* top talkers shown; the tile keeps room for all */ var TALKERS = 3; /* top talkers shown; the tile keeps room for all */
var SWITCH_MS = 30 * 1000; /* LuCI runs swconfig for each switch's link state: at most this often */ var SWITCH_MS = 30 * 1000; /* LuCI runs swconfig for each switch's link state: at most this often */
var STORE_KEY = 'vantage.hist.v1'; var STORE_KEY = 'vantage.hist.v1';
var VERSION = '1.0.17'; /* this package's, as its Makefile has it (tests/version.test.js) */
var SVGNS = 'http://www.w3.org/2000/svg'; var SVGNS = 'http://www.w3.org/2000/svg';
var HOSTAPD_OBJ = /^[A-Za-z0-9_.-]{1,64}$/; var HOSTAPD_OBJ = /^[A-Za-z0-9_.-]{1,64}$/;
@@ -458,8 +459,17 @@ function spanShort(ms) {
return s >= 60 ? _('%d min', 'vantage').format(Math.floor(s / 60)) : _('%d s', 'vantage').format(s); return s >= 60 ? _('%d min', 'vantage').format(Math.floor(s / 60)) : _('%d s', 'vantage').format(s);
} }
/* the history belongs to one login: the next one in this tab starts empty,
under any theme. Vantage's login page also deletes it (login.js). */
function storeOwner() {
return String(L.env.sessionid || '').slice(-8);
}
function readStore() { function readStore() {
try { return JSON.parse(window.sessionStorage.getItem(STORE_KEY) || 'null'); } catch (e) { return null; } try {
var v = JSON.parse(window.sessionStorage.getItem(STORE_KEY) || 'null');
return v && (v.sid || '') === storeOwner() ? v : null;
} catch (e) { return null; }
} }
function writeStore(v) { function writeStore(v) {
@@ -901,7 +911,7 @@ return view.extend({
Object.keys(st.clientHist).slice(0, 32).forEach(function(mac) { Object.keys(st.clientHist).slice(0, 32).forEach(function(mac) {
keepClients[mac] = st.clientHist[mac].slice(-60).filter(function(p) { return p.every(function(v) { return isFinite(v); }); }); keepClients[mac] = st.clientHist[mac].slice(-60).filter(function(p) { return p.every(function(v) { return isFinite(v); }); });
}); });
writeStore({ net: st.hist.net, cpu: st.hist.cpu, mem: st.hist.mem, clients: keepClients }); writeStore({ sid: storeOwner(), net: st.hist.net, cpu: st.hist.cpu, mem: st.hist.mem, clients: keepClients });
this.stickyGen(m); this.stickyGen(m);
m.clients.forEach(function(c) { c.exp = insight.experience(c); c.firstSeen = p.state.firstSeen[c.mac]; c.isNew = !!p.isNew[c.mac]; }); m.clients.forEach(function(c) { c.exp = insight.experience(c); c.firstSeen = p.state.firstSeen[c.mac]; c.isNew = !!p.isNew[c.mac]; });
@@ -1113,9 +1123,13 @@ return view.extend({
poll.add(L.bind(this.refresh, this), POLL_S); poll.add(L.bind(this.refresh, this), POLL_S);
poll.add(L.bind(this.refreshFast, this), FAST_S); poll.add(L.bind(this.refreshFast, this), FAST_S);
document.addEventListener('visibilitychange', function() { document.addEventListener('visibilitychange', function() {
if (!document.hidden && self.st.paused) self.refresh(); if (!document.hidden && self.st.paused && poll.active()) self.refresh();
else self.paintLive(); else self.paintLive();
}); });
/* LuCI's refresh paused or resumed from its indicator; on resume
CPU starts over, rather than read as the average over the pause */
document.addEventListener('poll-stop', function() { window.setTimeout(function() { self.paintLive(); }, 0); });
document.addEventListener('poll-start', function() { self.st.stat = null; self.paintLive(); });
document.addEventListener('keydown', L.bind(this.handleKey, this)); document.addEventListener('keydown', L.bind(this.handleKey, this));
window.setInterval(function() { self.paintLive(); }, 1000); window.setInterval(function() { self.paintLive(); }, 1000);
if (window.ResizeObserver) { if (window.ResizeObserver) {
@@ -1668,6 +1682,9 @@ return view.extend({
paintHead: function() { paintHead: function() {
var s = this.st.summary, clients = this.m.clients.length; var s = this.st.summary, clients = this.m.clients.length;
this.el.orb.className = 'vt-orb vt-orb-' + s.level; this.el.orb.className = 'vt-orb vt-orb-' + s.level;
/* the Vantage theme's refresh dot pulses fast in the same colour
while something needs attention (vantage.css) */
document.documentElement.setAttribute('data-vt-alert', s.level === 'warn' || s.level === 'err' ? s.level : '');
txt(this.el.headline, s.headline); txt(this.el.headline, s.headline);
txt(this.el.subline, this.identity(this.m.device).concat( txt(this.el.subline, this.identity(this.m.device).concat(
this.m.wired ? [] : [ clients === 1 ? _('1 client') : _('%d clients').format(clients) ]).join(' · ')); this.m.wired ? [] : [ clients === 1 ? _('1 client') : _('%d clients').format(clients) ]).join(' · '));
@@ -1688,15 +1705,36 @@ return view.extend({
paintLive: function() { paintLive: function() {
if (!this.el || !this.el.live) return; if (!this.el || !this.el.live) return;
var st = this.st, state, label; var st = this.st, state, label;
if (st.paused || document.hidden) { state = 'paused'; label = _('Paused while hidden'); } /* LuCI's own refresh can be paused (its indicator in the header) */
var stopped = typeof poll.active === 'function' && !poll.active();
if (st.paused || document.hidden || stopped) { state = 'paused'; label = stopped && !document.hidden ? _('Paused') : _('Paused while hidden'); }
else if (st.lastErr) { state = 'err'; label = _('Reconnecting…', 'vantage'); } else if (st.lastErr) { state = 'err'; label = _('Reconnecting…', 'vantage'); }
else { state = 'ok'; label = _('Live') + ' · ' + fmt.ago(Date.now() - st.lastOk); } else { state = 'ok'; label = _('Live') + ' · ' + fmt.ago(Date.now() - st.lastOk); }
this.el.live.className = 'vt-livepill vt-live-' + state; this.el.live.className = 'vt-livepill vt-live-' + state;
/* the Vantage theme's refresh dot pulses fast and amber while this
reconnects (vantage.css) */
document.documentElement.setAttribute('data-vt-live', state);
/* the Vantage theme's poll indicator already says the page is /* the Vantage theme's poll indicator already says the page is
live: then this pill shows only when it is not */ live, or that its refresh is paused: then this pill shows only
this.el.live.hidden = state === 'ok' && vantageTheme(); when neither */
this.el.live.hidden = (state === 'ok' || stopped) && vantageTheme();
this.el.live.setAttribute('title', label); this.el.live.setAttribute('title', label);
txt(this.el.liveText, label); txt(this.el.liveText, label);
this.setMotion(state !== 'paused');
},
/* Nothing moves while the page is paused: the status circle's ring
(app.css, .vt-still), the flow dots stay where they are and the
backdrop keeps its last frame. */
setMotion: function(on) {
if (this.motionOn === on) return;
this.motionOn = on;
if (this.el && this.el.root) this.el.root.classList.toggle('vt-still', !on);
var map = this.wireMap || {};
Object.keys(map).forEach(function(k) {
map[k].anims.forEach(function(a) { if (on) a.play(); else a.pause(); });
});
if (this.backdrop) this.backdrop.pause(!on);
}, },
/* ----------------------------------------------------------- health */ /* ----------------------------------------------------------- health */
@@ -2302,6 +2340,7 @@ return view.extend({
/* every dot takes the whole trip, started i/n of the way along */ /* every dot takes the whole trip, started i/n of the way along */
var a = dot.animate(wires.keyframes(g.samples), { duration: dur, delay: -dur * i / n, iterations: Infinity, easing: 'linear' }); var a = dot.animate(wires.keyframes(g.samples), { duration: dur, delay: -dur * i / n, iterations: Infinity, easing: 'linear' });
a.playbackRate = rate; a.playbackRate = rate;
if (this.motionOn === false) a.pause();
g.dots.push(dot); g.dots.push(dot);
g.anims.push(a); g.anims.push(a);
} }
@@ -2428,7 +2467,8 @@ return view.extend({
})); }));
patch(el.fw, [ patch(el.fw, [
E('span', {}, [ d.firmware || '' ]), E('span', {}, [ d.firmware || '' ]),
d.kernel ? E('span', { 'class': 'vt-hint' }, [ _('Kernel %s').format(d.kernel) ]) : '' d.kernel ? E('span', { 'class': 'vt-hint' }, [ _('Kernel %s').format(d.kernel) ]) : '',
E('span', { 'class': 'vt-hint' }, [ 'Vantage ' + VERSION ])
]); ]);
}, },
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+15
View File
@@ -13710,3 +13710,18 @@ msgstr ""
msgid "Wireless scan failed: %s" msgid "Wireless scan failed: %s"
msgstr "" msgstr ""
msgid "Export eMMC partition"
msgstr ""
msgid "Download the raw contents of the selected eMMC partition."
msgstr ""
msgid "Choose eMMC partition"
msgstr ""
msgid "Download eMMC partition"
msgstr ""
msgid "Export partition"
msgstr ""
+15
View File
@@ -14238,3 +14238,18 @@ msgstr "启用 CSA"
msgid "Announce channel switches to clients. Automatic channel selection remains available when disabled." msgid "Announce channel switches to clients. Automatic channel selection remains available when disabled."
msgstr "向客户端通告信道切换。关闭后仍可自动选择信道。" msgstr "向客户端通告信道切换。关闭后仍可自动选择信道。"
msgid "Export eMMC partition"
msgstr "导出 eMMC 分区"
msgid "Download the raw contents of the selected eMMC partition."
msgstr "下载所选 eMMC 分区的原始内容。"
msgid "Choose eMMC partition"
msgstr "选择 eMMC 分区"
msgid "Download eMMC partition"
msgstr "下载 eMMC 分区"
msgid "Export partition"
msgstr "导出分区"
+7 -3
View File
@@ -1,17 +1,21 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
LUCI_TITLE:=Fluent dashboard for LuCI LUCI_TITLE:=Fluent dashboard for LuCI
LUCI_DEPENDS:=+luci-base +libiwinfo LUCI_DEPENDS:=+luci-base +libiwinfo +rpcd-mod-ucode +ucode-mod-fs +ucode-mod-uci
LUCI_DESCRIPTION:=A fixed-card, theme-independent LuCI status dashboard with system, network, wireless, DHCP lease, and wireless client information. LUCI_DESCRIPTION:=A theme-independent configurable dashboard workbench with shared layouts and live system, network, wireless and client widgets.
LUCI_MAINTAINER:=LazuliKao LUCI_MAINTAINER:=LazuliKao
LUCI_MINIFY_CSS:=0 LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0 LUCI_MINIFY_JS:=0
PKG_NAME:=luci-mod-fluentdashboard PKG_NAME:=luci-mod-fluentdashboard
PKG_VERSION:=1.0.9 PKG_VERSION:=1.0.9
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
PKG_BUILD_DEPENDS:=iwinfo PKG_BUILD_DEPENDS:=iwinfo
define Package/luci-mod-fluentdashboard/conffiles
/etc/config/fluentdashboard
endef
# call BuildPackage - OpenWrt buildroot signature # call BuildPackage - OpenWrt buildroot signature
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk
@@ -1,7 +1,7 @@
.fluent-dashboard { .fluent-dashboard {
--fluent-dashboard-border: var(--fluent-card-border, var(--inputborder-color, color-mix(in srgb, currentColor 16%, transparent))); --fluent-dashboard-border: var(--fluent-card-border, var(--inputborder-color, color-mix(in srgb, currentColor 16%, transparent)));
--fluent-dashboard-muted: var(--fluent-text-secondary, color-mix(in srgb, currentColor 68%, transparent)); --fluent-dashboard-muted: var(--fluent-text-secondary, color-mix(in srgb, currentColor 68%, transparent));
--fluent-dashboard-surface: var(--fluent-card-bg, rgba(var(--primary-rgbbody, 255, 255, 255), 0.7)); --fluent-dashboard-surface: var(--fluent-card-bg, rgba(var(--primary-rgbbody, 255, 255, 255), .7));
--fluent-dashboard-shadow: var(--fluent-card-shadow, 0 .1rem .3rem var(--input-boxcolor, color-mix(in srgb, CanvasText 8%, transparent))); --fluent-dashboard-shadow: var(--fluent-card-shadow, 0 .1rem .3rem var(--input-boxcolor, color-mix(in srgb, CanvasText 8%, transparent)));
color: var(--fluent-text, CanvasText); color: var(--fluent-text, CanvasText);
container-type: inline-size; container-type: inline-size;
@@ -12,6 +12,7 @@
} }
.fluent-dashboard__header { .fluent-dashboard__header {
z-index: 0;
color: highlighttext; color: highlighttext;
background: linear-gradient(120deg, Highlight, color-mix(in srgb, Highlight 68%, CanvasText)); background: linear-gradient(120deg, Highlight, color-mix(in srgb, Highlight 68%, CanvasText));
border-radius: .75rem; border-radius: .75rem;
@@ -21,9 +22,8 @@
margin-block-end: 1rem; margin-block-end: 1rem;
padding: 1.25rem; padding: 1.25rem;
display: flex; display: flex;
z-index: 0;
overflow: hidden;
position: relative; position: relative;
overflow: hidden;
} }
.fluent-dashboard__header h1, .fluent-dashboard__header p { .fluent-dashboard__header h1, .fluent-dashboard__header p {
@@ -239,3 +239,394 @@
transition: none; transition: none;
} }
} }
.fluent-dashboard {
--dash-card-bg: var(--fluent-card-bg, Canvas);
--dash-text: var(--fluent-text, CanvasText);
--dash-border: var(--fluent-card-border, color-mix(in srgb, CanvasText 18%, transparent));
--dash-gap: .75rem;
--dash-radius: var(--fluent-border-radius, .5rem);
color: var(--dash-text);
}
.fluent-dashboard__toolbar {
gap: var(--dash-gap);
margin-block: var(--dash-gap);
border: 0;
flex-wrap: wrap;
padding: 0;
display: flex;
}
.fluent-dashboard__workspace {
gap: var(--dash-gap);
grid-template-columns: minmax(0, 1fr);
display: grid;
}
.fluent-dashboard__canvas {
min-width: 0;
min-height: 12rem;
}
.fluent-dashboard__library, .fluent-dashboard__properties {
border: 1px solid var(--dash-border);
border-radius: var(--dash-radius);
min-width: 0;
padding: var(--dash-gap);
}
.fluent-dashboard__library > button, .fluent-dashboard__properties > button {
margin: .2rem;
}
.fluent-dashboard__field {
margin-block: var(--dash-gap);
display: block;
}
.fluent-dashboard__field > input {
width: 100%;
display: block;
}
.fluent-dashboard .grid-stack-item-content > .fluent-dashboard__card {
flex-direction: column;
height: 100%;
display: flex;
}
.fluent-dashboard__card-header {
flex: none;
justify-content: space-between;
align-items: center;
min-height: 2rem;
display: flex;
}
.fluent-dashboard__card-header > .fluent-dashboard__card-title {
border: 0;
flex: 1;
padding-block: .5rem;
}
.fluent-dashboard__card-header > button {
flex: none;
margin-inline-end: .5rem;
font-size: .75rem;
}
.fluent-dashboard__card-content {
min-height: 0;
overflow: auto;
}
.fluent-dashboard__widget-status {
padding-inline: var(--dash-gap);
margin: 0;
font-size: .85rem;
}
.fluent-dashboard__card--editing .fluent-dashboard__card-header {
cursor: move;
}
.fluent-dashboard__card--hidden {
opacity: .5;
}
.fluent-dashboard__card--locked .fluent-dashboard__card-header {
cursor: default;
}
.fluent-dashboard__tile--selected > .grid-stack-item-content {
outline-offset: -2px;
outline: 2px solid highlight;
}
.fluent-dashboard__group {
border: 1px solid var(--dash-border);
border-radius: var(--dash-radius);
background: var(--dash-card-bg);
flex-direction: column;
height: 100%;
display: flex;
}
.fluent-dashboard__group > .fluent-dashboard__card-header {
height: 40px;
}
.fluent-dashboard__group-description {
padding: .25rem var(--dash-gap);
margin: 0;
font-size: .85rem;
}
.fluent-dashboard__group-body {
flex: 1;
min-height: 80px;
}
.fluent-dashboard__metric, .fluent-dashboard__traffic {
padding: var(--dash-gap);
flex-direction: column;
gap: .4rem;
display: flex;
overflow: auto;
}
.fluent-dashboard__metric-value {
overflow-wrap: anywhere;
font-size: clamp(1rem, 1.5vw, 1.6rem);
}
.fluent-dashboard__metric-detail {
color: var(--dash-text);
opacity: .7;
overflow-wrap: anywhere;
}
.fluent-dashboard__metric progress {
width: 100%;
accent-color: var(--fluent-primary, Highlight);
}
.fluent-dashboard__traffic svg {
width: 100%;
height: 5rem;
min-height: 2rem;
}
.fluent-dashboard__traffic polyline {
fill: none;
stroke-width: 1.5px;
vector-effect: non-scaling-stroke;
}
.fluent-dashboard__traffic-rx {
stroke: var(--fluent-primary, Highlight);
}
.fluent-dashboard__traffic-tx {
stroke: var(--fluent-success, #26884a);
}
.fluent-dashboard__shortcuts {
gap: var(--dash-gap);
padding: var(--dash-gap);
flex-wrap: wrap;
display: flex;
}
.fluent-dashboard__shortcuts a {
border: 1px solid var(--dash-border);
border-radius: var(--dash-radius);
color: var(--dash-text);
align-items: center;
padding: .5rem;
display: inline-flex;
}
.fluent-dashboard__notice:empty, .fluent-dashboard [hidden] {
display: none;
}
.fluent-dashboard .grid-stack-item-content {
min-width: 0;
overflow: hidden;
}
.fluent-dashboard__canvas .grid-stack-item > .grid-stack-item-content {
overflow: visible;
}
.fluent-dashboard__group-body.grid-stack {
width: 100%;
min-width: 0;
}
.fluent-dashboard__workspace--editing .fluent-dashboard__canvas > .grid-stack-item {
touch-action: auto;
}
@media (min-width: 1400px) {
.fluent-dashboard__workspace--editing {
grid-template-columns: 12rem minmax(0, 1fr) 15rem;
align-items: start;
}
}
.fluent-dashboard .grid-stack {
position: relative;
}
.fluent-dashboard .grid-stack-rtl {
direction: ltr;
}
.fluent-dashboard .grid-stack-rtl > .grid-stack-item {
direction: rtl;
}
.fluent-dashboard .grid-stack-placeholder > .placeholder-content {
background-color: #0000001a;
width: auto;
margin: 0;
position: absolute;
z-index: 0 !important;
}
.fluent-dashboard .grid-stack > .grid-stack-item {
width: var(--gs-column-width);
height: var(--gs-cell-height);
padding: 0;
position: absolute;
top: 0;
left: 0;
}
.fluent-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content {
width: auto;
margin: 0;
position: absolute;
overflow: hidden auto;
}
.fluent-dashboard .grid-stack > .grid-stack-item.size-to-content:not(.size-to-content-max) > .grid-stack-item-content {
overflow-y: hidden;
}
.fluent-dashboard .grid-stack > .grid-stack-item > .grid-stack-item-content, .fluent-dashboard .grid-stack > .grid-stack-placeholder > .placeholder-content {
top: var(--gs-item-margin-top);
right: var(--gs-item-margin-right);
bottom: var(--gs-item-margin-bottom);
left: var(--gs-item-margin-left);
}
.fluent-dashboard .grid-stack-item > .ui-resizable-handle {
-ms-touch-action: none;
touch-action: none;
font-size: .1px;
display: block;
position: absolute;
}
.fluent-dashboard .grid-stack-item.ui-resizable-disabled > .ui-resizable-handle, .fluent-dashboard .grid-stack-item.ui-resizable-autohide > .ui-resizable-handle {
display: none;
}
.fluent-dashboard .grid-stack-item > .ui-resizable-ne, .fluent-dashboard .grid-stack-item > .ui-resizable-nw, .fluent-dashboard .grid-stack-item > .ui-resizable-se, .fluent-dashboard .grid-stack-item > .ui-resizable-sw {
background-image: url("data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"%23666\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" viewBox=\"0 0 20 20\"><path d=\"m10 3 2 2H8l2-2v14l-2-2h4l-2 2\"/></svg>");
background-position: center;
background-repeat: no-repeat;
}
.fluent-dashboard .grid-stack-item > .ui-resizable-ne, .fluent-dashboard .grid-stack-item > .ui-resizable-sw {
transform: rotate(45deg);
}
.fluent-dashboard .grid-stack-item > .ui-resizable-nw, .fluent-dashboard .grid-stack-item > .ui-resizable-se {
transform: rotate(-45deg);
}
.fluent-dashboard .grid-stack-item > .ui-resizable-nw {
cursor: nw-resize;
width: 20px;
height: 20px;
top: var(--gs-item-margin-top);
left: var(--gs-item-margin-left);
}
.fluent-dashboard .grid-stack-item > .ui-resizable-n {
cursor: n-resize;
height: 10px;
top: var(--gs-item-margin-top);
left: 25px;
right: 25px;
}
.fluent-dashboard .grid-stack-item > .ui-resizable-ne {
cursor: ne-resize;
width: 20px;
height: 20px;
top: var(--gs-item-margin-top);
right: var(--gs-item-margin-right);
}
.fluent-dashboard .grid-stack-item > .ui-resizable-e {
cursor: e-resize;
width: 10px;
top: 15px;
bottom: 15px;
right: var(--gs-item-margin-right);
}
.fluent-dashboard .grid-stack-item > .ui-resizable-se {
cursor: se-resize;
width: 20px;
height: 20px;
bottom: var(--gs-item-margin-bottom);
right: var(--gs-item-margin-right);
}
.fluent-dashboard .grid-stack-item > .ui-resizable-s {
cursor: s-resize;
height: 10px;
left: 25px;
bottom: var(--gs-item-margin-bottom);
right: 25px;
}
.fluent-dashboard .grid-stack-item > .ui-resizable-sw {
cursor: sw-resize;
width: 20px;
height: 20px;
bottom: var(--gs-item-margin-bottom);
left: var(--gs-item-margin-left);
}
.fluent-dashboard .grid-stack-item > .ui-resizable-w {
cursor: w-resize;
width: 10px;
top: 15px;
bottom: 15px;
left: var(--gs-item-margin-left);
}
.fluent-dashboard .grid-stack-item.ui-draggable-dragging > .ui-resizable-handle {
display: none !important;
}
.fluent-dashboard .grid-stack-item.ui-draggable-dragging {
will-change: left,top;
}
.fluent-dashboard .grid-stack-item.ui-resizable-resizing {
will-change: width,height;
}
.fluent-dashboard .ui-draggable-dragging, .fluent-dashboard .ui-resizable-resizing {
z-index: 10000;
}
.fluent-dashboard .ui-draggable-dragging > .grid-stack-item-content, .fluent-dashboard .ui-resizable-resizing > .grid-stack-item-content {
opacity: .8;
box-shadow: 1px 4px 6px #0003;
}
.fluent-dashboard .grid-stack-animate, .fluent-dashboard .grid-stack-animate .grid-stack-item {
transition: left .3s, top .3s, height .3s, width .3s;
}
.fluent-dashboard .grid-stack-animate .grid-stack-item.ui-draggable-dragging, .fluent-dashboard .grid-stack-animate .grid-stack-item.ui-resizable-resizing, .fluent-dashboard .grid-stack-animate .grid-stack-item.grid-stack-placeholder {
transition: left, top, height, width;
}
.fluent-dashboard .grid-stack > .grid-stack-item[gs-y="0"] {
top: 0;
}
.fluent-dashboard .grid-stack > .grid-stack-item[gs-x="0"] {
left: 0%;
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,3 @@
config dashboard 'global'
option schema_version '1'
option revision '0'
@@ -6,8 +6,17 @@
"iwinfo": [ "assoclist" ], "iwinfo": [ "assoclist" ],
"luci": [ "getVersion" ], "luci": [ "getVersion" ],
"luci-rpc": [ "getDHCPLeases" ], "luci-rpc": [ "getDHCPLeases" ],
"luci.fluentdashboard": [ "load" ],
"system": [ "board", "info" ] "system": [ "board", "info" ]
} }
} }
},
"luci-mod-fluentdashboard-edit": {
"description": "Grant permission to publish the shared Fluent dashboard layout",
"write": {
"ubus": {
"luci.fluentdashboard": [ "save" ]
}
}
} }
} }
@@ -0,0 +1,120 @@
'use strict';
import { open, readfile, writefile, rename, unlink, mkdtemp, rmdir } from 'fs';
import { cursor } from 'uci';
const config = '/etc/config/fluentdashboard';
const max_size = 262144;
function require_value(ok, message) { if (!ok) die(message); }
function integer(v, min, max) { return type(v) == 'int' && v >= min && v <= max; }
function id(v) { return type(v) == 'string' && match(v, /^[A-Za-z0-9_-]{1,64}$/); }
function safe_json(v, depth) {
if (depth > 16) return false;
if (type(v) == 'object' || type(v) == 'array') {
for (let k, item in v) {
if (k in [ '__proto__', 'constructor', 'prototype' ] || !safe_json(item, depth + 1)) return false;
}
}
return true;
}
function validate(doc) {
require_value(type(doc) == 'object' && doc.schemaVersion == 1 && safe_json(doc, 0), 'Invalid schema');
require_value(integer(doc.revision, 0, 9007199254740990), 'Invalid revision');
require_value(type(doc.dashboards) == 'array' && length(doc.dashboards) >= 1 && length(doc.dashboards) <= 16, 'Invalid dashboards');
let dashboards = {};
for (let dashboard in doc.dashboards) {
require_value(type(dashboard) == 'object' && id(dashboard.id) && !exists(dashboards, dashboard.id), 'Invalid dashboard id');
dashboards[dashboard.id] = true;
require_value(type(dashboard.name) == 'string' && length(trim(dashboard.name)) && length(dashboard.name) <= 128, 'Invalid name');
require_value(type(dashboard.nodes) == 'array' && length(dashboard.nodes) <= 120, 'Too many nodes');
let nodes = {};
for (let node in dashboard.nodes) {
require_value(type(node) == 'object' && id(node.id) && !exists(nodes, node.id), 'Invalid node id');
nodes[node.id] = node;
require_value(node.kind in [ 'widget', 'group' ] && exists(node, 'parentId') && (node.parentId == null || id(node.parentId)), 'Invalid node');
for (let flag in [ 'hidden', 'locked', 'collapsed' ]) require_value(!exists(node, flag) || type(node[flag]) == 'bool', 'Invalid flag');
require_value(!exists(node, 'title') || (type(node.title) == 'string' && length(node.title) <= 256), 'Invalid title');
let t = node.tile;
require_value(type(t) == 'object' && t.size in [ 'small', 'medium', 'wide', 'large', 'xlarge', 'full', 'custom' ], 'Invalid tile');
require_value(integer(t.x, 0, 11) && integer(t.y, 0, 10000) && integer(t.w, 1, 12) && integer(t.h, 1, 48) && integer(t.order, 0, 100000), 'Invalid coordinates');
if (node.kind == 'widget')
require_value(type(node.widgetType) == 'string' && match(node.widgetType, /^[A-Za-z0-9_.-]{1,128}$/) && integer(node.widgetVersion, 1, 100000) && type(node.config) == 'object', 'Invalid widget');
else {
require_value(type(node.title) == 'string', 'Invalid group title');
require_value(!exists(node, 'description') || (type(node.description) == 'string' && length(node.description) <= 2048), 'Invalid description');
}
}
for (let node in dashboard.nodes) {
let seen = {}; seen[node.id] = true;
let parent = node.parentId, depth = node.kind == 'group' ? 1 : 0;
while (parent != null) {
require_value(!exists(seen, parent) && nodes[parent]?.kind == 'group', 'Invalid parent graph');
seen[parent] = true;
require_value(++depth <= 2, 'Too many group levels');
parent = nodes[parent].parentId;
}
require_value(node.tile.x + node.tile.w <= (node.parentId == null ? 12 : nodes[node.parentId].tile.w), 'Tile outside parent');
}
}
require_value(type(doc.defaultDashboardId) == 'string' && exists(dashboards, doc.defaultDashboardId), 'Invalid default dashboard');
require_value(!exists(doc, 'meta') || type(doc.meta) == 'object', 'Invalid metadata');
}
function load_document() {
const uci = cursor('/etc/config', '/var/lock', '');
require_value(uci.load('fluentdashboard'), 'Cannot read dashboard configuration');
let raw = uci.get('fluentdashboard', 'global', 'layout');
let revision = int(uci.get('fluentdashboard', 'global', 'revision'));
require_value(integer(revision, 0, 9007199254740990), 'Invalid stored revision');
if (!raw && revision == 0) return { document: null, revision: 0 };
require_value(type(raw) == 'string' && length(raw) <= max_size, 'Invalid stored layout');
let doc = json(raw);
validate(doc);
require_value(doc.revision == revision, 'Stored revision mismatch');
return { document: doc, revision };
}
function save_document(args) {
require_value(type(args.layout) == 'string' && length(args.layout) <= max_size, 'Layout too large');
let doc = json(args.layout);
validate(doc);
require_value(integer(args.expectedRevision, 0, 9007199254740989) && doc.revision == args.expectedRevision, 'Invalid expected revision');
const current = load_document();
if (current.revision != args.expectedRevision) return { error: 'conflict', revision: current.revision };
doc.revision = current.revision + 1;
// Isolated UCI cursor: no global /tmp/.uci deltas, commit then atomic same-filesystem rename.
const staging = mkdtemp('/etc/config/.fluentdashboard-XXXXXX');
require_value(staging, 'Cannot create staging directory');
let result;
try {
const old = readfile(config);
require_value(old != null && writefile(staging + '/fluentdashboard', old) == length(old), 'Cannot stage config');
const uci = cursor(staging, staging, '');
require_value(uci.load('fluentdashboard'), 'Cannot load staged config');
require_value(uci.set('fluentdashboard', 'global', 'layout', sprintf('%J', doc)) && uci.set('fluentdashboard', 'global', 'revision', sprintf('%d', doc.revision)) && uci.set('fluentdashboard', 'global', 'schema_version', '1'), 'Cannot set config');
require_value(uci.commit('fluentdashboard'), 'Cannot commit config');
require_value(writefile(staging + '/previous', old) == length(old) && rename(staging + '/previous', config + '.previous'), 'Cannot preserve previous config');
require_value(rename(staging + '/fluentdashboard', config), 'Cannot publish config');
result = { document: doc, revision: doc.revision };
} catch (error) { result = { error: 'storage', message: `${error}` }; }
unlink(staging + '/fluentdashboard'); unlink(staging + '/previous'); rmdir(staging);
return result;
}
return {
'luci.fluentdashboard': {
load: { call: function() { try { return load_document(); } catch (error) { return { error: 'corrupt', message: `${error}` }; } } },
save: {
args: { layout: '', expectedRevision: 0 },
call: function(request) {
const lock = open('/var/lock/fluentdashboard.lock', 'a', 0600);
if (!lock) return { error: 'storage', message: 'Cannot open lock' };
if (!lock.lock('xn')) { lock.close(); return { error: 'busy', message: 'Another save is in progress' }; }
let result;
try { result = save_document(request.args); } catch (error) { result = { error: 'invalid', message: `${error}` }; }
lock.close();
return result;
}
}
}
};
@@ -307,16 +307,6 @@ function loadIwinfoInfoMap(force) {
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 ])
)).then(async (entries) => { )).then(async (entries) => {
await Promise.all(entries.map(async ([ name, info ]) => {
const iface = uci.sections('wireless', 'wifi-iface').find((iface) =>
isQcaWifiHwtype(uci.get('wireless', iface.device, 'type')) &&
resolver.aliasMap[iface['.name']] == name);
if (!iface || info?.mode != 'Master' || !/^ath\d+(?:\.\d+)?$/.test(name))
return;
const output = await L.resolveDefault(fs.exec_direct('/usr/sbin/cfg80211tool',
[ name, 'get_maxrate' ]), '');
info.theoretical_bitrate = parseQcaMaxPhyRate(output);
}));
const phyQueries = {}; const phyQueries = {};
for (const [ name, info ] of entries) { for (const [ name, info ] of entries) {
if (info?.hardware?.id?.[0] != 0x14c3 || !/^phy\d+$/.test(info?.phy || '')) if (info?.hardware?.id?.[0] != 0x14c3 || !/^phy\d+$/.test(info?.phy || ''))
@@ -948,13 +938,13 @@ function getFrequencyListBand(entry, hwmode) {
return null; return null;
} }
function normalizeIwinfoBitRate(rate) { function normalizeIwinfoBitRate(rate, inKbit = false) {
rate = +rate; rate = +rate;
if (isNaN(rate) || rate <= 0) if (isNaN(rate) || rate <= 0)
return null; return null;
return (rate > 100000) ? (rate / 1000) : rate; return (inKbit || rate > 100000) ? (rate / 1000) : rate;
} }
function getIwinfoInfoCandidates(radioNet) { function getIwinfoInfoCandidates(radioNet) {
@@ -982,9 +972,10 @@ function getIwinfoInfoCandidates(radioNet) {
} }
function getDisplayIwinfoBitRate(radioNet) { function getDisplayIwinfoBitRate(radioNet) {
const inKbit = isQcaWifiHwtype(uci.get('wireless', radioNet.getWifiDeviceName(), 'type'));
for (const candidate of getIwinfoInfoCandidates(radioNet)) { for (const candidate of getIwinfoInfoCandidates(radioNet)) {
const info = cachedIwinfoInfoMap ? cachedIwinfoInfoMap[candidate] : null; const info = cachedIwinfoInfoMap ? cachedIwinfoInfoMap[candidate] : null;
const rate = normalizeIwinfoBitRate(info != null ? info.bitrate : null); const rate = normalizeIwinfoBitRate(info != null ? info.bitrate : null, inKbit);
if (rate != null) if (rate != null)
return rate; return rate;
@@ -1022,21 +1013,16 @@ function getDisplayTheoreticalBitRate(radioNet) {
return null; return null;
for (const candidate of getIwinfoInfoCandidates(radioNet)) { for (const candidate of getIwinfoInfoCandidates(radioNet)) {
const rate = cachedIwinfoInfoMap?.[candidate]?.theoretical_bitrate; const info = cachedIwinfoInfoMap?.[candidate];
/* QSDK SIOCGIWRATE reports the BSS PHY ceiling in kbit/s. */
const rate = isQcaWifiHwtype(hwtype)
? +info?.bitrate / 1000 : info?.theoretical_bitrate;
if (rate > 0) if (rate > 0)
return rate; return rate;
} }
return null; return null;
} }
/* QSDK returns the running BSS PHY ceiling in kbit/s, including its
* bandwidth-dependent NSS mapping. Never infer it from client rates. */
function parseQcaMaxPhyRate(output) {
const match = String(output || '').match(/\bget_maxrate:\s*(\d+)\s*$/m);
const rate = match ? +match[1] : 0;
return rate > 0 ? Math.round(rate / 100) / 10 : null;
}
function getDisplayBitRate(radioNet) { function getDisplayBitRate(radioNet) {
if (isQcaWifiHwtype(uci.get('wireless', radioNet.getWifiDeviceName(), 'type')) && if (isQcaWifiHwtype(uci.get('wireless', radioNet.getWifiDeviceName(), 'type')) &&
radioNet.getMode() == 'ap') radioNet.getMode() == 'ap')
@@ -8,7 +8,6 @@
"/proc/sys/net/ipv6/conf/*/mtu": [ "read" ], "/proc/sys/net/ipv6/conf/*/mtu": [ "read" ],
"/proc/sys/net/ipv6/conf/*/hop_limit": [ "read" ], "/proc/sys/net/ipv6/conf/*/hop_limit": [ "read" ],
"/usr/sbin/iw": [ "exec" ], "/usr/sbin/iw": [ "exec" ],
"/usr/sbin/cfg80211tool ath* get_maxrate": [ "exec" ],
"/usr/libexec/luci-peeraddr": [ "exec" ], "/usr/libexec/luci-peeraddr": [ "exec" ],
"/usr/sbin/wlanconfig": [ "exec" ], "/usr/sbin/wlanconfig": [ "exec" ],
"/proc/sys/net/ipv[46]/conf/*": [ "read" ], "/proc/sys/net/ipv[46]/conf/*": [ "read" ],
@@ -136,6 +136,30 @@ function findStorageSize(procmtd, procpart, procmounts) {
} }
function loadEmmcPartitions() {
const devices = Object.create(null);
return L.resolveDefault(fs.list('/sys/class/block'), []).then(entries =>
Promise.all(entries.filter(entry => /^mmcblk\d+(?:p\d+|boot[01])$/.test(entry.name)).map(async entry => {
const name = entry.name;
const disk = name.match(/^(mmcblk\d+)/)[1];
devices[disk] ??= L.resolveDefault(fs.trimmed(`/sys/class/block/${disk}/device/type`), '');
if (await devices[disk] != 'MMC')
return null;
const [ uevent, sectors ] = await Promise.all([
L.resolveDefault(fs.read(`/sys/class/block/${name}/uevent`), ''),
L.resolveDefault(fs.trimmed(`/sys/class/block/${name}/size`), '0')
]);
const size = +sectors * 512;
if (!Number.isSafeInteger(size) || size <= 0)
return null;
return { device: name, name: uevent.match(/^PARTNAME=(.+)$/m)?.[1] || name, size };
}))
).then(partitions => partitions.filter(Boolean).sort((a, b) => L.naturalCompare(a.device, b.device)));
}
const mapdata = { actions: {}, config: {} }; const mapdata = { actions: {}, config: {} };
return view.extend({ return view.extend({
@@ -145,7 +169,8 @@ return view.extend({
fs.trimmed('/proc/sys/kernel/hostname'), fs.trimmed('/proc/sys/kernel/hostname'),
fs.trimmed('/proc/mtd'), fs.trimmed('/proc/mtd'),
fs.trimmed('/proc/partitions'), fs.trimmed('/proc/partitions'),
fs.trimmed('/proc/mounts') fs.trimmed('/proc/mounts'),
loadEmmcPartitions()
]; ];
return Promise.all(tasks); return Promise.all(tasks);
@@ -243,18 +268,19 @@ return view.extend({
.finally(function() { btn.firstChild.data = _('Upload archive...') }); .finally(function() { btn.firstChild.data = _('Upload archive...') });
}, },
handleBlock(hostname, ev) { handleBlock(hostname, selector, ev) {
const mtdblock = dom.parent(ev.target, '.cbi-section').querySelector('[data-name="mtdselect"] select'); const block = dom.parent(ev.target, '.cbi-section').querySelector('[data-name="%s"] select'.format(selector));
const mtdnumber = mtdblock.value; const device = selector == 'mtdselect' ? 'mtdblock' + block.value : block.value;
const mtdname = mtdblock.selectedOptions[0].text.replace(/([^a-zA-Z0-9]+)/g, '-'); const name = block.selectedOptions[0].text.replace(/([^a-zA-Z0-9]+)/g, '-');
const id = selector == 'mtdselect' ? 'mtd' + block.value : device;
const form = E('form', { const form = E('form', {
'method': 'post', 'method': 'post',
'action': L.env.cgi_base + '/cgi-download', 'action': L.env.cgi_base + '/cgi-download',
'enctype': 'application/x-www-form-urlencoded' 'enctype': 'application/x-www-form-urlencoded'
}, [ }, [
E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }), E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }),
E('input', { 'type': 'hidden', 'name': 'path', 'value': '/dev/mtdblock%d'.format(mtdnumber) }), E('input', { 'type': 'hidden', 'name': 'path', 'value': '/dev/' + device }),
E('input', { 'type': 'hidden', 'name': 'filename', 'value': '%s.mtd%d.%s.bin'.format(hostname, mtdnumber, mtdname) }) E('input', { 'type': 'hidden', 'name': 'filename', 'value': '%s.%s.%s.bin'.format(hostname, id, name) })
]); ]);
ev.currentTarget.parentNode.appendChild(form); ev.currentTarget.parentNode.appendChild(form);
@@ -452,7 +478,7 @@ return view.extend({
}); });
}, },
render([p_fstat, hostname, procmtd, procpart, procmounts]) { render([p_fstat, hostname, procmtd, procpart, procmounts, emmcPartitions]) {
const has_sysupgrade = (p_fstat.type == 'file'); const has_sysupgrade = (p_fstat.type == 'file');
const has_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || (procmounts.match("overlayfs:/overlay / ") != null); const has_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || (procmounts.match("overlayfs:/overlay / ") != null);
const storage_size = findStorageSize(procmtd, procpart, procmounts); const storage_size = findStorageSize(procmtd, procpart, procmounts);
@@ -509,10 +535,24 @@ return view.extend({
o = ss.option(form.Button, 'mtddownload', _('Download mtdblock')); o = ss.option(form.Button, 'mtddownload', _('Download mtdblock'));
o.inputstyle = 'action important'; o.inputstyle = 'action important';
o.inputtitle = _('Save mtdblock'); o.inputtitle = _('Save mtdblock');
o.onclick = L.bind(this.handleBlock, this, hostname); o.onclick = L.bind(this.handleBlock, this, hostname, 'mtdselect');
} }
if (emmcPartitions.length) {
o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Export eMMC partition'), _('Download the raw contents of the selected eMMC partition.'));
ss = o.subsection;
o = ss.option(form.ListValue, 'emmcselect', _('Choose eMMC partition'));
for (const partition of emmcPartitions)
o.value(partition.device, '%s (%s, %1024.2mB)'.format(partition.name, partition.device, partition.size));
o = ss.option(form.Button, 'emmcdl', _('Download eMMC partition'));
o.inputstyle = 'action important';
o.inputtitle = _('Export partition');
o.onclick = L.bind(this.handleBlock, this, hostname, 'emmcselect');
}
o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Flash new firmware image'), o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Flash new firmware image'),
has_sysupgrade has_sysupgrade
? _('Upload a sysupgrade-compatible image here to replace the running firmware.') ? _('Upload a sysupgrade-compatible image here to replace the running firmware.')
@@ -152,6 +152,12 @@
"cgi-io": [ "backup", "download" ], "cgi-io": [ "backup", "download" ],
"file": { "file": {
"/dev/mtdblock[0-9]*": [ "read" ], "/dev/mtdblock[0-9]*": [ "read" ],
"/dev/mmcblk[0-9]*p[0-9]*": [ "read" ],
"/dev/mmcblk[0-9]*boot[01]": [ "read" ],
"/sys/class/block": [ "list" ],
"/sys/class/block/mmcblk[0-9]*/device/type": [ "read" ],
"/sys/class/block/mmcblk[0-9]*/uevent": [ "read" ],
"/sys/class/block/mmcblk[0-9]*/size": [ "read" ],
"/etc/sysupgrade.conf": [ "read" ], "/etc/sysupgrade.conf": [ "read" ],
"/lib/upgrade/platform.sh": [ "list" ], "/lib/upgrade/platform.sh": [ "list" ],
"/proc/[0-9]*/mounts": [ "read" ], "/proc/[0-9]*/mounts": [ "read" ],
@@ -162,7 +168,7 @@
"/sbin/sysupgrade --list-backup": [ "exec" ] "/sbin/sysupgrade --list-backup": [ "exec" ]
}, },
"ubus": { "ubus": {
"file": [ "exec", "read", "stat" ] "file": [ "exec", "list", "read", "stat" ]
} }
}, },
"write": { "write": {
+1 -1
View File
@@ -9,7 +9,7 @@ LUCI_TITLE:=Design Theme
LUCI_DEPENDS:= LUCI_DEPENDS:=
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
PKG_VERSION:=7.1 PKG_VERSION:=7.1
PKG_RELEASE:=12 PKG_RELEASE:=13
LUCI_MINIFY_CSS:= LUCI_MINIFY_CSS:=
CONFIG_LUCI_CSSTIDY:= CONFIG_LUCI_CSSTIDY:=
@@ -4803,6 +4803,444 @@ body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"
min-width: 200px; min-width: 200px;
} }
/* Keep wide tables scrollable without widening the content panel. */
#maincontent .design-table-scroll,
#modal_overlay .design-table-scroll {
display: block;
max-width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
#maincontent .tabs {
display: flex;
flex-wrap: wrap;
}
#maincontent .tabs > li {
display: block;
max-width: 100%;
}
#maincontent .cbi-value > .cbi-value-field {
min-width: 0;
}
@media screen and (max-width: 768px) {
/* Keep client details readable while retaining the sortable column headings. */
#maincontent #status_leases,
#maincontent #status_leases > tbody {
display: block;
max-width: 100%;
}
#status_leases .tr {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: .15rem .5rem;
margin: .5rem 0;
padding: .5rem;
border: 1px solid var(--border_color);
border-radius: 8px;
}
#status_leases .td {
display: grid;
grid-column: 1 / -1;
grid-template-columns: 5.5rem minmax(0, 1fr);
gap: .5rem;
min-width: 0;
padding: .15rem 0;
text-align: left;
white-space: normal;
overflow-wrap: anywhere;
}
#status_leases .td::before {
display: block;
content: attr(data-title);
padding: 0;
font-weight: normal;
white-space: normal;
opacity: .7;
}
#status_leases .td:first-child {
position: absolute;
top: .7rem;
left: .5rem;
display: flex;
align-items: center;
}
#status_leases .td:nth-child(2) {
display: block;
padding-left: 1.5rem;
font-weight: 600;
}
#status_leases .td:first-child::before,
#status_leases .td:nth-child(2)::before {
display: none;
}
#status_leases .td > * {
min-width: 0;
max-width: 100%;
}
#status_leases .td:nth-child(4):not(:has(div)) {
display: none;
}
#status_leases .design-client-mac {
display: inline-block;
white-space: nowrap;
}
#status_leases .td:nth-child(6),
#status_leases .td:nth-child(7),
#status_leases .td:nth-child(9) {
grid-column: auto;
grid-template-columns: auto minmax(0, 1fr);
gap: .35rem;
align-items: center;
}
#status_leases .td:nth-child(8) {
grid-template-columns: max-content minmax(0, 1fr);
}
#status_leases .td:nth-child(8)::before,
#status_leases .luci-client-rate[data-direction="total"] {
white-space: nowrap;
}
#status_leases .td.cbi-section-actions {
display: flex;
grid-column: auto;
align-items: center;
justify-content: flex-end;
}
#status_leases .td.cbi-section-actions::before {
display: none;
}
#status_leases .cbi-button {
min-width: 0;
margin: 0;
padding: .3rem .5rem;
font-size: .8rem;
}
#status_leases .tr.table-titles {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
gap: .35rem;
margin: 0;
padding: .25rem 0 .5rem;
border: 0;
border-radius: 0;
}
#status_leases .th {
display: block;
flex: 0 0 auto;
padding: .5rem;
border-radius: 4px;
background: var(--inputbg_color);
}
#status_leases .tr.placeholder,
#status_leases .tr.placeholder > .td {
position: static;
display: block;
height: auto;
}
[data-page="admin-network-wireless"] #cbi-wireless .cbi-section-table,
[data-page="admin-network-wireless"] #cbi-wireless .cbi-section-tbody,
#maincontent #wifi_assoclist_table {
display: block;
max-width: 100%;
}
[data-page="admin-network-wireless"] #cbi-wireless .cbi-section-table-row {
position: relative;
display: block;
padding: .65rem;
margin-bottom: .6rem;
border: 1px solid var(--border_color);
border-radius: 8px;
}
[data-page="admin-network-wireless"] #cbi-wireless .cbi-section-table-row > .td {
display: block;
padding: 0;
white-space: normal;
overflow-wrap: anywhere;
}
[data-page="admin-network-wireless"] #cbi-wireless .cbi-section-table-row > .td[data-name="_badge"],
[data-page="admin-network-wireless"] #cbi-wireless .cbi-section-table-row > .td.cbi-section-actions {
width: auto !important;
min-width: 0 !important;
max-width: 100%;
}
[data-page="admin-network-wireless"] #cbi-wireless .td[data-name="_badge"] {
position: absolute;
top: .65rem;
right: .65rem;
}
[data-page="admin-network-wireless"] #cbi-wireless .design-wireless-radio > .td[data-name="_badge"] {
right: auto;
left: .65rem;
}
[data-page="admin-network-wireless"] #cbi-wireless .design-wireless-radio > .td[data-name="_stat"] {
min-height: 64px;
margin-left: 72px;
}
[data-page="admin-network-wireless"] #cbi-wireless .td[data-name="_stat"] big {
font-size: .95rem;
}
[data-page="admin-network-wireless"] #cbi-wireless .td[data-name="_stat"] .nowrap {
display: block;
white-space: normal;
}
[data-page="admin-network-wireless"] #cbi-wireless .cbi-section-table-row:not(.design-wireless-radio) .td[data-name="_stat"] > div > .nowrap:first-child {
padding-right: 85px;
font-weight: 600;
}
[data-page="admin-network-wireless"] #cbi-wireless .cbi-section-table-row:not(.design-wireless-radio) .td[data-name="_stat"] strong {
display: inline-block;
min-width: 4.5rem;
}
[data-page="admin-network-wireless"] #cbi-wireless .td[data-name="_stat"] br,
[data-page="admin-network-wireless"] #cbi-wireless .design-wireless-sep,
[data-page="admin-network-wireless"] #cbi-wireless .wireless-status-sep {
display: none;
}
[data-page="admin-network-wireless"] #cbi-wireless .td.cbi-section-actions > div {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: .35rem;
margin-top: .5rem;
}
[data-page="admin-network-wireless"] #cbi-wireless .cbi-button {
width: 100%;
min-width: 0;
margin: 0;
padding: .4rem;
font-size: .85rem;
}
#wifi_assoclist_table .tr {
display: block;
margin: .5rem 0;
padding: .5rem;
border: 1px solid var(--border_color);
border-radius: 8px;
}
#wifi_assoclist_table .td {
display: grid;
grid-template-columns: 5.5rem minmax(0, 1fr);
gap: .5rem;
padding: .2rem 0;
text-align: left;
white-space: normal;
overflow-wrap: anywhere;
}
#wifi_assoclist_table .td::before {
display: block;
content: attr(data-title);
padding: 0;
font-weight: normal;
white-space: normal;
opacity: .7;
}
#wifi_assoclist_table .td > * {
min-width: 0;
max-width: 100%;
}
#wifi_assoclist_table .design-client-mac {
display: inline-block;
white-space: nowrap;
}
#wifi_assoclist_table .tr.table-titles {
display: flex;
overflow-x: auto;
gap: .35rem;
margin: 0;
padding: .25rem 0 .5rem;
border: 0;
border-radius: 0;
}
#wifi_assoclist_table .th {
display: block;
flex: 0 0 auto;
padding: .5rem;
border-radius: 4px;
background: var(--inputbg_color);
}
#wifi_assoclist_table .tr.placeholder,
#wifi_assoclist_table .tr.placeholder > .td {
position: static;
display: block;
height: auto;
text-align: center;
}
#wifi_assoclist_table .tr.placeholder > .td::before {
display: none;
}
#maincontent .cbi-value,
#modal_overlay .cbi-value {
display: flex;
}
#maincontent .cbi-value > .cbi-value-title,
#maincontent .cbi-value > .cbi-value-field,
#modal_overlay .cbi-value > .cbi-value-title,
#modal_overlay .cbi-value > .cbi-value-field {
width: 100%;
max-width: 100%;
min-width: 0;
}
#maincontent input:not([type="checkbox"]):not([type="radio"]),
#maincontent select,
#maincontent textarea,
#maincontent .cbi-dropdown,
#modal_overlay input:not([type="checkbox"]):not([type="radio"]),
#modal_overlay select,
#modal_overlay textarea,
#modal_overlay .cbi-dropdown {
min-width: 0;
max-width: 100%;
}
#modal_overlay .cbi-section,
#modal_overlay .cbi-section-node,
#modal_overlay .network-status-table .ifacebox-body .ifacebadge {
min-width: 0;
max-width: 100%;
}
#modal_overlay .cbi-value-field .ifacebadge {
white-space: normal;
overflow-wrap: anywhere;
}
[data-page="admin-system-package-manager"] #maincontent .controls > * {
flex-basis: 100%;
padding: .25rem 0;
gap: .4rem;
}
[data-page="admin-system-package-manager"] #maincontent .control-group > input {
flex: 1 1 0;
width: 0;
margin: 0;
}
[data-page="admin-system-package-manager"] #maincontent .control-group > .btn {
flex: 0 0 auto;
margin: 0;
min-height: 2.8rem;
display: flex;
align-items: center;
justify-content: center;
white-space: normal;
line-height: 1.3;
}
[data-page="admin-system-package-manager"] #maincontent #action-label + .control-group {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
[data-page="admin-system-package-manager"] #maincontent #action-label + .control-group > :last-child {
grid-column: 1 / -1;
}
[data-page="admin-system-package-manager"] #maincontent .controls > *,
[data-page="admin-system-package-manager"] #maincontent .controls > * > * {
min-width: 0;
max-width: 100%;
white-space: normal;
}
[data-page="admin-system-package-manager"] #maincontent .control-group {
display: flex;
flex-wrap: wrap;
gap: .25rem;
}
}
#maincontent .cbi-section,
#maincontent .cbi-section-node,
#maincontent .cbi-value > .cbi-section {
min-width: 0;
max-width: 100%;
}
@media screen and (max-width: 768px) {
#maincontent .cbi-value > .cbi-section {
width: 100%;
}
}
#maincontent .cbi-map-descr,
#maincontent .cbi-section-descr,
#maincontent .cbi-value-description {
overflow-wrap: anywhere;
}
@media screen and (max-width: 992px) {
.showSide {
top: 0;
left: 0;
width: 60px;
height: 55px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
}
#maincontent .cbi-section-create {
flex-wrap: wrap;
max-width: 100%;
}
#maincontent .cbi-section-create > * {
min-width: 0;
max-width: 100%;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
code, pre { code, pre {
@@ -66,10 +66,6 @@
} }
function getOverviewPortColumns(width, cardCount) { function getOverviewPortColumns(width, cardCount) {
if (width <= 480) {
return 1;
}
if (width <= 768) { if (width <= 768) {
return Math.min(cardCount, 2); return Math.min(cardCount, 2);
} }
@@ -80,6 +76,7 @@
function syncOverviewPortStatus() { function syncOverviewPortStatus() {
var view = document.getElementById('view'); var view = document.getElementById('view');
var portGroups = new Map(); var portGroups = new Map();
var portIconSelector = 'img[src*="/luci-static/resources/icons/port_"], img[src*="/images/port_"]';
var mediaPath = (L.env && L.env.media) ? L.env.media : '/luci-static/design'; var mediaPath = (L.env && L.env.media) ? L.env.media : '/luci-static/design';
var iconMap = { var iconMap = {
'alias': 'ethernet', 'alias': 'ethernet',
@@ -148,7 +145,7 @@
grid.style && grid.style &&
grid.style.display === 'grid' && grid.style.display === 'grid' &&
grid.style.gridTemplateColumns.indexOf('minmax(70px, 1fr)') !== -1 && grid.style.gridTemplateColumns.indexOf('minmax(70px, 1fr)') !== -1 &&
grid.querySelector('img[src*="/luci-static/resources/icons/port_"]'); grid.querySelector(portIconSelector);
if (!isPortGrid) { if (!isPortGrid) {
return; return;
@@ -161,7 +158,7 @@
}); });
}); });
Array.prototype.forEach.call(view.querySelectorAll('img[src*="/luci-static/resources/icons/port_"]'), function (img) { Array.prototype.forEach.call(view.querySelectorAll(portIconSelector), function (img) {
var card = img.closest('.ifacebox'); var card = img.closest('.ifacebox');
var slot = card ? card.closest('.port-status-slot') : null; var slot = card ? card.closest('.port-status-slot') : null;
var parent = card ? (slot ? slot.parentElement : card.parentElement) : null; var parent = card ? (slot ? slot.parentElement : card.parentElement) : null;
@@ -375,25 +372,96 @@
}); });
} }
function syncWirelessCards() {
document.querySelectorAll('#cbi-wireless .cbi-section-table-row').forEach(function (row) {
row.classList.toggle('design-wireless-radio', !!row.querySelector('.wireless-radio-badge'));
row.querySelectorAll('[data-name="_stat"] div').forEach(function (container) {
Array.from(container.childNodes).forEach(function (node) {
if (node.nodeType === Node.TEXT_NODE && /^\s*\|\s*$/.test(node.textContent)) {
var separator = document.createElement('span');
separator.className = 'design-wireless-sep';
container.replaceChild(separator, node);
separator.appendChild(node);
}
});
});
});
}
function initWirelessIcons() { function initWirelessIcons() {
if (document.body.getAttribute('data-page') !== 'admin-network-wireless') { if (document.body.getAttribute('data-page') !== 'admin-network-wireless') {
return; return;
} }
syncWirelessIcons(); syncWirelessIcons();
syncWirelessCards();
new MutationObserver(syncWirelessIcons).observe(document.body, { new MutationObserver(function () {
syncWirelessIcons();
syncWirelessCards();
}).observe(document.body, {
childList: true, childList: true,
subtree: true subtree: true
}); });
} }
function initResponsiveTables() {
var pending = false;
function sync() {
pending = false;
document.querySelectorAll('#status_leases .tr > .td:nth-child(5), #wifi_assoclist_table .tr > .td:nth-child(2)').forEach(function (cell) {
var text = cell.firstChild;
var match = text && text.nodeType === Node.TEXT_NODE && text.textContent.match(/^([\da-f]{2}(?::[\da-f]{2}){5})(.*)$/i);
if (match && cell.childNodes.length === 1) {
var address = document.createElement('span');
var mac = document.createElement('span');
mac.className = 'design-client-mac';
mac.textContent = match[1];
address.appendChild(mac);
address.appendChild(document.createTextNode(match[2]));
cell.replaceChild(address, text);
}
});
document.querySelectorAll('#maincontent table, #maincontent .table, #modal_overlay table, #modal_overlay .table').forEach(function (table) {
var parent = table.parentElement;
var style = window.getComputedStyle(parent);
var available = parent.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
if (available > 0 && table.getClientRects().length) {
table.classList.toggle('design-table-scroll', table.scrollWidth > available + 1);
}
});
}
function schedule() {
if (!pending) {
pending = true;
window.requestAnimationFrame(sync);
}
}
new MutationObserver(schedule).observe(document.body, {
childList: true,
subtree: true
});
window.addEventListener('resize', schedule);
document.addEventListener('click', schedule);
document.fonts.ready.then(schedule);
schedule();
}
function init() { function init() {
applySafeAreaFix(); applySafeAreaFix();
initIndicators(); initIndicators();
initOverviewPortStatus(); initOverviewPortStatus();
initNetworkInterfaceIcons(); initNetworkInterfaceIcons();
initWirelessIcons(); initWirelessIcons();
initResponsiveTables();
setHeaderShadow(); setHeaderShadow();
$(window).on('resize', setHeaderShadow); $(window).on('resize', setHeaderShadow);
} }
@@ -21,7 +21,7 @@
const show_proxy_icon = access('/etc/config/shadowsocksr') || access('/etc/config/google_fu_mode'); const show_proxy_icon = access('/etc/config/shadowsocksr') || access('/etc/config/google_fu_mode');
const show_openclash_icon = access('/etc/config/openclash'); const show_openclash_icon = access('/etc/config/openclash');
const show_homeproxy_icon = access('/etc/config/homeproxy'); const show_homeproxy_icon = access('/etc/config/homeproxy');
const theme_rev = `${version?.luciversion ?? 'dev'}-designfix-20260927`; const theme_rev = `${version?.luciversion ?? 'dev'}-designfix-20261108-3`;
http.prepare_content('text/html; charset=UTF-8'); http.prepare_content('text/html; charset=UTF-8');
-%} -%}
+2 -2
View File
@@ -2,11 +2,11 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-fluent PKG_NAME:=luci-theme-fluent
PKG_VERSION:=1.0.9 PKG_VERSION:=1.0.9
PKG_RELEASE:=2 PKG_RELEASE:=3
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
LUCI_TITLE:=Fluent Theme for LuCI LUCI_TITLE:=Fluent Theme for LuCI
LUCI_DEPENDS:=+luci-base LUCI_DEPENDS:=+luci-base +flock
LUCI_MINIFY_CSS:=0 LUCI_MINIFY_CSS:=0
define Package/$(PKG_NAME)/conffiles define Package/$(PKG_NAME)/conffiles
/etc/config/fluent /etc/config/fluent
@@ -6676,6 +6676,52 @@ cbi-dropdown[open] > ul.dropdown:not(.preview) > li .create-item-input, .cbi-dro
outline: none; outline: none;
} }
cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group {
cursor: default;
padding: 0;
display: block;
}
cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > .fluent-select-group__label, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > .fluent-select-group__label, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > .fluent-select-group__label {
padding: var(--fluent-space-xxs) var(--fluent-space-xs);
color: var(--fluent-text-secondary);
display: block;
}
cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul {
margin: 0;
padding: 0;
list-style: none;
}
cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"], .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"], .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"] {
min-height: var(--fluent-input-height);
padding: 5px var(--fluent-space-xs);
border-radius: var(--fluent-radius-md);
cursor: pointer;
align-items: center;
display: flex;
}
cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"]:hover, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"]:hover, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"]:hover {
background-color: var(--fluent-dropdown-option-hover);
}
cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][selected], .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][selected], .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][selected] {
background-color: var(--fluent-dropdown-option-selected);
font-weight: var(--fluent-font-weight-semibold);
}
cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][disabled], .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][disabled], .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][disabled] {
color: var(--fluent-text-disabled);
cursor: not-allowed;
pointer-events: none;
}
cbi-dropdown.fluent-custom-select > ul.dropdown [hidden], .cbi-dropdown.fluent-custom-select > ul.dropdown [hidden], .fluent-custom-select.fluent-custom-select > ul.dropdown [hidden] {
display: none;
}
cbi-dropdown > select, .cbi-dropdown > select, .fluent-custom-select > select { cbi-dropdown > select, .cbi-dropdown > select, .fluent-custom-select > select {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
@@ -6841,12 +6887,12 @@ cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:dis
background-size: 12px 12px !important; background-size: 12px 12px !important;
} }
cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li { cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li, cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"], .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"], .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"] {
padding-inline-start: 32px; padding-inline-start: 32px;
position: relative !important; position: relative !important;
} }
cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li:before { cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li:before, cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li:before, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li:before, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"]:before {
--icon-check: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=); --icon-check: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=);
content: "" !important; content: "" !important;
inset-inline-start: 8px !important; inset-inline-start: 8px !important;
@@ -6868,14 +6914,18 @@ cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li:before, .cbi-dropd
mask-repeat: no-repeat !important; mask-repeat: no-repeat !important;
} }
cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[selected]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[selected]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li[selected]:before { cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[selected]:before, cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][selected]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[selected]:before, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][selected]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li[selected]:before, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][selected]:before {
background-color: var(--fluent-text) !important; background-color: var(--fluent-text) !important;
} }
cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[disabled][selected]:before, cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[unselectable][selected]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[disabled][selected]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[unselectable][selected]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li[disabled][selected]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li[unselectable][selected]:before { cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[disabled][selected]:before, cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[unselectable][selected]:before, cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][disabled][selected]:before, cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][unselectable][selected]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[disabled][selected]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[unselectable][selected]:before, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][disabled][selected]:before, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][unselectable][selected]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li[disabled][selected]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li[unselectable][selected]:before, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][disabled][selected]:before, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group > ul > li[role="option"][unselectable][selected]:before {
background-color: var(--fluent-text-disabled) !important; background-color: var(--fluent-text-disabled) !important;
} }
cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group:before, .cbi-dropdown.fluent-custom-select > ul.dropdown > .fluent-select-group:before, .fluent-custom-select.fluent-custom-select > ul.dropdown > .fluent-select-group:before {
display: none !important;
}
cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) { cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) {
background: none !important; background: none !important;
padding: 0 !important; padding: 0 !important;
@@ -10048,6 +10098,17 @@ html[dir="rtl"] .fluent-menu-editor__category-header:after, body[data-fluent-dir
display: flex; display: flex;
} }
.fluent-about-manager [hidden] {
display: none;
}
.fluent-about-manager > .fluent-update-changelog {
white-space: pre-wrap;
overflow-wrap: anywhere;
max-height: 12rem;
overflow: auto;
}
.fluent-about-manager h3 { .fluent-about-manager h3 {
color: var(--fluent-text); color: var(--fluent-text);
margin: 8px 0 4px; margin: 8px 0 4px;
@@ -10124,23 +10185,6 @@ html[dir="rtl"] .fluent-menu-editor__category-header:after, body[data-fluent-dir
display: flex; display: flex;
} }
.fluent-update-controls .fluent-update-channel-selector {
color: var(--fluent-text);
align-items: center;
gap: 8px;
font-size: 13px;
display: flex;
}
.fluent-update-controls .fluent-update-channel-selector label {
color: var(--fluent-text-secondary);
}
.fluent-update-controls .fluent-update-channel-selector select {
min-width: 140px;
padding: 4px 8px;
}
.fluent-update-status { .fluent-update-status {
border-radius: var(--fluent-radius-sm); border-radius: var(--fluent-radius-sm);
background-color: var(--fluent-bg); background-color: var(--fluent-bg);
@@ -10214,71 +10258,6 @@ html[dir="rtl"] .fluent-menu-editor__category-header:after, body[data-fluent-dir
overflow-y: auto; overflow-y: auto;
} }
.fluent-update-card .fluent-update-footer {
justify-content: flex-end;
display: flex;
}
.fluent-update-card .fluent-update-success {
text-align: center;
padding: 8px 0;
}
.fluent-update-card .fluent-update-success h4 {
color: #107c41;
margin: 0 0 8px;
font-weight: 600;
}
.fluent-update-card .fluent-update-success p {
color: var(--fluent-text-secondary);
margin: 0;
font-size: 13px;
}
.fluent-progress-bar {
background-color: var(--fluent-border);
border-radius: 2px;
width: 100%;
height: 4px;
position: relative;
overflow: hidden;
}
.fluent-progress-bar__fill {
background-color: var(--fluent-primary);
border-radius: 2px;
height: 100%;
transition: width .2s ease-in-out;
}
.fluent-progress-bar__fill.fill-verify, .fluent-progress-bar__fill.fill-install {
transform-origin: 0;
animation: 1.5s linear infinite fluent-indeterminate-progress;
width: 100% !important;
}
.fluent-progress-text {
color: var(--fluent-text-secondary);
text-align: center;
margin-top: -8px;
font-size: 12px;
}
@keyframes fluent-indeterminate-progress {
0% {
transform: translateX(-100%) scaleX(.2);
}
50% {
transform: translateX(-20%) scaleX(.5);
}
100% {
transform: translateX(100%) scaleX(.2);
}
}
.fluent-mac-selector-group { .fluent-mac-selector-group {
align-items: center; align-items: center;
gap: var(--fluent-space-xs); gap: var(--fluent-space-xs);
@@ -1670,28 +1670,32 @@ function setupSelectionPause() {
;// CONCATENATED MODULE: ./web/resources/utils/select-dropdown.tsx ;// CONCATENATED MODULE: ./web/resources/utils/select-dropdown.tsx
let select_dropdown_r = !1; let select_dropdown_l = !1, select_dropdown_a = !1, select_dropdown_s = new WeakMap(), select_dropdown_d = 0;
function select_dropdown_s() { function select_dropdown_c() {
document.querySelectorAll("cbi-dropdown[open], .cbi-dropdown[open], .fluent-custom-select[open]").forEach((e)=>{ document.querySelectorAll("cbi-dropdown[open], .cbi-dropdown[open], .fluent-custom-select[open]").forEach((e)=>{
if (e instanceof HTMLElement) if (e.classList.contains("fluent-custom-select")) { if (e instanceof HTMLElement) if (e.classList.contains("fluent-custom-select")) {
let t = e.previousElementSibling; let t = e.previousElementSibling;
t instanceof HTMLSelectElement && select_dropdown_f(e, t); t instanceof HTMLSelectElement && select_dropdown_h(e, t);
} else select_dropdown_a(e); } else select_dropdown_u(e);
}); });
} }
function select_dropdown_a(e) { function select_dropdown_u(e) {
let t = e.getBoundingClientRect(), n = window.innerHeight - t.bottom, o = t.top, l = e.querySelector("ul.dropdown, ul:not(.preview)"), i = l ? l.querySelectorAll("li").length : 8, r = l ? l.scrollHeight : 0, s = Math.min(r > 0 ? r + 8 : 36 * i + 10, select_dropdown_c), a = n < s && o > n, d = a ? Math.max(64, o - select_dropdown_u - select_dropdown_p) : Math.max(64, n - select_dropdown_u - select_dropdown_p); let t = e.getBoundingClientRect(), n = window.innerHeight - t.bottom, r = t.top, i = e.querySelector("ul.dropdown, ul:not(.preview)"), o = i ? i.querySelectorAll("li").length : 8, l = i ? i.scrollHeight : 0, a = Math.min(l > 0 ? l + 8 : 36 * o + 10, select_dropdown_b), s = n < a && r > n, d = s ? Math.max(64, r - select_dropdown_m - select_dropdown_f) : Math.max(64, n - select_dropdown_m - select_dropdown_f);
e.setAttribute("data-open-direction", a ? "up" : "down"), e.style.setProperty("--fluent-dropdown-max-height", `${Math.min(select_dropdown_c, s, d)}px`); e.setAttribute("data-open-direction", s ? "up" : "down"), e.style.setProperty("--fluent-dropdown-max-height", `${Math.min(select_dropdown_b, a, d)}px`);
} }
function setupFluentSelects() { function setupFluentSelects() {
select_dropdown_r || (select_dropdown_r = !0, window.addEventListener("scroll", select_dropdown_s, !0), window.addEventListener("resize", select_dropdown_s), new MutationObserver((e)=>{ select_dropdown_l || (select_dropdown_l = !0, window.addEventListener("scroll", select_dropdown_c, !0), window.addEventListener("resize", select_dropdown_c), document.addEventListener("click", (e)=>{
document.querySelectorAll(".fluent-custom-select[open]").forEach((t)=>{
t.contains(e.target) || e.target === t.previousElementSibling || select_dropdown_y(t);
});
}, !0), new MutationObserver((e)=>{
e.forEach((e)=>{ e.forEach((e)=>{
if ("attributes" === e.type && "open" === e.attributeName) { if ("attributes" === e.type && "open" === e.attributeName) {
let t = e.target; let t = e.target;
if (t.hasAttribute("open")) if (t.classList.contains("fluent-custom-select")) { if (t.hasAttribute("open")) if (t.classList.contains("fluent-custom-select")) {
let e = t.previousElementSibling; let e = t.previousElementSibling;
e instanceof HTMLSelectElement && select_dropdown_f(t, e); e instanceof HTMLSelectElement && select_dropdown_h(t, e);
} else ("CBI-DROPDOWN" === t.tagName || t.classList.contains("cbi-dropdown")) && select_dropdown_a(t); } else ("CBI-DROPDOWN" === t.tagName || t.classList.contains("cbi-dropdown")) && select_dropdown_u(t);
} }
}); });
}).observe(document.body, { }).observe(document.body, {
@@ -1700,55 +1704,59 @@ function setupFluentSelects() {
"open" "open"
], ],
subtree: !0 subtree: !0
})), "0" === document.body.getAttribute("data-theme-custom-select") || (document.querySelectorAll("select").forEach((e)=>{ })), "0" === document.body.getAttribute("data-theme-custom-select") || select_dropdown_a || (select_dropdown_a = !0, document.querySelectorAll("select").forEach((e)=>{
select_dropdown_h(e); select_dropdown_v(e);
}), document.querySelectorAll("cbi-dropdown, .cbi-dropdown").forEach((e)=>{ }), document.querySelectorAll("cbi-dropdown, .cbi-dropdown").forEach((e)=>{
e.classList.contains("fluent-custom-select") || select_dropdown_d(e); e.classList.contains("fluent-custom-select") || select_dropdown_p(e);
}), new MutationObserver((e)=>{ }), new MutationObserver((e)=>{
let t = new Set();
e.forEach((e)=>{ e.forEach((e)=>{
e.addedNodes.forEach((e)=>{ e.removedNodes.forEach((e)=>{
e.nodeType === Node.ELEMENT_NODE && ("SELECT" === e.tagName ? select_dropdown_h(e) : "CBI-DROPDOWN" === e.tagName || e.classList.contains("cbi-dropdown") ? e.classList.contains("fluent-custom-select") || select_dropdown_d(e) : (e.querySelectorAll("select").forEach((e)=>{ e instanceof HTMLSelectElement ? t.add(e) : e instanceof Element && e.querySelectorAll("select").forEach((e)=>{
select_dropdown_h(e); t.add(e);
});
}), e.addedNodes.forEach((e)=>{
e.nodeType === Node.ELEMENT_NODE && ("SELECT" === e.tagName ? select_dropdown_v(e) : "CBI-DROPDOWN" === e.tagName || e.classList.contains("cbi-dropdown") ? e.classList.contains("fluent-custom-select") || select_dropdown_p(e) : (e.querySelectorAll("select").forEach((e)=>{
select_dropdown_v(e);
}), e.querySelectorAll("cbi-dropdown, .cbi-dropdown").forEach((e)=>{ }), e.querySelectorAll("cbi-dropdown, .cbi-dropdown").forEach((e)=>{
e.classList.contains("fluent-custom-select") || select_dropdown_d(e); e.classList.contains("fluent-custom-select") || select_dropdown_p(e);
}))); })));
}); });
}), t.forEach((e)=>{
let t = select_dropdown_s.get(e);
t && (e.isConnected ? (e.nextElementSibling !== t.dropdown && (select_dropdown_y(t.dropdown), e.after(t.dropdown)), t.refreshAncestors()) : t.dispose());
}); });
}).observe(document.body, { }).observe(document.body, {
childList: !0, childList: !0,
subtree: !0 subtree: !0
})); }));
} }
function select_dropdown_d(e) { function select_dropdown_p(e) {
let t; let t;
(t = e.querySelector("span.open")) && "true" !== t.getAttribute("data-chevron-upgraded") && (t.setAttribute("data-chevron-upgraded", "true"), t.innerHTML = ""); (t = e.querySelector("span.open")) && "true" !== t.getAttribute("data-chevron-upgraded") && (t.setAttribute("data-chevron-upgraded", "true"), t.innerHTML = "");
let n = e.querySelector("ul:not(.preview)"); let n = e.querySelector("ul:not(.preview)");
n && !n.classList.contains("dropdown") && n.classList.add("dropdown"); n && !n.classList.contains("dropdown") && n.classList.add("dropdown");
} }
let select_dropdown_c = 350, select_dropdown_u = 8, select_dropdown_p = 2; let select_dropdown_b = 350, select_dropdown_m = 8, select_dropdown_f = 2;
function select_dropdown_b(e, t) { function select_dropdown_h(e, t) {
let n = t.offsetTop, o = n + t.offsetHeight, l = e.scrollTop, i = l + e.clientHeight; let l = e.getBoundingClientRect(), a = getEffectiveDocumentDirection(), s = getViewportInlineSize(), d = window.innerHeight, c = d - l.bottom, u = l.top;
n < l ? e.scrollTop = n : o > i && (e.scrollTop = o - e.clientHeight); if (c < Math.min(32 * t.options.length + 10, select_dropdown_b) && u > c ? e.setAttribute("data-open-direction", "up") : e.setAttribute("data-open-direction", "down"), !e.closest("#modal_overlay .modal, .fluent-mac-overlay-card, [class*='overlay']")) return void e.removeAttribute("data-fluent-floating");
} let p = Math.min(32 * t.options.length + 10, select_dropdown_b, 0.45 * d), h = c < p && u > c, y = Math.min(p, Math.max(64, h ? u - select_dropdown_m - select_dropdown_f : c - select_dropdown_m - select_dropdown_f)), v = Math.min(l.width, s - 2 * select_dropdown_m), w = Math.min(Math.max(select_dropdown_m, getRectInlineStart(l, a, s)), s - v - select_dropdown_m), A = h ? Math.max(select_dropdown_m, l.top - y - select_dropdown_f) : Math.min(d - select_dropdown_m - y, l.bottom + select_dropdown_f);
function select_dropdown_f(e, t) {
let r = e.getBoundingClientRect(), s = getEffectiveDocumentDirection(), a = getViewportInlineSize(), d = window.innerHeight, b = d - r.bottom, f = r.top;
if (b < Math.min(32 * t.options.length + 10, select_dropdown_c) && f > b ? e.setAttribute("data-open-direction", "up") : e.setAttribute("data-open-direction", "down"), !e.closest("#modal_overlay .modal, .fluent-mac-overlay-card, [class*='overlay']")) return void e.removeAttribute("data-fluent-floating");
let m = Math.min(32 * t.options.length + 10, select_dropdown_c, 0.45 * d), h = b < m && f > b, v = Math.min(m, Math.max(64, h ? f - select_dropdown_u - select_dropdown_p : b - select_dropdown_u - select_dropdown_p)), w = Math.min(r.width, a - 2 * select_dropdown_u), y = Math.min(Math.max(select_dropdown_u, getRectInlineStart(r, s, a)), a - w - select_dropdown_u), A = h ? Math.max(select_dropdown_u, r.top - v - select_dropdown_p) : Math.min(d - select_dropdown_u - v, r.bottom + select_dropdown_p);
e.setAttribute("data-open-direction", h ? "up" : "down"), e.setAttribute("data-fluent-floating", "modal"), setInlineCssCustomProperties(e.style, { e.setAttribute("data-open-direction", h ? "up" : "down"), e.setAttribute("data-fluent-floating", "modal"), setInlineCssCustomProperties(e.style, {
inlineStart: y, inlineStart: w,
inlineSize: w inlineSize: v
}, s, a, { }, a, s, {
inlineStart: "--fluent-dropdown-left", inlineStart: "--fluent-dropdown-left",
inlineSize: "--fluent-dropdown-width" inlineSize: "--fluent-dropdown-width"
}), e.style.setProperty("--fluent-dropdown-top", `${A}px`), e.style.setProperty("--fluent-dropdown-max-height", `${v}px`); }), e.style.setProperty("--fluent-dropdown-top", `${A}px`), e.style.setProperty("--fluent-dropdown-max-height", `${y}px`);
} }
function select_dropdown_m(e) { function select_dropdown_y(e) {
e.removeAttribute("open"), e.removeAttribute("data-fluent-floating"), e.style.removeProperty("--fluent-dropdown-left"), e.style.removeProperty("--fluent-dropdown-top"), e.style.removeProperty("--fluent-dropdown-width"), e.style.removeProperty("--fluent-dropdown-max-height"), e.closest(".cbi-value-field, .cbi-value")?.classList.remove("cbi-dropdown-open"); e.removeAttribute("open"), e.setAttribute("aria-expanded", "false"), e.removeAttribute("aria-activedescendant"), e.removeAttribute("data-fluent-floating"), e.style.removeProperty("--fluent-dropdown-left"), e.style.removeProperty("--fluent-dropdown-top"), e.style.removeProperty("--fluent-dropdown-width"), e.style.removeProperty("--fluent-dropdown-max-height"), e.closest(".cbi-value-field, .cbi-value")?.classList.remove("cbi-dropdown-open");
let t = e.querySelector("ul.dropdown"); let t = e.querySelector("ul.dropdown");
t instanceof HTMLElement && "function" == typeof t.hidePopover && t.matches(":popover-open") && t.hidePopover(); t instanceof HTMLElement && "function" == typeof t.hidePopover && t.matches(":popover-open") && t.hidePopover();
} }
function select_dropdown_h(n) { function select_dropdown_v(n) {
if (n?.tagName !== "SELECT" || "true" === n.getAttribute("data-fluent-transformed") || n.closest(".cbi-dropdown") || n.multiple || function(e) { if (n?.tagName !== "SELECT" || !n.isConnected || "true" === n.getAttribute("data-fluent-transformed") || n.closest(".cbi-dropdown") || n.multiple || function(e) {
let t = e.getAttribute("style") || ""; let t = e.getAttribute("style") || "";
if (/\bdisplay\s*:\s*none/i.test(t) || "none" === e.style.display) return !0; if (/\bdisplay\s*:\s*none/i.test(t) || "none" === e.style.display) return !0;
if (!e.isConnected) return !1; if (!e.isConnected) return !1;
@@ -1765,163 +1773,207 @@ function select_dropdown_h(n) {
t = t.parentElement; t = t.parentElement;
} }
if (!n) return !0; if (!n) return !0;
let o = e.cloneNode(!1); let r = e.cloneNode(!1);
o.style.display = "", document.body.appendChild(o); r.style.display = "", document.body.appendChild(r);
let l = window.getComputedStyle(o), i = "none" === l.display; let i = window.getComputedStyle(r), o = "none" === i.display;
return document.body.removeChild(o), i; return document.body.removeChild(r), o;
} }
} catch (e) {} } catch (e) {}
return !1; return !1;
}(n)) return; }(n)) return;
let o = n.getAttribute("style"); let r = n.getAttribute("style"), i = n.style.getPropertyValue("display"), o = n.style.getPropertyPriority("display");
n.setAttribute("data-fluent-transformed", "true"), n.style.setProperty("display", "none", "important"); n.setAttribute("data-fluent-transformed", "true"), n.style.setProperty("display", "none", "important");
let l = jsx("li", {}); let l = jsx("li", {});
l.setAttribute("selected", ""); l.setAttribute("selected", "");
let i = jsx("ul", { let a = jsx("ul", {
class: "dropdown" class: "dropdown"
}), r = jsx("span", { });
a.id = `fluent-select-list-${++select_dropdown_d}`, a.setAttribute("role", "listbox");
let c = jsx("span", {
class: "open" class: "open"
}), s = jsxs("div", { }), u = jsxs("div", {
class: "fluent-custom-select", class: "fluent-custom-select",
children: [ children: [
jsx("ul", { jsx("ul", {
children: l children: l
}), }),
r, c,
i a
] ]
}); });
if (s.setAttribute("tabindex", "0"), o) { if (u.setAttribute("tabindex", "0"), u.setAttribute("role", "combobox"), u.setAttribute("aria-haspopup", "listbox"), u.setAttribute("aria-controls", a.id), u.setAttribute("aria-expanded", "false"), u.setAttribute("aria-required", String(n.required)), u.querySelector("ul:not(.dropdown)")?.setAttribute("aria-hidden", "true"), c.setAttribute("aria-hidden", "true"), r) {
let e = o.replace(/\bdisplay\s*:\s*[^;]+(;|$)/gi, "").trim(); let e = r.replace(/\bdisplay\s*:\s*[^;]+(;|$)/gi, "").trim();
e && s.setAttribute("style", e); e && u.setAttribute("style", e);
} }
n.disabled && (s.setAttribute("disabled", ""), s.removeAttribute("tabindex")); let p = new Map(), b = null, m = 0, f = 0, v = "", w = 0, A = null, g = [], E = (e)=>!e.hidden && !e.closest("[hidden]") && !e.matches(":disabled"), L = ()=>Array.from(n.options).filter((e)=>E(e)), x = (e)=>{
let a = ()=>{ if ((b = e && p.has(e) && E(e) ? e : null) && u.hasAttribute("open")) {
i.innerHTML = ""; let e = p.get(b);
let t = "", o = !1; if (e) {
if (Array.from(n.options).forEach((n)=>{ let t, n, r, i;
let l = jsx("li", { u.setAttribute("aria-activedescendant", e.id), n = (t = e.offsetTop) + e.offsetHeight, i = (r = a.scrollTop) + a.clientHeight, t < r ? a.scrollTop = t : n > i && (a.scrollTop = n - a.clientHeight);
children: n.text }
}); } else u.removeAttribute("aria-activedescendant");
l.setAttribute("data-value", n.value), n.selected && (l.setAttribute("selected", ""), t = n.text, o = !0), n.disabled && l.setAttribute("disabled", ""), i.appendChild(l); }, S = ()=>{
}), !o && n.options.length > 0) { let e = n.matches(":disabled");
let e = n.options[n.selectedIndex >= 0 ? n.selectedIndex : 0]; u.toggleAttribute("disabled", e), u.setAttribute("aria-disabled", String(e)), u.setAttribute("aria-required", String(n.required)), e ? (u.removeAttribute("tabindex"), select_dropdown_y(u)) : u.setAttribute("tabindex", "0");
t = e.text; let t = g.map((e)=>e.id).filter(Boolean), r = n.getAttribute("aria-labelledby");
let o = i.querySelector(`li[data-value="${e.value}"]`); if (r || t.length) u.setAttribute("aria-labelledby", r || t.join(" ")), u.removeAttribute("aria-label");
o?.setAttribute("selected", ""); else {
u.removeAttribute("aria-labelledby");
let e = n.getAttribute("aria-label") || g.map((e)=>e.textContent?.trim()).filter(Boolean).join(" ") || n.title;
e ? u.setAttribute("aria-label", e) : u.removeAttribute("aria-label");
} }
l.textContent = t; let i = n.options[n.selectedIndex];
for (let [e, t] of (l.textContent = i?.text ?? "", p)){
let n = e === i;
t.toggleAttribute("selected", n), t.setAttribute("aria-selected", String(n));
let r = !E(e);
t.toggleAttribute("disabled", r), t.setAttribute("aria-disabled", String(r));
}
b && !E(b) && x(i && E(i) ? i : null), u.hasAttribute("open") && !b && x(i && E(i) ? i : L()[0] || null);
}, M = ()=>{
let t = new Map(), r = null, i = null;
a.replaceChildren(), Array.from(n.options).forEach((n, o)=>{
let l = n.parentElement instanceof HTMLOptGroupElement ? n.parentElement : null;
if (l !== r && (r = l, i = null), l && !i) {
let t = jsx("li", {
class: "fluent-select-group"
});
t.setAttribute("role", "group"), t.setAttribute("aria-label", l.label), t.hidden = l.hidden;
let n = jsx("span", {
class: "fluent-select-group__label"
});
n.textContent = l.label, n.setAttribute("aria-hidden", "true"), (i = jsx("ul", {})).setAttribute("role", "presentation"), t.append(n, i), a.append(t);
}
let s = jsx("li", {});
s.id = `${a.id}-option-${o}`, s.textContent = n.text, s.setAttribute("role", "option"), s.hidden = n.hidden || !!l?.hidden, (i || a).append(s), t.set(n, s);
}), p = t, b && !p.has(b) && (b = null), S();
}, k = (e)=>{
!n.matches(":disabled") && E(e) && (n.selectedIndex = Array.from(n.options).indexOf(e), S(), n.dispatchEvent(new Event("input", {
bubbles: !0
})), n.dispatchEvent(new Event("change", {
bubbles: !0
})));
}, T = (e)=>{
!(e.target instanceof Element && e.target.closest("a, button, input, select, textarea")) && (n.matches(":disabled") || u.focus());
}, C = ()=>{
window.clearTimeout(f), f = window.setTimeout(()=>{
n.isConnected && (M(), x(null));
}, 0);
}, q = new MutationObserver(()=>S()), P = ()=>{
q.disconnect(), Array.from(function*() {
let e = n.parentElement;
for(; e;)e instanceof HTMLFieldSetElement && (yield e), e = e.parentElement;
}()).forEach((e)=>{
q.observe(e, {
attributes: !0,
attributeFilter: [
"disabled"
]
});
});
let e = n.form;
A !== e && (A?.removeEventListener("reset", C), A = e, A?.addEventListener("reset", C));
let t = Array.from(n.labels || []);
g.forEach((e)=>{
e.removeEventListener("click", T);
}), (g = t).forEach((e)=>{
e.addEventListener("click", T);
}), S();
}; };
a(), n.parentNode?.insertBefore(s, n.nextSibling), s.addEventListener("click", (e)=>{ M(), n.parentNode?.insertBefore(u, n.nextSibling), u.addEventListener("click", (e)=>{
if (s.hasAttribute("disabled")) return; if (u.hasAttribute("disabled")) return;
let t = e.target.closest("ul.dropdown > li"); let t = e.target.closest('[role="option"]');
if (t) { if (t && a.contains(t)) {
let o = t.getAttribute("data-value"); let n = Array.from(p).find(([, e])=>e === t)?.[0];
null === o || t.hasAttribute("disabled") || (n.value = o, n.dispatchEvent(new Event("change", { n && E(n) && k(n), select_dropdown_y(u), e.stopPropagation();
bubbles: !0
})), n.dispatchEvent(new Event("input", {
bubbles: !0
}))), select_dropdown_m(s), e.stopPropagation();
return; return;
} }
if (s.hasAttribute("open")) select_dropdown_m(s); if (!e.target.closest('[role="group"]')) {
else { if (u.hasAttribute("open")) select_dropdown_y(u);
document.querySelectorAll("cbi-dropdown[open], .cbi-dropdown[open], .fluent-custom-select[open]").forEach((e)=>{ else {
e instanceof HTMLElement && e.classList.contains("fluent-custom-select") ? select_dropdown_m(e) : (e.removeAttribute("open"), e.closest(".cbi-value-field, .cbi-value")?.classList.remove("cbi-dropdown-open")); document.querySelectorAll("cbi-dropdown[open], .cbi-dropdown[open], .fluent-custom-select[open]").forEach((e)=>{
}), select_dropdown_f(s, n), s.setAttribute("open", ""), s.closest(".cbi-value-field, .cbi-value")?.classList.add("cbi-dropdown-open"); e instanceof HTMLElement && e.classList.contains("fluent-custom-select") ? select_dropdown_y(e) : (e.removeAttribute("open"), e.closest(".cbi-value-field, .cbi-value")?.classList.remove("cbi-dropdown-open"));
let e = i.querySelector("li[selected]"); }), M(), select_dropdown_h(u, n), u.setAttribute("open", ""), u.setAttribute("aria-expanded", "true"), u.closest(".cbi-value-field, .cbi-value")?.classList.add("cbi-dropdown-open"), x(E(n.options[n.selectedIndex]) ? n.options[n.selectedIndex] : L()[0] || null);
e && select_dropdown_b(i, e); let e = ()=>{
if (!u.hasAttribute("open")) {
m = 0;
return;
}
S(), m = requestAnimationFrame(e);
};
m || (m = requestAnimationFrame(e));
}
e.stopPropagation();
} }
e.stopPropagation(); }), u.addEventListener("keydown", (e)=>{
}); if (u.hasAttribute("disabled")) return;
let d = (e)=>{ let t = u.hasAttribute("open"), n = L(), r = b ? n.indexOf(b) : -1;
!s.contains(e.target) && e.target !== n && s.hasAttribute("open") && select_dropdown_m(s);
};
document.addEventListener("click", d, !0), s.addEventListener("keydown", (e)=>{
if (s.hasAttribute("disabled")) return;
let t = s.hasAttribute("open"), o = Array.from(i.querySelectorAll("li:not([disabled])")), l = o.findIndex((e)=>e.hasAttribute("selected"));
switch(e.key){ switch(e.key){
case "Enter": case "Enter":
case " ": case " ":
if (e.preventDefault(), t) { e.preventDefault(), t ? b && (k(b), select_dropdown_y(u)) : u.click();
let e = o[l];
e && e.click();
} else s.click();
break; break;
case "Escape": case "Escape":
t && (e.preventDefault(), select_dropdown_m(s)); t && (e.preventDefault(), select_dropdown_y(u));
break; break;
case "ArrowDown": case "ArrowDown":
if (e.preventDefault(), t) { e.preventDefault(), t ? n.length > 0 && x(n[(r + 1) % n.length]) : u.click();
if (o.length > 0) {
let e = (l + 1) % o.length;
o.forEach((t, o)=>{
if (o === e) {
t.setAttribute("selected", ""), select_dropdown_b(i, t);
let e = t.getAttribute("data-value");
null !== e && (n.value = e, n.dispatchEvent(new Event("change", {
bubbles: !0
})), n.dispatchEvent(new Event("input", {
bubbles: !0
})));
} else t.removeAttribute("selected");
});
}
} else s.click();
break; break;
case "ArrowUp": case "ArrowUp":
if (e.preventDefault(), t) { e.preventDefault(), t ? n.length > 0 && x(n[(r - 1 + n.length) % n.length]) : u.click();
if (o.length > 0) { break;
let e = (l - 1 + o.length) % o.length; case "Home":
o.forEach((t, o)=>{ case "End":
if (o === e) { e.preventDefault(), t || u.click(), x("Home" === e.key ? n[0] || null : n[n.length - 1] || null);
t.setAttribute("selected", ""), select_dropdown_b(i, t);
let e = t.getAttribute("data-value");
null !== e && (n.value = e, n.dispatchEvent(new Event("change", {
bubbles: !0
})), n.dispatchEvent(new Event("input", {
bubbles: !0
})));
} else t.removeAttribute("selected");
});
}
} else s.click();
break; break;
case "Tab": case "Tab":
t && select_dropdown_m(s); t && select_dropdown_y(u);
break;
default:
if (1 === e.key.length && !e.ctrlKey && !e.altKey && !e.metaKey) {
v += e.key.toLocaleLowerCase(), window.clearTimeout(w), w = window.setTimeout(()=>{
v = "";
}, 700);
let r = n.find((e)=>e.text.trim().toLocaleLowerCase().startsWith(v));
r && (t || u.click(), x(r));
}
} }
}), n.addEventListener("change", ()=>{
let e = n.value, t = Array.from(i.querySelectorAll("li")), o = "";
t.forEach((t)=>{
t.getAttribute("data-value") === e ? (t.setAttribute("selected", ""), o = t.textContent || "") : t.removeAttribute("selected");
}), l.textContent = o;
}); });
let c = new MutationObserver(()=>{ let H = ()=>{
a(); S(), x(null);
};
n.addEventListener("change", H), n.addEventListener("input", H), u.addEventListener("focus", ()=>{
P(), M();
}); });
c.observe(n, { let N = new MutationObserver((e)=>{
childList: !0 e.some((e)=>"childList" === e.type || e.target !== n) ? M() : S(), e.some((e)=>"form" === e.attributeName || "id" === e.attributeName) && P();
}); });
let u = new MutationObserver((e)=>{ N.observe(n, {
e.forEach((e)=>{ childList: !0,
"disabled" === e.attributeName && (n.disabled ? (s.setAttribute("disabled", ""), s.removeAttribute("tabindex"), select_dropdown_m(s)) : (s.removeAttribute("disabled"), s.setAttribute("tabindex", "0"))); subtree: !0,
}); characterData: !0,
});
u.observe(n, {
attributes: !0, attributes: !0,
attributeFilter: [ attributeFilter: [
"disabled" "disabled",
"hidden",
"label",
"selected",
"value",
"required",
"aria-label",
"aria-labelledby",
"id",
"form"
] ]
}); }), P(), select_dropdown_s.set(n, {
let p = new MutationObserver((e)=>{ dropdown: u,
e.forEach((e)=>{ refreshAncestors: P,
e.removedNodes.forEach((e)=>{ dispose: ()=>{
(e === n || e.contains?.(n)) && (document.removeEventListener("click", d, !0), c.disconnect(), u.disconnect(), p.disconnect(), s.remove()); select_dropdown_y(u), m && cancelAnimationFrame(m), window.clearTimeout(f), window.clearTimeout(w), N.disconnect(), q.disconnect(), A?.removeEventListener("reset", C), g.forEach((e)=>{
}); e.removeEventListener("click", T);
}); }), n.removeEventListener("change", H), n.removeEventListener("input", H), u.remove(), n.removeAttribute("data-fluent-transformed"), i ? n.style.setProperty("display", i, o) : n.style.removeProperty("display"), select_dropdown_s.delete(n);
}); }
n.parentNode && p.observe(n.parentNode, {
childList: !0
}); });
} }
@@ -93,169 +93,81 @@ function jsxDEV(t, n) {
;// CONCATENATED MODULE: ./web/resources/utils/update.ts ;// CONCATENATED MODULE: ./web/resources/utils/update.ts
let update_t = L.rpc; const callGetVersion = L.rpc.declare({
const callGetVersion = update_t.declare({
object: "luci.fluent", object: "luci.fluent",
method: "get_version" method: "get_version"
}); });
const callStartDownload = update_t.declare({ const callStartUpdate = L.rpc.declare({
object: "luci.fluent", object: "luci.fluent",
method: "start_download", method: "start_update",
params: [ params: [
"url", "manifest"
"i18n_url",
"i18n_urls"
] ]
}); });
const callCheckDownload = update_t.declare({ const callUpdateStatus = L.rpc.declare({
object: "luci.fluent", object: "luci.fluent",
method: "check_download" method: "update_status",
});
const callDoInstall = update_t.declare({
object: "luci.fluent",
method: "do_install",
params: [ params: [
"hash", "job"
"i18n_hash",
"i18n_hashes"
] ]
}); });
class GitHubAPIError extends Error { class GitHubAPIError extends Error {
status; status;
constructor(t, e){ constructor(t, e){
super(t), this.name = "GitHubAPIError", this.status = e; super(t), this.status = e, this.name = "GitHubAPIError";
} }
} }
function matchI18nAsset(t, e, l) { function matchI18nAsset(t, e, a) {
let s = e.toLowerCase().trim(); let r = `luci-i18n-fluent-${e.toLowerCase().trim()}${"apk" === a ? "-" : "_"}`;
for (let e of t){ return t.find((t)=>t.name.toLowerCase().startsWith(r) && t.name.endsWith(`.${a}`)) ?? null;
let t = String(e.name || "").toLowerCase();
if ("apk" === l) {
if (t.startsWith(`luci-i18n-fluent-${s}-`) && t.endsWith(".apk")) return e;
} else if ((t.startsWith(`luci-i18n-fluent-${s}_`) || t.startsWith(`luci-i18n-fluent-${s}-`)) && t.endsWith(".ipk")) return e;
}
return null;
} }
async function fetchLatestRelease(t, e, l = [], s) { function updateManifest(t, e = "") {
let a = {}; return JSON.stringify({
s && (a.Authorization = `token ${s}`); packages: t.map((t)=>{
let r = await fetch("nightly" === t ? "https://api.github.com/repos/LazuliKao/luci-theme-fluent/releases/tags/nightly" : "https://api.github.com/repos/LazuliKao/luci-theme-fluent/releases/latest", { let a = t.digest ?? "";
headers: a if (!/^sha256:[a-f\d]{64}$/i.test(a)) throw Error(`Missing SHA-256 digest: ${t.name}`);
}), n = await r.json(); if (!t.browser_download_url.startsWith("https://")) throw Error(`Invalid package URL: ${t.name}`);
if (!r.ok) { return {
let t = n?.message ? `: ${n.message}` : ""; url: e + t.browser_download_url,
throw new GitHubAPIError(`GitHub API returned status ${r.status}${t}`, r.status); sha256: a.slice(7).toLowerCase()
} };
let i = n.assets || [], o = null, u = []; })
for (let t of i){ });
let l = String(t.name || ""); }
"apk" === e && /^luci-theme-fluent-[0-9].*\.apk$/.test(l) ? o = t : "ipk" === e && /^luci-theme-fluent_[^/]+\.ipk$/.test(l) && (o = t); async function fetchLatestRelease(t, e, a = [], r) {
} let s = await fetch(`https://api.github.com/repos/LazuliKao/luci-theme-fluent/releases/${"nightly" === t ? "tags/nightly" : "latest"}`, {
for (let t of l){ headers: r ? {
let l = matchI18nAsset(i, t, e); Authorization: `Bearer ${r}`
l && !u.includes(l) && u.push(l); } : {},
} signal: AbortSignal.timeout(30000)
}), n = await s.json();
if (!s.ok) throw new GitHubAPIError(`GitHub API returned status ${s.status}: ${n.message ?? ""}`, s.status);
let i = n.assets ?? [], o = "apk" === e ? /^luci-theme-fluent-[0-9].*\.apk$/ : /^luci-theme-fluent_[^/]+\.ipk$/, l = i.find((t)=>o.test(t.name)) ?? null, u = a.map((t)=>{
let a = matchI18nAsset(i, t, e);
if (!a) throw Error(`Missing translation package: ${t}`);
return a;
});
return { return {
tag_name: String(n.tag_name || ""), tag_name: String(n.tag_name ?? ""),
published_at: String(n.published_at || ""), published_at: String(n.published_at ?? ""),
body: String(n.body || ""), body: String(n.body ?? ""),
html_url: String(n.html_url || ""), package_asset: l,
package_asset: o,
i18n_assets: u i18n_assets: u
}; };
} }
;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/about.tsx ;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/about.tsx
let about_l = L.form, about_n = "https://ghfast.top/";
let about_u = about_l.DummyValue.extend({ let about_d = L.form.DummyValue.extend({
renderWidget: (l, u, p)=>{ renderWidget: ()=>{
let h = "1.0.1", b = "ipk", f = [], g = jsxs("div", { let d, c = jsx("span", {
class: "fluent-about-logo", children: "\u2026"
children: [ }), u = jsx("span", {
jsx("img", { children: "\u2026"
src: `${L.media()}/img/fluent.svg`, }), h = jsxs("select", {
alt: "Fluent Theme Logo" id: "fluent-update-channel",
}),
jsx("h2", {
children: "Fluent Theme"
}),
jsx("p", {
class: "fluent-about-subtitle",
children: _("Fluent Design 2 theme for LuCI")
})
]
}), y = jsxs("div", {
class: "fluent-about-details",
children: [
jsxs("div", {
class: "fluent-about-detail-row",
children: [
jsxs("strong", {
children: [
_("Author"),
":"
]
}),
jsx("span", {
children: "LazuliKao"
})
]
}),
jsxs("div", {
class: "fluent-about-detail-row",
children: [
jsxs("strong", {
children: [
_("Installed version"),
":"
]
}),
jsx("span", {
class: "fluent-about-current-version",
children: "v..."
})
]
}),
jsxs("div", {
class: "fluent-about-detail-row",
children: [
jsxs("strong", {
children: [
_("Package format"),
":"
]
}),
jsx("span", {
class: "fluent-about-pkg-type",
children: "..."
})
]
}),
jsxs("div", {
class: "fluent-about-detail-row",
children: [
jsxs("strong", {
children: [
_("Source code"),
":"
]
}),
jsx("span", {
children: jsx("a", {
href: "https://github.com/LazuliKao/luci-theme-fluent",
target: "_blank",
rel: "noreferrer",
children: "GitHub Repository"
})
})
]
})
]
}), m = jsxs("select", {
class: "cbi-input-select", class: "cbi-input-select",
id: "update-channel-select",
children: [ children: [
jsx("option", { jsx("option", {
value: "stable", value: "stable",
@@ -266,284 +178,238 @@ let about_u = about_l.DummyValue.extend({
children: _("Nightly Channel (Prerelease)") children: _("Nightly Channel (Prerelease)")
}) })
] ]
}), w = jsxs("select", { }), b = jsxs("select", {
id: "fluent-update-method",
class: "cbi-input-select", class: "cbi-input-select",
id: "update-method-select",
children: [ children: [
jsx("option", { jsx("option", {
value: "backend_official", value: "",
children: _("Backend (Official GitHub)") children: _("Backend (Official GitHub)")
}), }),
jsx("option", { jsx("option", {
value: "backend_ghproxy", value: "https://ghfast.top/",
children: _("Backend (GHProxy Acceleration)") children: _("Backend (GHProxy Acceleration)")
}) })
] ]
}), v = jsx("button", { }), p = jsx("button", {
class: "btn cbi-button cbi-button-action",
type: "button", type: "button",
class: "btn cbi-button-action",
children: _("Check for updates") children: _("Check for updates")
}), k = jsxs("div", { }), f = jsx("p", {
class: "fluent-update-controls",
style: "display: flex; gap: 15px; align-items: center; flex-wrap: wrap;",
children: [
jsxs("div", {
class: "fluent-update-channel-selector",
children: [
jsxs("label", {
htmlFor: "update-channel-select",
children: [
_("Update channel"),
": "
]
}),
m
]
}),
v
]
}), x = jsx("div", {
class: "fluent-update-status", class: "fluent-update-status",
style: "display: none" role: "status"
}), T = jsx("div", { }), g = jsx("div", {
class: "fluent-progress-bar__fill",
style: "width: 0%"
}), C = jsx("div", {
class: "fluent-progress-bar",
style: "display: none",
children: T
}), S = jsx("div", {
class: "fluent-progress-text",
style: "display: none"
}), $ = jsx("div", {
class: "fluent-update-card", class: "fluent-update-card",
style: "display: none" hidden: !0
}), F = jsxs("div", { }), m = jsx("pre", {
class: "fluent-update-changelog",
hidden: !0
}), w = jsx("button", {
type: "button",
class: "btn cbi-button-action",
hidden: !0,
onclick: ()=>window.location.reload(),
children: _("Reload Web Interface")
}), v = jsxs("div", {
class: "fluent-about-manager", class: "fluent-about-manager",
children: [ children: [
g, jsxs("div", {
y, class: "fluent-about-logo",
children: [
jsx("img", {
src: `${L.media()}/img/fluent.svg`,
alt: ""
}),
jsx("h2", {
children: "Fluent Theme"
}),
jsx("p", {
class: "fluent-about-subtitle",
children: _("Fluent Design 2 theme for LuCI")
})
]
}),
jsxs("div", {
class: "fluent-about-details",
children: [
jsxs("div", {
class: "fluent-about-detail-row",
children: [
jsxs("strong", {
children: [
_("Author"),
":"
]
}),
jsx("span", {
children: "LazuliKao"
})
]
}),
jsxs("div", {
class: "fluent-about-detail-row",
children: [
jsxs("strong", {
children: [
_("Installed version"),
":"
]
}),
c
]
}),
jsxs("div", {
class: "fluent-about-detail-row",
children: [
jsxs("strong", {
children: [
_("Package format"),
":"
]
}),
u
]
}),
jsxs("div", {
class: "fluent-about-detail-row",
children: [
jsxs("strong", {
children: [
_("Source code"),
":"
]
}),
jsx("a", {
href: "https://github.com/LazuliKao/luci-theme-fluent",
target: "_blank",
rel: "noreferrer",
children: "GitHub"
})
]
})
]
}),
jsx("hr", { jsx("hr", {
class: "fluent-about-divider" class: "fluent-about-divider"
}), }),
jsx("h3", { jsx("h3", {
children: _("Software Update") children: _("Software Update")
}), }),
k, jsxs("div", {
x, class: "fluent-update-controls",
$, children: [
C, jsx("label", {
S htmlFor: "fluent-update-channel",
children: _("Update channel")
}),
h,
jsx("label", {
htmlFor: "fluent-update-method",
children: _("Download method")
}),
b,
p
]
}),
f,
g,
m,
w
] ]
}), P = (e, t = "info")=>{ });
dom.content(x, [ function y(e, t = "info") {
document.createTextNode(e) f.textContent = e, f.className = `fluent-update-status status-${t}`;
]), x.className = `fluent-update-status status-${t}`, x.style.display = "block"; }
}, A = (e, t, a)=>{ function k(e) {
C.style.display = "block", S.style.display = "block", T.style.width = `${t}%`, T.className = `fluent-progress-bar__fill fill-${e}`, dom.content(S, [ h.disabled = b.disabled = p.disabled = e, g.querySelectorAll("button").forEach((t)=>{
document.createTextNode(`${a} (${t}%)`) t.disabled = e;
]); });
}, z = ()=>{ }
C.style.display = "none", S.style.display = "none"; async function C(e) {
}; let t = e, a = Date.now() + 1200000;
(async ()=>{ k(!0);
try { try {
let e = await callGetVersion(); for(; v.isConnected;){
h = e.version, b = e.pkg_type, f = e.installed_i18n || (e.i18n_zh_cn_installed ? [ if (0 !== t.result) throw Error(t.message || _("Unable to read update status."));
"zh-cn" if (m.hidden = !t.log, m.textContent !== t.log && (m.textContent = t.log ?? ""), "done" === t.stage) {
] : []); y(_("Theme successfully updated. Reload the web interface to use the new version."), "success"), w.hidden = !1;
let t = y.querySelector(".fluent-about-current-version"); return;
t && (t.textContent = `v${h}`); }
let a = y.querySelector(".fluent-about-pkg-type"); if ("error" === t.stage) return void y(_("Update failed. Check the installation log below."), "error");
a && (a.textContent = "apk" === b ? "APK (OpenWrt 25.12+)" : "IPK (OpenWrt 24.10)"); if ("downloading" !== t.stage && "installing" !== t.stage) throw Error(_("Unexpected update status."));
if (y("downloading" === t.stage ? _("Downloading and verifying packages...") : _("Installing packages...")), Date.now() >= a) throw Error(_("Timed out waiting for update status."));
if (await new Promise((e)=>setTimeout(e, 1000)), !v.isConnected) return;
if (!t.job) throw Error(_("Missing update job."));
t = await callUpdateStatus(t.job);
}
} catch (e) { } catch (e) {
console.error("Failed to fetch version", e), P(_("Failed to fetch current theme version."), "error"); y(`${_("Update status unavailable. Reload and sign in again to check the result.")} ${String(e)}`, "warn"), w.hidden = !1;
} finally{
k(!1);
} }
})(); }
let N = async (l)=>{ async function x(n) {
let s = m.value; k(!0), g.hidden = !0, y(_("Checking for updates..."));
try { try {
let c = await fetchLatestRelease(s, b, f, l); let a = await fetchLatestRelease(h.value, d.pkg_type, d.installed_i18n, n), i = a.package_asset;
console.log(c), v.disabled = !1; if (!i) throw Error(_("No matching theme package found in this release."));
let u = h.replace(/^v/, "").trim(), p = c.tag_name.replace(/^v/, "").trim(), g = L.naturalCompare(p, u) > 0, y = !g && "nightly" !== s; let o = d.version.replace(/^v/, ""), c = a.tag_name.replace(/^v/, ""), u = "stable" === h.value && 0 >= L.naturalCompare(c, o), p = jsx("button", {
if (y ? P(_("Your theme is up to date!"), "success") : P(g ? _("A new version is available!") : _("Nightly build available (reinstallation check)."), g ? "warn" : "info"), !c.package_asset) return void P(_("No matching package asset found for your system architecture in this release."), "error");
let k = c.body ? c.body : "", x = jsx("button", {
class: "btn cbi-button cbi-button-save",
type: "button", type: "button",
style: "white-space: nowrap;", class: "btn cbi-button-save",
children: _("Download and Install") children: _("Download and Install")
}), T = [ });
dom.content(g, [
jsxs("div", { jsxs("div", {
class: "fluent-update-header", class: "fluent-update-header",
children: [ children: [
jsx("span", { jsx("span", {
class: "fluent-update-badge", class: "fluent-update-badge",
children: "nightly" === p ? "Nightly" : `v${p}` children: a.tag_name
}), }),
jsx("span", { jsx("span", {
class: "fluent-update-date", children: a.published_at.split("T")[0]
children: c.published_at.split("T")[0]
}) })
] ]
}) }),
]; jsx("pre", {
k && T.push(jsx("pre", { class: "fluent-update-changelog",
class: "fluent-update-changelog", children: a.body
children: k }),
})), y || T.push(jsxs("div", { ...u ? [] : [
class: "fluent-update-footer", p
style: "display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 15px; flex-wrap: wrap;",
children: [
w,
x
] ]
})), dom.content($, T), $.style.display = "block", x.addEventListener("click", async ()=>{ ]), g.hidden = !1, y(u ? _("Your theme is up to date!") : _("A new version is available!"), u ? "success" : "info"), p.addEventListener("click", async ()=>{
let l = c.package_asset; k(!0), m.hidden = w.hidden = !0;
if (!l) return void P(_("No matching package asset found for your system architecture in this release."), "error");
x.disabled = !0, m.disabled = !0, w.disabled = !0, v.disabled = !0, P(_("Starting update process..."), "info");
let s = c.i18n_assets || [], d = null, u = [];
for (let e of (l.digest?.startsWith("sha256:") ? d = l.digest.replace("sha256:", "") : (console.warn("Unable to determine expected package hash from digest. Skipping verification."), d = "skip"), s))e.digest?.startsWith("sha256:") ? u.push(e.digest.replace("sha256:", "")) : u.push("skip");
let p = async ()=>{
let c = w.value.includes("ghproxy");
P(_("Starting backend download..."), "info"), A("download", 0, _("Downloading on router"));
let p = c ? about_n + l.browser_download_url : l.browser_download_url, h = s.map((e)=>c ? about_n + e.browser_download_url : e.browser_download_url).join(" "), b = await callStartDownload(p, h.split(" ")[0] || "", h);
if (0 !== b.result) throw Error(b.message || "Failed to start router download.");
for(;;){
let e = await callCheckDownload(), t = e.size || 0, a = s.reduce((e, t)=>e + (t.size || 0), 0), n = l.size + a, i = n > 0 ? Math.min(Math.round(t / n * 100), 100) : 0;
if (A("download", i, `${_("Downloading on router")} (${(t / 1024).toFixed(0)} / ${(n / 1024).toFixed(0)} KB)`), !e.running) if (0 !== e.code) throw Error("Router background download failed or file is empty.");
else break;
await new Promise((e)=>setTimeout(e, 1000));
}
if (!await new Promise((l)=>{
L.ui.showModal(_("Confirm Installation"), jsxs(Fragment, {
children: [
jsx("div", {
children: jsx("p", {
children: _("The theme package is ready. Are you sure you want to proceed with the installation?")
})
}),
jsxs("div", {
class: "right",
children: [
jsx("button", {
type: "button",
class: "btn",
onclick: ()=>{
L.ui.hideModal(), l(!1);
},
children: _("Cancel")
}),
jsx("button", {
type: "button",
class: "btn cbi-button-action",
onclick: ()=>{
L.ui.hideModal(), l(!0);
},
children: _("Continue")
})
]
})
]
}));
})) throw Error(_("Installation cancelled by user."));
P(_("Triggering installation on router..."), "info"), A("install", 100, _("Installing package"));
let f = await callDoInstall(d, u[0] || "", u.join(" "));
if (0 !== f.result) throw Error(f.message || "Router installation failed.");
let g = jsx("pre", {
style: "background: var(--fluent-code-bg, #1a1a1a); color: var(--fluent-text, #fff); padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-wrap: break-word; max-height: 200px; overflow-y: auto; margin-top: 15px;"
});
(async ()=>{
let e = L.env.ubuspath || "/ubus", t = L.env.sessionid;
for(;;)try {
let a = await fetch(e, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "call",
params: [
t,
"luci.fluent",
"get_install_log",
{}
]
})
}), l = await a.json();
if (l.error) break;
l.result?.[1]?.log && (g.textContent = l.result[1].log, g.scrollTop = g.scrollHeight), await new Promise((e)=>setTimeout(e, 1000));
} catch {
break;
}
})(), P(_("Theme successfully updated! Reloading RPC service, please refresh the page in 5 seconds."), "success"), A("done", 100, _("Finished")), dom.content($, [
jsxs("div", {
class: "fluent-update-success",
children: [
jsx("h3", {
style: "color: var(--fluent-success); text-align: center;",
children: _("Upgrade Successful!")
}),
jsx("p", {
style: "text-align: center; margin: 15px 0;",
children: _("The theme is being installed on your router. The logs are displayed below. Reloading the web interface will apply the changes once RPCD restarts.")
}),
g,
jsx("div", {
style: "display: flex; justify-content: center; margin-top: 20px;",
children: jsx("button", {
class: "btn cbi-button-action",
type: "button",
style: "margin-top: 15px",
onclick: ()=>{
window.location.reload();
},
children: _("Reload Web Interface")
})
})
]
})
]);
};
try { try {
await p(); let e = updateManifest([
i,
...a.i18n_assets
], b.value), t = await callStartUpdate(e);
if (0 !== t.result || !t.job) throw Error(t.message || _("Unable to start update."));
await C({
result: 0,
job: t.job,
stage: "downloading"
});
} catch (e) { } catch (e) {
console.error("Update failed", e), P(`${_("Update failed")}: ${e instanceof Error ? e.message : String(e)}`, "error"), x.disabled = !1, x.removeAttribute("disabled"), m.disabled = !1, m.removeAttribute("disabled"), w.disabled = !1, w.removeAttribute("disabled"), v.disabled = !1, v.removeAttribute("disabled"), z(); y(`${_("Unable to start update.")} ${String(e)}`, "error");
} finally{
k(!1);
} }
}); });
} catch (l) { } catch (n) {
if (v.disabled = !1, console.error("Failed checking updates", l), l instanceof GitHubAPIError && 403 === l.status) { if (y(String(n), "error"), n instanceof GitHubAPIError && (403 === n.status || 429 === n.status)) {
let n = jsx("input", { let n = jsx("input", {
type: "text", type: "password",
class: "cbi-input-text", class: "cbi-input-text",
style: "width: 100%", "aria-label": _("GitHub token"),
placeholder: "ghp_..." autocomplete: "off"
}), r = jsx("p", {
children: _("API rate limit exceeded. Please enter a GitHub Token to continue. This token is only used for frontend requests and will not be saved on the router backend.")
}), i = jsxs("p", {
style: "margin-top: 10px; font-size: 13px; color: var(--fluent-text-secondary);",
children: [
_("You can create a new token at"),
" ",
jsx("a", {
href: "https://github.com/settings/personal-access-tokens",
target: "_blank",
rel: "noopener noreferrer",
children: "https://github.com/settings/personal-access-tokens/new"
}),
".",
_("The token does NOT require any permissions/scopes to be granted (read-only public access is sufficient).")
]
}), s = jsx("pre", {
style: "margin-top: 10px; margin-bottom: 15px; font-size: 12px; white-space: pre-wrap; word-break: break-word; color: var(--fluent-error-text); background: var(--fluent-card-bg); padding: 8px; border-radius: var(--fluent-border-radius);",
children: l instanceof Error ? l.message : String(l)
}); });
L.ui.showModal(_("GitHub Token Required"), jsxs(Fragment, { L.ui.showModal(_("GitHub Token Required"), jsxs(Fragment, {
children: [ children: [
r, jsx("p", {
i, children: _("API rate limit exceeded. Please enter a GitHub Token to continue. This token is only used for frontend requests and will not be saved on the router backend.")
s, }),
n, n,
jsxs("div", { jsxs("div", {
class: "right", class: "right",
@@ -559,7 +425,7 @@ let about_u = about_l.DummyValue.extend({
class: "btn cbi-button-save", class: "btn cbi-button-save",
onclick: ()=>{ onclick: ()=>{
let e = n.value.trim(); let e = n.value.trim();
L.ui.hideModal(), e && (P(_("Retrying with token..."), "info"), v.disabled = !0, N(e)); L.ui.hideModal(), e && x(e);
}, },
children: _("Submit") children: _("Submit")
}) })
@@ -567,16 +433,31 @@ let about_u = about_l.DummyValue.extend({
}) })
] ]
})); }));
} else P(`${_("Failed to check for updates")}: ${l instanceof Error ? l.message : String(l)}`, "error"); }
} finally{
k(!1);
} }
}; }
return v.addEventListener("click", ()=>{ return k(!0), (async ()=>{
P(_("Checking for updates..."), "info"), v.disabled = !0, $.style.display = "none", z(), N(); try {
}), F; if (!(d = await callGetVersion()).version || ![
"ipk",
"apk"
].includes(d.pkg_type)) throw Error(_("Unable to determine installed theme version."));
c.textContent = d.version, u.textContent = d.pkg_type.toUpperCase(), p.addEventListener("click", ()=>{
x();
});
let e = await callUpdateStatus("");
if (0 !== e.result) throw Error(e.message);
"idle" !== e.stage && await C(e), k(!1);
} catch (e) {
y(String(e), "error");
}
})(), v;
} }
}); });
const registerAboutTab = (e)=>{ const registerAboutTab = (e)=>{
e.tab("about", _("About")), e.taboption("about", about_u, "_about_mgr"); e.tab("about", _("About")), e.taboption("about", about_d, "_about_mgr");
}; };
;// CONCATENATED MODULE: ./web/resources/fluent-defaults.ts ;// CONCATENATED MODULE: ./web/resources/fluent-defaults.ts
+125
View File
@@ -0,0 +1,125 @@
#!/bin/sh
# The worker is copied into its job directory before launch: upgrading this
# package must not replace the script while the shell is still reading it.
if [ "$1" = worker ]; then
job="$2"
stage() { printf '%s\n' "$1" > "$job/state.tmp" && mv "$job/state.tmp" "$job/state"; }
finish() {
code=$?
trap - EXIT
if [ "$code" -ne 0 ]; then
printf 'Update failed (exit %s).\n' "$code"
stage error
fi
rm -f "$job"/*.ipk "$job"/*.apk
exit "$code"
}
trap finish EXIT
trap 'exit 1' HUP INT TERM
set -e
stage downloading
set --
i=0
while IFS="$(printf '\t')" read -r hash url; do
i=$((i + 1))
file="$job/$i.$(cat "$job/format")"
printf 'Downloading package %s...\n' "$i"
if command -v uclient-fetch >/dev/null 2>&1; then
uclient-fetch -T 60 -O "$file" "$url"
else
wget -T 60 -O "$file" "$url"
fi
printf '%s %s\n' "$hash" "$file" | sha256sum -c -
set -- "$@" "$file"
done < "$job/packages"
[ "$#" -gt 0 ]
stage installing
if [ "$(cat "$job/format")" = apk ]; then
apk add --allow-untrusted "$@"
else
opkg install "$@"
fi
stage done
exit 0
fi
. /usr/share/libubox/jshn.sh
umask 077
work=/tmp/fluent-updater
error() {
json_init
json_add_int result 1
json_add_string message "$1"
json_dump
exit 0
}
read -r input
json_load "$input" || error 'Invalid request'
case "$1" in
start_update)
json_get_var manifest manifest
json_load "$manifest" || error 'Invalid package manifest'
json_select packages || error 'Missing packages'
json_get_keys keys
[ -n "$keys" ] || error 'Missing packages'
mkdir -p "$work" || error 'Unable to create update directory'
exec 9>"$work/lock"
flock -n 9 || error 'An update is already running'
job=$(mktemp -d "$work/job.XXXXXX") || error 'Unable to create update job'
trap 'rm -rf "$job"' EXIT
count=0
for key in $keys; do
count=$((count + 1))
[ "$count" -le 32 ] || error 'Too many packages'
json_select "$key" || error 'Invalid package'
json_get_var url url
json_get_var hash sha256
case "$url" in https://?*) ;; *) error 'Package URLs must use HTTPS' ;; esac
case "$url" in *[[:space:]]*) error 'Invalid package URL' ;; esac
case "$hash" in ''|*[!a-fA-F0-9]*) error 'Invalid SHA-256 digest' ;; esac
[ "${#hash}" -eq 64 ] || error 'Missing SHA-256 digest'
printf '%s\t%s\n' "$hash" "$url" >> "$job/packages" || error 'Unable to write package manifest'
json_select ..
done
if command -v apk >/dev/null 2>&1; then echo apk; else echo ipk; fi > "$job/format" || error 'Unable to write package format'
cp "$0" "$job/worker" || error 'Unable to prepare update worker'
echo downloading > "$job/state" || error 'Unable to write update state'
# Keep only the latest job, under the same lock as the worker.
for old in "$work"/job.*; do
[ "$old" = "$job" ] || rm -rf "$old"
done
printf '%s\n' "${job##*/}" > "$work/latest.tmp" && mv "$work/latest.tmp" "$work/latest" || error 'Unable to publish update job'
nohup setsid /bin/sh "$job/worker" worker "$job" > "$job/log" 2>&1 < /dev/null &
echo "$!" > "$job/pid"
trap - EXIT
json_init
json_add_int result 0
json_add_string job "${job##*/}"
json_dump
;;
update_status)
json_get_var id job
[ -n "$id" ] || id=$(cat "$work/latest" 2>/dev/null)
case "$id" in job.*) ;; '') echo '{"result":0,"stage":"idle"}'; exit 0 ;; *) error 'Invalid job' ;; esac
case "$id" in *[!a-zA-Z0-9.]*) error 'Invalid job' ;; esac
job="$work/$id"
[ -f "$job/state" ] || error 'Update job no longer available'
state=$(cat "$job/state")
case "$state" in
downloading|installing)
pid=$(cat "$job/pid" 2>/dev/null)
[ -z "$pid" ] || kill -0 "$pid" 2>/dev/null || state=error
;;
esac
json_init
json_add_int result 0
json_add_string job "$id"
json_add_string stage "$state"
json_add_string log "$(tail -n 100 "$job/log" 2>/dev/null)"
json_dump
;;
esac
@@ -1,274 +1,86 @@
#!/bin/sh #!/bin/sh
. /lib/functions.sh
. /usr/share/libubox/jshn.sh . /usr/share/libubox/jshn.sh
if command -v apk >/dev/null 2>&1; then
PKG_EXT=".apk"
else
PKG_EXT=".ipk"
fi
readonly bg_path="/www/luci-static/fluent/background" readonly bg_path="/www/luci-static/fluent/background"
readonly tmp_path="/tmp/fluent_background.tmp" readonly tmp_path="/tmp/fluent_background.tmp"
readonly update_pkg="/tmp/fluent-update${PKG_EXT}"
readonly i18n_pkg="/tmp/fluent-i18n-update${PKG_EXT}"
case "$1" in case "$1" in
"list") list)
json_init
json_add_object "avail"
json_close_object
json_add_object "remove"
json_add_string "filename" "filename"
json_close_object
json_add_object "rename"
json_add_string "newname" "filename"
json_close_object
json_add_object "set_mode"
json_add_string "mode" "mode"
json_close_object
json_add_object "get_version"
json_close_object
json_add_object "start_download"
json_add_string "url" "url"
json_add_string "i18n_url" "i18n_url"
json_add_string "i18n_urls" "i18n_urls"
json_close_object
json_add_object "check_download"
json_close_object
json_add_object "do_install"
json_add_string "hash" "hash"
json_add_string "i18n_hash" "i18n_hash"
json_add_string "i18n_hashes" "i18n_hashes"
json_close_object
json_add_object "get_install_log"
json_close_object
json_dump
json_cleanup
;;
"call")
case "$2" in
"avail")
json_init json_init
json_add_int "avail" "$(df | grep -E '/$' | awk '{print $4}')" for method in avail get_version; do
json_add_object "$method"
json_close_object
done
for pair in remove:filename rename:newname set_mode:mode start_update:manifest update_status:job; do
json_add_object "${pair%:*}"
json_add_string "${pair#*:}" ""
json_close_object
done
json_dump json_dump
json_cleanup
;; ;;
"remove") call)
read -r input case "$2" in
json_load "$input" start_update|update_status)
json_get_var filename "filename" exec /bin/sh /usr/libexec/fluent/update "$2"
json_cleanup ;;
if dirname "$filename" | grep -q ".."; then avail)
echo '{ "result": 255 }' json_init
exit 255 json_add_int avail "$(df | grep -E '/$' | awk '{print $4}')"
fi json_dump
rm -f "$bg_path/$filename" ;;
echo '{ "result": 0 }' remove|rename)
;; read -r input
"rename") json_load "$input"
read -r input if [ "$2" = remove ]; then
json_load "$input" json_get_var name filename
json_get_var newname "newname" else
json_cleanup json_get_var name newname
if dirname "$newname" | grep -q ".."; then fi
echo '{ "result": 255 }' case "$name" in
exit 255 ''|.|..|*/*) echo '{"result":255}'; exit 0 ;;
fi esac
mkdir -p "$bg_path" if [ "$2" = remove ]; then
if mv "$tmp_path" "$bg_path/$newname" 2>"/dev/null"; then rm -f "$bg_path/$name"
chmod 0644 "$bg_path/$newname" result=$?
echo '{ "result": 0 }' else
else mkdir -p "$bg_path"
echo '{ "result": 1 }' mv "$tmp_path" "$bg_path/$name" && chmod 0644 "$bg_path/$name"
fi result=$?
;; fi
"set_mode") printf '{"result":%s}\n' "$result"
read -r input ;;
json_load "$input" set_mode)
json_get_var mode "mode" read -r input
json_cleanup json_load "$input"
case "$mode" in json_get_var mode mode
light|dark|auto) ;; case "$mode" in
*) light|dark|auto) ;;
echo '{ "result": 255 }' *) echo '{"result":255}'; exit 0 ;;
exit 255 esac
uci -q set fluent.@global[0].mode="$mode" && uci -q commit fluent
printf '{"result":%s}\n' "$?"
;;
get_version)
if command -v apk >/dev/null 2>&1; then
pkg_type=apk
version=$(apk list -I luci-theme-fluent 2>/dev/null | awk '{print $1}' | sed 's/^luci-theme-fluent-//' | head -1)
installed_i18n=$(apk info 2>/dev/null | sed -n 's/^luci-i18n-fluent-//p')
else
pkg_type=ipk
version=$(opkg status luci-theme-fluent 2>/dev/null | sed -n 's/^Version: //p')
installed_i18n=$(opkg list-installed 2>/dev/null | awk '/^luci-i18n-fluent-/ { sub(/^luci-i18n-fluent-/, "", $1); print $1 }')
fi
json_init
json_add_string version "$version"
json_add_string pkg_type "$pkg_type"
json_add_array installed_i18n
for lang in $installed_i18n; do
json_add_string "" "$lang"
done
json_close_array
json_dump
;; ;;
esac esac
if uci -q set fluent.@global[0].mode="$mode" && uci -q commit fluent; then
echo '{ "result": 0 }'
else
echo '{ "result": 1 }'
fi
;; ;;
"get_version")
version=""
pkg_type="ipk"
i18n_installed=0
installed_i18n=""
if command -v apk >/dev/null 2>&1; then
pkg_type="apk"
version=$(apk list -I luci-theme-fluent 2>/dev/null | awk '{print $1}' | sed 's/^luci-theme-fluent-//' | head -1)
installed_i18n=$(apk list -I 2>/dev/null | grep -E '^luci-i18n-fluent-' | awk '{print $1}' | sed 's/^luci-i18n-fluent-//')
else
version=$(opkg status luci-theme-fluent 2>/dev/null | grep -m1 'Version:' | awk '{print $2}')
installed_i18n=$(opkg list-installed 2>/dev/null | grep -E '^luci-i18n-fluent-' | awk '{print $1}' | sed 's/^luci-i18n-fluent-//')
fi
if echo "$installed_i18n" | grep -q "^zh-cn$"; then
i18n_installed=1
fi
if [ -z "$version" ]; then
version="1.0.1-dev"
fi
json_init
json_add_string "version" "$version"
json_add_string "pkg_type" "$pkg_type"
json_add_boolean "i18n_zh_cn_installed" "$i18n_installed"
json_add_array "installed_i18n"
for lang in $installed_i18n; do
json_add_string "" "$lang"
done
json_close_array
json_dump
json_cleanup
;;
"start_download")
read -r input
json_load "$input"
json_get_var url "url"
json_get_var i18n_url "i18n_url"
json_get_var i18n_urls "i18n_urls"
json_cleanup
rm -f "$update_pkg"
rm -f /tmp/fluent-i18n-*
rm -f /tmp/fluent-update.log
if [ -n "$url" ]; then
download_cmd=""
if command -v uclient-fetch >/dev/null 2>&1; then
download_cmd="uclient-fetch -O $update_pkg \"$url\""
urls_to_dl="${i18n_urls:-$i18n_url}"
idx=1
for u in $urls_to_dl; do
download_cmd="$download_cmd && uclient-fetch -O /tmp/fluent-i18n-${idx}${PKG_EXT} \"$u\""
idx=$((idx + 1))
done
else
download_cmd="wget -O $update_pkg \"$url\""
urls_to_dl="${i18n_urls:-$i18n_url}"
idx=1
for u in $urls_to_dl; do
download_cmd="$download_cmd && wget -O /tmp/fluent-i18n-${idx}${PKG_EXT} \"$u\""
idx=$((idx + 1))
done
fi
nohup sh -c "$download_cmd >/tmp/fluent-update.log 2>&1" >/dev/null 2>&1 &
echo '{ "result": 0 }'
else
echo '{ "result": 255, "message": "Missing URL" }'
fi
;;
"check_download")
json_init
size1=$(ls -l $update_pkg 2>/dev/null | awk '{print $5}')
size2=$(ls -l /tmp/fluent-i18n-* 2>/dev/null | awk '{sum+=$5} END {print sum}')
size=$(( ${size1:-0} + ${size2:-0} ))
json_add_int "size" "$size"
if pgrep -f "fluent-update" >/dev/null; then
json_add_boolean "running" 1
else
json_add_boolean "running" 0
if [ "$size" -eq 0 ]; then
# If process exited and size is 0, download failed
json_add_int "code" 255
else
json_add_int "code" 0
fi
fi
json_dump
json_cleanup
;;
"do_install")
read -r input
json_load "$input"
json_get_var hash "hash"
json_get_var i18n_hash "i18n_hash"
json_get_var i18n_hashes "i18n_hashes"
json_cleanup
if [ -z "$hash" ]; then
echo '{ "result": 255, "message": "Missing hash parameter" }'
exit 255
fi
if [ ! -f "$update_pkg" ]; then
echo '{ "result": 255, "message": "Package file not found" }'
exit 255
fi
if [ "$hash" != "skip" ]; then
if ! echo "$hash" | grep -qE '^[0-9a-fA-F]{64}$'; then
echo '{ "result": 255, "message": "Invalid hash parameter" }'
exit 255
fi
actual_hash=$(sha256sum "$update_pkg" | awk '{print $1}')
if [ "$actual_hash" != "$hash" ]; then
echo '{ "result": 255, "message": "Hash verification failed" }'
exit 255
fi
fi
hashes_to_check="${i18n_hashes:-$i18n_hash}"
if [ -n "$hashes_to_check" ]; then
idx=1
for h in $hashes_to_check; do
target_i18n_pkg="/tmp/fluent-i18n-${idx}${PKG_EXT}"
if [ -f "$target_i18n_pkg" ] && [ "$h" != "skip" ]; then
if ! echo "$h" | grep -qE '^[0-9a-fA-F]{64}$'; then
echo '{ "result": 255, "message": "Invalid i18n hash parameter" }'
exit 255
fi
actual_i18n_hash=$(sha256sum "$target_i18n_pkg" | awk '{print $1}')
if [ "$actual_i18n_hash" != "$h" ]; then
echo '{ "result": 255, "message": "i18n Hash verification failed" }'
exit 255
fi
fi
idx=$((idx + 1))
done
fi
install_cmd=""
pkgs="$update_pkg"
i18n_files=$(ls /tmp/fluent-i18n-* 2>/dev/null)
if [ -n "$i18n_files" ]; then
pkgs="$pkgs $i18n_files"
fi
if command -v apk >/dev/null 2>&1; then
install_cmd="apk add --allow-untrusted $pkgs"
else
install_cmd="opkg install $pkgs"
fi
nohup sh -c "sleep 1 && $install_cmd > /tmp/fluent-install.log 2>&1 ; rm -f $pkgs ; /etc/init.d/rpcd restart" >/dev/null 2>&1 &
echo '{ "result": 0 }'
;;
"get_install_log")
json_init
if [ -f "/tmp/fluent-install.log" ]; then
json_add_string "log" "$(cat /tmp/fluent-install.log)"
else
json_add_string "log" ""
fi
json_dump
json_cleanup
;;
esac
;;
esac esac
@@ -6,18 +6,16 @@
"/www/luci-static/fluent/background/*": [ "list" ] "/www/luci-static/fluent/background/*": [ "list" ]
}, },
"ubus": { "ubus": {
"luci.fluent": [ "avail", "remove", "rename", "set_mode", "get_version", "check_download", "get_install_log" ] "luci.fluent": [ "avail", "get_version", "update_status" ]
}, },
"uci": [ "fluent" ] "uci": [ "fluent" ]
}, },
"write": { "write": {
"file": { "file": {
"/tmp/fluent_background.tmp": [ "write" ], "/tmp/fluent_background.tmp": [ "write" ]
"/tmp/fluent-update.ipk": [ "write" ],
"/tmp/fluent-update.apk": [ "write" ]
}, },
"ubus": { "ubus": {
"luci.fluent": [ "set_mode", "do_install", "start_download" ] "luci.fluent": [ "remove", "rename", "set_mode", "start_update" ]
}, },
"uci": [ "fluent" ] "uci": [ "fluent" ]
} }
+2 -2
View File
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-vantage PKG_NAME:=luci-theme-vantage
PKG_VERSION:=1.0.16 PKG_VERSION:=1.0.17
PKG_RELEASE:=35 PKG_RELEASE:=36
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage theme LUCI_TITLE:=Vantage theme
@@ -40,10 +40,20 @@ const BACKDROP_EVENT = 'vantage:backdrop';
save (those keys are dropped then); this browser's last pick (the old save (those keys are dropped then); this browser's last pick (the old
key, also what the dashboard reads under another theme) counts until key, also what the dashboard reads under another theme) counts until
the router has one. */ the router has one. */
/* this package's version, as its Makefile has it (tests/version.test.js) */
const VERSION = '1.0.17';
/* Check for updates (Appearance): when asked, never on its own, the browser
reads the project's 24.10 package list on GitHub Pages (the same release
as 25.12's), without cookies or a referrer. Only a version number passes;
the link is a constant, to the latest release. */
const UPDATE_INDEX = 'https://anothadev.github.io/luci-theme-vantage/24.10/Packages';
const LOOK = { const LOOK = {
theme: { own: 'vantage.theme.own', before: THEME_KEY, ok: [ 'auto', 'light', 'dark' ] }, theme: { own: 'vantage.theme.own', before: THEME_KEY, ok: [ 'auto', 'light', 'dark' ] },
login: { own: 'vantage.login.own', before: LOGIN_KEY, ok: [ 'grid', 'stars', 'none' ] }, login: { own: 'vantage.login.own', before: LOGIN_KEY, ok: [ 'grid', 'stars', 'none' ] },
backdrop: { own: 'vantage.backdrop.own', before: BACKDROP_KEY, ok: [ 'grid', 'stars', 'none' ] } backdrop: { own: 'vantage.backdrop.own', before: BACKDROP_KEY, ok: [ 'grid', 'stars', 'none' ] },
/* what shows next to the logo; the hostname and model come from system board */
brand: { own: 'vantage.brand.own', before: 'vantage.brand', ok: [ 'none', 'hostname', 'model' ] }
}; };
const LOOK_SYNC = 'vantage.look'; /* another tab saved one: { name, value } */ const LOOK_SYNC = 'vantage.look'; /* another tab saved one: { name, value } */
const LOOK_SET_EVENT = 'vantage:backdrop-set'; /* the dashboard's own picker hands its pick over */ const LOOK_SET_EVENT = 'vantage:backdrop-set'; /* the dashboard's own picker hands its pick over */
@@ -154,6 +164,23 @@ function svg(name, size) {
function storageGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } } function storageGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
/* '1.0.16' or '1.0.16-r1' -> [ 1, 0, 16 ]; null for anything else */
function versionParts(v) {
const m = /^(\d{1,4}(?:\.\d{1,4}){1,3})(?:-r\d{1,4})?$/.exec(String(v == null ? '' : v));
return m ? m[1].split('.').map(Number) : null;
}
function newerVersion(a, b) {
for (let i = 0; i < Math.max(a.length, b.length); i++)
if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0);
return false;
}
/* the theme's version in an opkg package list, without its release */
function listedVersion(text) {
const block = String(text || '').split(/\n\s*\n/).find(b => /^Package: luci-theme-vantage$/m.test(b));
const m = block && /^Version: (\S+)$/m.exec(block);
return m && versionParts(m[1]) ? m[1].replace(/-r\d+$/, '') : null;
}
/* the value in force, from [ pending, own, router, before ]: the first /* the value in force, from [ pending, own, router, before ]: the first
the theme knows, else the first choice (the default); the templates' the theme knows, else the first choice (the default); the templates'
inline scripts and login.js do the same without the pending one */ inline scripts and login.js do the same without the pending one */
@@ -176,6 +203,7 @@ function setLookAttr(root, name, value) {
if (name === 'theme') root.setAttribute('data-vt-look-theme', value); if (name === 'theme') root.setAttribute('data-vt-look-theme', value);
else if (name === 'login') root.setAttribute('data-vt-look-login', value); else if (name === 'login') root.setAttribute('data-vt-look-login', value);
else if (name === 'backdrop') root.setAttribute('data-vt-look-backdrop', value); else if (name === 'backdrop') root.setAttribute('data-vt-look-backdrop', value);
else if (name === 'brand') root.setAttribute('data-vt-look-brand', value);
} }
function storageSet(k, v) { try { if (v == null) localStorage.removeItem(k); else localStorage.setItem(k, v); } catch (e) {} } function storageSet(k, v) { try { if (v == null) localStorage.removeItem(k); else localStorage.setItem(k, v); } catch (e) {} }
@@ -452,20 +480,33 @@ function replaceTable(table, block) {
} }
/* Memory: one bar. in use = total - available; cache (reclaimable) = /* Memory: one bar. in use = total - available; cache (reclaimable) =
available - free; free = total - LuCI's "Used" (which is total - free). */ available - free; free = total - LuCI's "Used" (which is total - free).
A table without "Total Available" (Kwrt's luci-mod-status, #103) is read
its own way, below. */
function enhanceMemory() { function enhanceMemory() {
statusSections(_('Memory')).forEach(section => { statusSections(_('Memory')).forEach(section => {
const table = pendingTable(section); const table = pendingTable(section);
if (!table) return; if (!table) return;
const rows = barRows(table), get = l => (rows.find(r => r.label === l) || {}).pair; const rows = barRows(table), get = l => (rows.find(r => r.label === l) || {}).pair;
const avail = get(_('Total Available')), used = get(_('Used')), const avail = get(_('Total Available')), used = get(_('Used')),
buffered = get(_('Buffered')), cached = get(_('Cached')), swap = get(_('Swap free')); buffered = get(_('Buffered')), cached = get(_('Cached')),
swapFree = get(_('Swap free')), swapUsed = get(_('Swap used'));
const total = (used || avail || {}).total; const total = (used || avail || {}).total;
if (!total || !used) return; if (!total || !used) return;
const free = Math.max(0, total - used.value); let free, available;
let available = avail ? avail.value : free + (buffered ? buffered.value : 0) + (cached ? cached.value : 0); /* the row decides, not its value: LuCI shows "?" when it has none */
available = Math.min(total, Math.max(available, free)); if (rows.some(r => r.label === _('Total Available'))) {
free = Math.max(0, total - used.value);
available = avail ? avail.value : free + (buffered ? buffered.value : 0) + (cached ? cached.value : 0);
available = Math.min(total, Math.max(available, free));
}
else {
/* Kwrt: "Used" is total - MemAvailable. MemFree isn't shown, so the
cache stands for what is reclaimable and Free is an estimate. */
available = Math.min(total, Math.max(0, total - used.value));
free = Math.max(0, available - (cached ? cached.value : 0));
}
const inuse = total - available, reclaim = available - free; const inuse = total - available, reclaim = available - free;
const usedPct = pctOf(inuse, total); const usedPct = pctOf(inuse, total);
@@ -489,14 +530,16 @@ function enhanceMemory() {
]) ])
]; ];
/* "Swap free" in LuCI's table, "Swap used" in Kwrt's */
const swap = swapFree ? { used: Math.max(0, swapFree.total - swapFree.value), total: swapFree.total }
: swapUsed ? { used: swapUsed.value, total: swapUsed.total } : null;
if (swap) { if (swap) {
const swapUsed = swap.total - swap.value;
children.push(E('div', { 'class': 'v-meter-rows v-meter-swap' }, E('div', { 'class': 'v-meter-row' }, [ children.push(E('div', { 'class': 'v-meter-rows v-meter-swap' }, E('div', { 'class': 'v-meter-row' }, [
E('span', { 'class': 'v-meter-label' }, [ _('Swap', 'vantage-theme') ]), E('span', { 'class': 'v-meter-label' }, [ _('Swap', 'vantage-theme') ]),
E('div', { 'class': 'v-meter-bar is-thin', 'role': 'img', E('div', { 'class': 'v-meter-bar is-thin', 'role': 'img',
'aria-label': '%s: %s / %s'.format(_('Swap', 'vantage-theme'), fmtBytes(swapUsed), fmtBytes(swap.total)) }, 'aria-label': '%s: %s / %s'.format(_('Swap', 'vantage-theme'), fmtBytes(swap.used), fmtBytes(swap.total)) },
segment('v-seg-used', swapUsed, swap.total)), segment('v-seg-used', swap.used, swap.total)),
E('span', { 'class': 'v-meter-val' }, [ E('b', {}, [ fmtBytes(swapUsed) ]), ' / ' + fmtBytes(swap.total) ]) E('span', { 'class': 'v-meter-val' }, [ E('b', {}, [ fmtBytes(swap.used) ]), ' / ' + fmtBytes(swap.total) ])
]))); ])));
} }
@@ -1549,6 +1592,29 @@ return baseclass.extend({
this.syncAppearance(); this.syncAppearance();
}, },
/* next to the logo: nothing unless set. In the rail when it is pinned
open, else first in the top bar (vantage.css); text only */
brandPref() {
return this.lookPref('brand');
},
setBrandPref(pref) {
this.setLook('brand', pref, () => this.applyBrand());
},
applyBrand() {
const v = this.brandPref(), b = this.board;
const name = !b ? '' : v === 'hostname' ? (host.chipNames(b.hostname, '')[0] || '') : v === 'model' ? host.productName(b) : '';
[ 'vt-brand-name', 'vt-top-name' ].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
if (el.textContent !== name) el.textContent = name;
el.title = name;
el.hidden = !name;
});
this.syncAppearance();
},
/* the login page's backdrop (login.js): grid packets unless set */ /* the login page's backdrop (login.js): grid packets unless set */
loginPref() { loginPref() {
return this.lookPref('login'); return this.lookPref('login');
@@ -1620,6 +1686,7 @@ return baseclass.extend({
} }
this.applyTheme(); this.applyTheme();
this.applyBackdrop(); this.applyBackdrop();
this.applyBrand();
}); });
/* the dashboard's own picker (Customize), saved as Appearance's */ /* the dashboard's own picker (Customize), saved as Appearance's */
window.addEventListener(LOOK_SET_EVENT, ev => { window.addEventListener(LOOK_SET_EVENT, ev => {
@@ -1641,6 +1708,7 @@ return baseclass.extend({
}); });
this.applyTheme(); this.applyTheme();
this.applyBackdrop(); this.applyBackdrop();
this.applyBrand();
}, },
/* Appearance, under the colour-theme button: the colour theme, the /* Appearance, under the colour-theme button: the colour theme, the
@@ -1651,7 +1719,7 @@ return baseclass.extend({
scrolling the page close it. */ scrolling the page close it. */
/* one of Appearance's radio groups */ /* one of Appearance's radio groups */
appearGroup(name, legend, opts, set) { appearGroup(name, legend, opts, set) {
const look = { 'vt-colour': 'theme', 'vt-login': 'login', 'vt-backdrop': 'backdrop' }[name]; const look = { 'vt-colour': 'theme', 'vt-logo-name': 'brand', 'vt-login': 'login', 'vt-backdrop': 'backdrop' }[name];
return E('fieldset', { 'class': 'vt-appear-set' }, [ return E('fieldset', { 'class': 'vt-appear-set' }, [
E('legend', {}, [ legend ]), E('legend', {}, [ legend ]),
E('div', { 'class': 'vt-appear-track' }, opts.map(o => { E('div', { 'class': 'vt-appear-track' }, opts.map(o => {
@@ -1678,16 +1746,22 @@ return baseclass.extend({
group('vt-colour', _('Colour theme'), [ group('vt-colour', _('Colour theme'), [
[ 'auto', _('Auto', 'vantage-theme'), 'auto' ], [ 'light', _('Light', 'vantage-theme'), 'sun' ], [ 'dark', _('Dark', 'vantage-theme'), 'moon' ] [ 'auto', _('Auto', 'vantage-theme'), 'auto' ], [ 'light', _('Light', 'vantage-theme'), 'sun' ], [ 'dark', _('Dark', 'vantage-theme'), 'moon' ]
], v => this.setTheme(v)), ], v => this.setTheme(v)),
group('vt-logo-name', _('Next to the logo'), [
[ 'none', _('Logo only') ], [ 'hostname', _('Hostname') ], [ 'model', _('Model') ]
], v => this.setBrandPref(v)),
group('vt-login', _('Login background'), [ [ 'grid', login.grid ], [ 'stars', login.stars ], [ 'none', login.none ] ], v => this.setLoginPref(v)) group('vt-login', _('Login background'), [ [ 'grid', login.grid ], [ 'stars', login.stars ], [ 'none', login.none ] ], v => this.setLoginPref(v))
].concat(this.hasDashboard() ? [ this.backdropGroup() ] : [], [ ].concat(this.hasDashboard() ? [ this.backdropGroup() ] : [], [
/* where the picks are kept, when not for every browser (syncAppearance) */ /* where the picks are kept, when not for every browser (syncAppearance) */
(this.appearNote = E('p', { 'class': 'vt-appear-note', 'role': 'status', 'hidden': 'hidden' })) (this.appearNote = E('p', { 'class': 'vt-appear-note', 'role': 'status', 'hidden': 'hidden' })),
this.versionEl()
])); ]));
box.addEventListener('keydown', ev => { box.addEventListener('keydown', ev => {
if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); this.closeAppearance(true); } if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); this.closeAppearance(true); }
else if (ev.key === 'Tab') { else if (ev.key === 'Tab') {
const sets = box.querySelectorAll('fieldset'); const sets = box.querySelectorAll('fieldset');
const edge = ev.shiftKey ? sets[0] : sets[sets.length - 1]; /* forward, the panel ends at the update check's link when it has one */
const last = Array.prototype.slice.call(box.querySelectorAll('.vt-appear-ver button, .vt-appear-ver a[href]')).pop();
const edge = ev.shiftKey ? sets[0] : (last || sets[sets.length - 1]);
if (edge.contains(ev.target)) { ev.preventDefault(); this.closeAppearance(true); } if (edge.contains(ev.target)) { ev.preventDefault(); this.closeAppearance(true); }
} }
}); });
@@ -1704,9 +1778,51 @@ return baseclass.extend({
return (this.appear = box); return (this.appear = box);
}, },
/* "Vantage 1.0.16 · Check for updates", and what the check found */
versionEl() {
const btn = E('button', { 'type': 'button', 'class': 'vt-appear-check' }, [ _('Check for updates') ]);
const out = E('span', { 'class': 'vt-appear-update', 'role': 'status', 'hidden': 'hidden' });
btn.addEventListener('click', () => this.checkUpdates(btn, out));
return E('p', { 'class': 'vt-appear-ver' }, [ E('span', { 'class': 'vt-appear-num' }, [ 'Vantage ' + VERSION ]), ' · ', btn, out ]);
},
releaseLink(label) {
return E('a', { 'href': 'https://github.com/anothaDev/luci-theme-vantage/releases/latest', 'target': '_blank', 'rel': 'noopener noreferrer' }, [ label ]);
},
checkUpdates(btn, out) {
/* aria-disabled, not disabled: a disabled button drops the focus,
and with it the panel's keys */
if (btn.getAttribute('aria-disabled') === 'true') return;
btn.setAttribute('aria-disabled', 'true');
out.textContent = _('Checking…');
out.hidden = false;
const ctl = window.AbortController ? new AbortController() : null;
const timer = ctl ? setTimeout(() => ctl.abort(), 10000) : 0;
fetch(UPDATE_INDEX, { cache: 'no-store', credentials: 'omit', referrerPolicy: 'no-referrer', signal: ctl ? ctl.signal : undefined })
.then(r => r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status)))
.then(text => {
const latest = listedVersion(text);
if (!latest) throw new Error('no version');
out.textContent = '';
if (newerVersion(versionParts(latest), versionParts(VERSION))) {
out.appendChild(document.createTextNode(_('Version %s is available.').format(latest) + ' '));
out.appendChild(this.releaseLink(_('What\'s new')));
}
else out.textContent = _('Up to date');
})
/* offline, or a router whose pages forbid the request (a
Content-Security-Policy): the release page still opens */
.catch(() => {
out.textContent = _('Could not check for updates.') + ' ';
out.appendChild(this.releaseLink(_('See the latest release')));
})
.then(() => { clearTimeout(timer); btn.removeAttribute('aria-disabled'); });
},
syncAppearance() { syncAppearance() {
if (!this.appear) return; if (!this.appear) return;
const now = { 'vt-colour': this.themePref(), 'vt-login': this.loginPref(), 'vt-backdrop': this.backdropPref() }; const now = { 'vt-colour': this.themePref(), 'vt-logo-name': this.brandPref(), 'vt-login': this.loginPref(), 'vt-backdrop': this.backdropPref() };
Array.prototype.forEach.call(this.appear.querySelectorAll('input[type="radio"]'), r => { r.checked = (now[r.name] === r.value); }); Array.prototype.forEach.call(this.appear.querySelectorAll('input[type="radio"]'), r => { r.checked = (now[r.name] === r.value); });
const state = this.lookState(); const state = this.lookState();
const note = state === 'relogin' ? _('Log in again to save Appearance for every browser.') const note = state === 'relogin' ? _('Log in again to save Appearance for every browser.')
@@ -1757,7 +1873,9 @@ return baseclass.extend({
if (!board || !board.hostname) return; if (!board || !board.hostname) return;
const product = host.productName(board); const product = host.productName(board);
this.hostname = board.hostname; this.hostname = board.hostname;
this.board = board;
this.updateTitle(); this.updateTitle();
this.applyBrand();
if (!chip) return; if (!chip) return;
chip.textContent = ''; chip.textContent = '';
/* device strings go in as text nodes (E() would parse a string as HTML) */ /* device strings go in as text nodes (E() would parse a string as HTML) */
@@ -8,9 +8,14 @@
* frame is a handful of batched draws, at most about 60 a second, and nothing runs while the * frame is a handful of batched draws, at most about 60 a second, and nothing runs while the
* window is in the background. With reduced motion it draws one still * window is in the background. With reduced motion it draws one still
* frame. Logging in speeds it up while the form posts. * frame. Logging in speeds it up while the form posts.
*
* This page means there is no session, so it also drops the history the
* dashboard kept in this tab (overview.js STORE_KEY): station MACs and samples
* shouldn't outlive a logout.
*/ */
(function () { (function () {
'use strict'; 'use strict';
try { sessionStorage.removeItem('vantage.hist.v1'); } catch (e) {}
var v = [ null, document.documentElement.getAttribute('data-vt-look-login'), null ], ok = [ 'grid', 'stars', 'none' ], pick = 'grid', i; var v = [ null, document.documentElement.getAttribute('data-vt-look-login'), null ], ok = [ 'grid', 'stars', 'none' ], pick = 'grid', i;
try { v[0] = localStorage.getItem('vantage.login.own'); v[2] = localStorage.getItem('vantage.login'); } catch (e) {} try { v[0] = localStorage.getItem('vantage.login.own'); v[2] = localStorage.getItem('vantage.login'); } catch (e) {}
for (i = 2; i >= 0; i--) if (ok.indexOf(v[i]) >= 0) pick = v[i]; for (i = 2; i >= 0; i--) if (ok.indexOf(v[i]) >= 0) pick = v[i];
@@ -393,6 +393,15 @@ html[data-darkmode="true"] .vt-brand-tile { fill: #16181b; }
text-decoration: none !important; text-decoration: none !important;
} }
.vt-brand svg { flex: none; } .vt-brand svg { flex: none; }
/* Appearance "Next to the logo" (menu-vantage.js applyBrand): beside the
logo while the rail is pinned open, else first in the top bar, as
"name / page"; not on phones, where the logo isn't shown either */
.vt-brand-name { display: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
html[data-rail="pinned"] .vt-brand-name:not([hidden]) { display: block; }
.vt-top-name { flex: none; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); font-weight: 600; }
.vt-top-name[hidden], html[data-rail="pinned"] .vt-top-name { display: none; }
.vt-top-name + .vt-crumbs::before { content: "/"; flex: none; margin-inline-end: 4px; color: var(--text-3); font-family: var(--mono); font-size: var(--fs-sm); }
.vt-top-name[hidden] + .vt-crumbs::before, html[data-rail="pinned"] .vt-top-name + .vt-crumbs::before { content: none; }
.vt-rail-list { .vt-rail-list {
list-style: none; margin: 0; padding: var(--sp-2) 0; list-style: none; margin: 0; padding: var(--sp-2) 0;
@@ -626,6 +635,12 @@ a.vt-crumb:hover { color: var(--text); }
/* where the picks are kept, when not for every browser */ /* where the picks are kept, when not for every browser */
.vt-appear-note { max-width: 32ch; margin: 0; color: var(--text-3); font-size: var(--fs-sm); line-height: 1.4; } .vt-appear-note { max-width: 32ch; margin: 0; color: var(--text-3); font-size: var(--fs-sm); line-height: 1.4; }
.vt-appear-note[hidden] { display: none; } .vt-appear-note[hidden] { display: none; }
.vt-appear-ver { margin: 0; color: var(--text-3); font-size: var(--fs-micro); }
.vt-appear-num { font-family: var(--mono); }
.vt-appear-check { margin: 0; padding: 0; border: 0; background: none; color: var(--accent-text); font: inherit; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
.vt-appear-check[aria-disabled="true"] { color: var(--text-3); cursor: default; text-decoration: none; }
.vt-appear-update { display: block; margin-top: 4px; color: var(--text-2); }
.vt-appear-update[hidden] { display: none; }
.vt-appear-set > legend { margin: 0 0 6px; padding: 0; color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; } .vt-appear-set > legend { margin: 0 0 6px; padding: 0; color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }
.vt-appear-track { display: flex; background: var(--seg-bg); border-radius: var(--r-lg); box-shadow: inset 0 0 0 1px var(--line); } .vt-appear-track { display: flex; background: var(--seg-bg); border-radius: var(--r-lg); box-shadow: inset 0 0 0 1px var(--line); }
.vt-appear-opt { .vt-appear-opt {
@@ -672,12 +687,38 @@ a.vt-crumb:hover { color: var(--text); }
} }
#indicators > span[data-clickable] { cursor: pointer; } #indicators > span[data-clickable] { cursor: pointer; }
#indicators > span[data-clickable]:hover { border-color: var(--text-3); color: var(--text); } #indicators > span[data-clickable]:hover { border-color: var(--text-3); color: var(--text); }
/* auto-refresh on: a still dot (the label says it, a pulse only /* auto-refresh on: a dot with a slow, soft ring (#100); fast while the
distracted); paused: a square */ dashboard reports a problem: amber while it reconnects or warns, red for
an error (luci-app-vantage sets html[data-vt-live] and [data-vt-alert]);
paused: a still square. The ring scales and fades on the compositor,
behind the dot (z-index -1 in the pill's own stack), and is transparent
unless it runs, so reduced motion shows none. */
#indicators > span[data-indicator="poll-status"][data-style="active"] { position: relative; isolation: isolate; }
#indicators > span[data-indicator="poll-status"][data-style="active"]::before { #indicators > span[data-indicator="poll-status"][data-style="active"]::before {
content: ""; width: 7px; height: 7px; border-radius: 50%; content: ""; width: 7px; height: 7px; border-radius: 50%;
background: var(--ok); background: var(--ok);
} }
#indicators > span[data-indicator="poll-status"][data-style="active"]::after {
content: ""; position: absolute; z-index: -1; left: 10px; top: calc(50% - 3.5px);
width: 7px; height: 7px; border-radius: 50%;
background: color-mix(in oklab, var(--ok) 50%, transparent);
opacity: 0; pointer-events: none;
animation: vt-live-soft 5s ease-out infinite;
}
html[data-vt-live="err"] #indicators > span[data-indicator="poll-status"][data-style="active"]::after, html[data-vt-alert="warn"] #indicators > span[data-indicator="poll-status"][data-style="active"]::after {
background: color-mix(in oklab, var(--warn) 50%, transparent);
animation: vt-live 2s ease-out infinite;
}
html[data-vt-alert="err"] #indicators > span[data-indicator="poll-status"][data-style="active"]::after {
background: color-mix(in oklab, var(--err) 50%, transparent);
animation: vt-live 2s ease-out infinite;
}
@media (width > 900px) {
html[data-vt-live="err"] #indicators > span[data-indicator="poll-status"][data-style="active"]::before, html[data-vt-alert="warn"] #indicators > span[data-indicator="poll-status"][data-style="active"]::before { background: var(--warn); }
html[data-vt-alert="err"] #indicators > span[data-indicator="poll-status"][data-style="active"]::before { background: var(--err); }
}
@keyframes vt-live-soft { 0% { transform: scale(1); opacity: .6; } 70%, 100% { transform: scale(2.2); opacity: 0; } }
@keyframes vt-live { 0% { transform: scale(1); opacity: 1; } 70%, 100% { transform: scale(2.7); opacity: 0; } }
#indicators > span[data-indicator="poll-status"][data-style="inactive"]::before { #indicators > span[data-indicator="poll-status"][data-style="inactive"]::before {
content: ""; width: 7px; height: 7px; border-radius: 1px; background: var(--text-3); content: ""; width: 7px; height: 7px; border-radius: 1px; background: var(--text-3);
} }
@@ -1717,6 +1758,14 @@ table { border-collapse: separate; border-spacing: 0; width: 100%; }
.cbi-section-table-descr > .th { font-weight: 400; text-transform: none; letter-spacing: 0; font-size: var(--fs-sm); } .cbi-section-table-descr > .th { font-weight: 400; text-transform: none; letter-spacing: 0; font-size: var(--fs-sm); }
.cbi-section-table-cell, .cbi-section-table-row > .td { vertical-align: middle; } .cbi-section-table-cell, .cbi-section-table-row > .td { vertical-align: middle; }
.td.cbi-value-field .cbi-dropdown, .td.cbi-value-field input:not([type="checkbox"]), .td.cbi-value-field select { max-width: 220px; } .td.cbi-value-field .cbi-dropdown, .td.cbi-value-field input:not([type="checkbox"]), .td.cbi-value-field select { max-width: 220px; }
/* A text field is width:100%, which an auto table layout counts as 0 wide, so
a column with a short header (Name and Path in Services > Network Shares)
shrank to a sliver and hid its value. Give the field a floor: 7em, which a
wide table (Samba's shares) still nearly fits beside; not on a phone,
where each row is a card of fixed columns and a floor overflows it. */
@media (width > 720px) {
.td.cbi-value-field input[type="text"], .td.cbi-value-field input:not([type]) { min-width: 7em; }
}
/* key/value tables (status pages): quiet label column */ /* key/value tables (status pages): quiet label column */
.table:not(:has(.table-titles)) .tr > .td:first-child:nth-last-child(2) { .table:not(:has(.table-titles)) .tr > .td:first-child:nth-last-child(2) {
@@ -3212,14 +3261,18 @@ body[data-page="admin-system-filemanager"] .sort-button { border-color: var(--li
.vt-search { width: 36px; padding: 0; justify-content: center; } .vt-search { width: 36px; padding: 0; justify-content: center; }
.vt-search-label, .vt-search .vt-kbd { display: none; } .vt-search-label, .vt-search .vt-kbd { display: none; }
.vt-host { max-width: 180px; } .vt-host { max-width: 180px; }
/* the name next to the logo crowded the host chip, which says it too */
.vt-top-name, .vt-top-name + .vt-crumbs::before { display: none !important; }
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.vt-host { display: none !important; } .vt-host { display: none !important; }
.vt-top-name, .vt-top-name + .vt-crumbs::before { display: none !important; }
/* indicators go compact (a sign, a count) before the crumbs get cut; /* indicators go compact (a sign, a count) before the crumbs get cut;
the label stays as text for screen readers and as the title */ the label stays as text for screen readers and as the title */
#indicators > span[data-indicator] { height: 28px; padding: 0 8px; } #indicators > span[data-indicator] { height: 28px; padding: 0 8px; }
#indicators > span[data-indicator="poll-status"] { font-size: 0; gap: 0; width: 28px; justify-content: center; padding: 0; } #indicators > span[data-indicator="poll-status"] { font-size: 0; gap: 0; width: 28px; justify-content: center; padding: 0; }
#indicators > span[data-indicator="poll-status"][data-style="active"]::after { left: calc(50% - 3.5px); }
/* without its label the pill is the pause / resume button: a pause /* without its label the pill is the pause / resume button: a pause
sign while it refreshes, a play sign while paused */ sign while it refreshes, a play sign while paused */
#indicators > span[data-indicator="poll-status"][data-style="active"]::before { #indicators > span[data-indicator="poll-status"][data-style="active"]::before {
+139 -90
View File
@@ -13,7 +13,7 @@ msgid_plural "%d more chains at this hook"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1403 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1446
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "%d result" msgid "%d result"
msgid_plural "%d results" msgid_plural "%d results"
@@ -56,7 +56,7 @@ msgctxt "vantage-theme"
msgid "Accept" msgid "Accept"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1323 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1366
msgid "Actions" msgid "Actions"
msgstr "" msgstr ""
@@ -65,22 +65,22 @@ msgctxt "nft match any traffic"
msgid "Any packet" msgid "Any packet"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1354
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1355
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1356
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1359
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1363
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1677 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1745
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:132 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:136
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Appearance" msgid "Appearance"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:48 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:50
msgid "Authorization Required" msgid "Authorization Required"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1747
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Auto" msgid "Auto"
msgstr "" msgstr ""
@@ -95,7 +95,7 @@ msgctxt "vantage-theme"
msgid "Back to the calling chain" msgid "Back to the calling chain"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:120 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:124
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Breadcrumb" msgid "Breadcrumb"
msgstr "" msgstr ""
@@ -113,17 +113,17 @@ msgctxt "vantage-theme"
msgid "Bridged between ports" msgid "Bridged between ports"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:492
msgid "Buffered" msgid "Buffered"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "Cache" msgid "Cache"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:492
msgid "Cached" msgid "Cached"
msgstr "" msgstr ""
@@ -142,19 +142,27 @@ msgctxt "vantage-theme"
msgid "Chains no rule jumps to directly" msgid "Chains no rule jumps to directly"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:544 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:587
msgid "Channel" msgid "Channel"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1783
msgid "Check for updates"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1798
msgid "Checking…"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:993
msgid "Collapse navigation" msgid "Collapse navigation"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1678 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1746
msgid "Colour theme" msgid "Colour theme"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1347 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1390
msgid "Command palette" msgid "Command palette"
msgstr "" msgstr ""
@@ -173,11 +181,15 @@ msgctxt "vantage-theme"
msgid "Continues in %s, then comes back here" msgid "Continues in %s, then comes back here"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1714 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1817
msgid "Could not check for updates."
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1830
msgid "Could not save this for every browser. Try again." msgid "Could not save this for every browser. Try again."
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1747
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Dark" msgid "Dark"
msgstr "" msgstr ""
@@ -201,8 +213,8 @@ msgstr ""
msgid "Egress" msgid "Egress"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:993
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:105 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:107
msgid "Expand navigation" msgid "Expand navigation"
msgstr "" msgstr ""
@@ -219,7 +231,7 @@ msgid ""
"their counters see only its first packets." "their counters see only its first packets."
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1356
msgid "Follow system theme" msgid "Follow system theme"
msgstr "" msgstr ""
@@ -237,21 +249,25 @@ msgid ""
"if br_netfilter is enabled; the ruleset doesn't show that.)" "if br_netfilter is enabled; the ruleset doesn't show that.)"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:515
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:488 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:529
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Free" msgid "Free"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1624
msgid "Grid packets" msgid "Grid packets"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:100 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:101
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Home" msgid "Home"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1750
msgid "Hostname"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:63 #: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:63
msgid "IPv4 traffic table \"%h\"" msgid "IPv4 traffic table \"%h\""
msgstr "" msgstr ""
@@ -264,9 +280,9 @@ msgstr ""
msgid "IPv6 traffic table \"%h\"" msgid "IPv6 traffic table \"%h\""
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:482 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:523
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:486 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:527
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "In use" msgid "In use"
msgstr "" msgstr ""
@@ -280,11 +296,11 @@ msgctxt "vantage-theme"
msgid "Input" msgid "Input"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:52 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:54
msgid "Invalid username and/or password! Please try again." msgid "Invalid username and/or password! Please try again."
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1386
msgid "Jump to a page or action…" msgid "Jump to a page or action…"
msgstr "" msgstr ""
@@ -293,31 +309,35 @@ msgctxt "vantage-theme"
msgid "Leaves unmatched unchanged" msgid "Leaves unmatched unchanged"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1747
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Light" msgid "Light"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:28 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:30
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:64 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:66
msgid "Log in" msgid "Log in"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1712 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1828
msgid "Log in again to save Appearance for every browser." msgid "Log in again to save Appearance for every browser."
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1365
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:135 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:139
msgid "Log out" msgid "Log out"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1359
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1681 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1752
msgid "Login background" msgid "Login background"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:99 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1750
msgid "Logo only"
msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:100
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Main navigation" msgid "Main navigation"
msgstr "" msgstr ""
@@ -327,13 +347,17 @@ msgctxt "vantage-theme"
msgid "Masquerade" msgid "Masquerade"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:457 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:472 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:513
msgid "Memory" msgid "Memory"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1961 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1750
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1978 msgid "Model"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2079
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2096
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "More actions" msgid "More actions"
msgstr "" msgstr ""
@@ -351,8 +375,8 @@ msgstr ""
msgid "Network device table \"%h\"" msgid "Network device table \"%h\""
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1363
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1670 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1738
msgid "Network path background" msgid "Network path background"
msgstr "" msgstr ""
@@ -361,7 +385,11 @@ msgctxt "vantage-theme"
msgid "Next rule" msgid "Next rule"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1401 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1749
msgid "Next to the logo"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1444
msgid "No matching pages" msgid "No matching pages"
msgstr "" msgstr ""
@@ -375,17 +403,17 @@ msgctxt "vantage-theme"
msgid "No rules: %s" msgid "No rules: %s"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1624
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "None" msgid "None"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1218 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1261
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:17 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:17
msgid "Not found" msgid "Not found"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1238 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1281
msgid "Other pages in %s" msgid "Other pages in %s"
msgstr "" msgstr ""
@@ -399,7 +427,7 @@ msgctxt "vantage-theme"
msgid "Packet path" msgid "Packet path"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:141 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:145
msgid "Page tabs" msgid "Page tabs"
msgstr "" msgstr ""
@@ -413,7 +441,7 @@ msgctxt "vantage-theme"
msgid "Passes unmatched" msgid "Passes unmatched"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:61 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:63
msgid "Password" msgid "Password"
msgstr "" msgstr ""
@@ -442,7 +470,7 @@ msgctxt "vantage-theme"
msgid "Reject" msgid "Reject"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1345 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1388
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Results" msgid "Results"
msgstr "" msgstr ""
@@ -462,17 +490,21 @@ msgctxt "vantage-theme"
msgid "Rules table" msgid "Rules table"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1713 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1829
msgid "Saved in this browser only." msgid "Saved in this browser only."
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1386
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:123 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:127
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:125 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:129
msgid "Search pages" msgid "Search pages"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1818
msgid "See the latest release"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1365
msgid "Session" msgid "Session"
msgstr "" msgstr ""
@@ -483,47 +515,52 @@ msgid_plural "Show %d more rules"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2118 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2236
msgid "Show full description" msgid "Show full description"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2107 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2225
msgid "Show full help: %s" msgid "Show full help: %s"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:96 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:97
msgid "Skip to content" msgid "Skip to content"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1624
msgid "Starfield" msgid "Starfield"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:509 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:552
msgid "Storage" msgid "Storage"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:495 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:538
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:497 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:540
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Swap" msgid "Swap"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:493
msgid "Swap free" msgid "Swap free"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:493
msgid "Swap used"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:249 #: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:249
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "This device" msgid "This device"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:515
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Total" msgid "Total"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:461 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:491
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:499
msgid "Total Available" msgid "Total Available"
msgstr "" msgstr ""
@@ -537,19 +574,23 @@ msgctxt "vantage-theme"
msgid "Unchanged" msgid "Unchanged"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1812
msgid "Up to date"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1354
msgid "Use dark theme" msgid "Use dark theme"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1355
msgid "Use light theme" msgid "Use light theme"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:461 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:491
msgid "Used" msgid "Used"
msgstr "" msgstr ""
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:57 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:59
msgid "Username" msgid "Username"
msgstr "" msgstr ""
@@ -565,21 +606,29 @@ msgctxt "vantage-theme"
msgid "Verdict map %s" msgid "Verdict map %s"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1809
msgid "Version %s is available."
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:204 #: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:204
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "View" msgid "View"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:537 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1810
msgid "What's new"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:580
msgid "Wireless" msgid "Wireless"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:478 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:519
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "available" msgid "available"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1353 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1396
msgid "close" msgid "close"
msgstr "" msgstr ""
@@ -593,12 +642,12 @@ msgctxt "vantage-theme"
msgid "drops unmatched %s" msgid "drops unmatched %s"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
msgid "free" msgid "free"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1351 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1394
msgid "navigate" msgid "navigate"
msgstr "" msgstr ""
@@ -617,7 +666,7 @@ msgctxt "vantage-theme"
msgid "on %s" msgid "on %s"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1352 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1395
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "open" msgid "open"
msgstr "" msgstr ""
@@ -627,9 +676,9 @@ msgctxt "vantage-theme"
msgid "priority %d" msgid "priority %d"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "reclaimable" msgid "reclaimable"
msgstr "" msgstr ""
@@ -638,12 +687,12 @@ msgctxt "vantage-theme"
msgid "rules drop some" msgid "rules drop some"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
msgid "total" msgid "total"
msgstr "" msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
msgid "used" msgid "used"
msgstr "" msgstr ""
+139 -90
View File
@@ -21,7 +21,7 @@ msgid "%d more chain at this hook"
msgid_plural "%d more chains at this hook" msgid_plural "%d more chains at this hook"
msgstr[0] "此钩子还有 %d 条链" msgstr[0] "此钩子还有 %d 条链"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1403 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1446
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "%d result" msgid "%d result"
msgid_plural "%d results" msgid_plural "%d results"
@@ -62,7 +62,7 @@ msgctxt "vantage-theme"
msgid "Accept" msgid "Accept"
msgstr "接受" msgstr "接受"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1323 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1366
msgid "Actions" msgid "Actions"
msgstr "操作" msgstr "操作"
@@ -71,22 +71,22 @@ msgctxt "nft match any traffic"
msgid "Any packet" msgid "Any packet"
msgstr "任意数据包" msgstr "任意数据包"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1354
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1355
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1356
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1359
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1363
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1677 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1745
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:132 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:136
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Appearance" msgid "Appearance"
msgstr "外观" msgstr "外观"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:48 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:50
msgid "Authorization Required" msgid "Authorization Required"
msgstr "需要授权" msgstr "需要授权"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1747
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Auto" msgid "Auto"
msgstr "自动" msgstr "自动"
@@ -101,7 +101,7 @@ msgctxt "vantage-theme"
msgid "Back to the calling chain" msgid "Back to the calling chain"
msgstr "返回调用链" msgstr "返回调用链"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:120 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:124
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Breadcrumb" msgid "Breadcrumb"
msgstr "面包屑" msgstr "面包屑"
@@ -119,17 +119,17 @@ msgctxt "vantage-theme"
msgid "Bridged between ports" msgid "Bridged between ports"
msgstr "端口间桥接" msgstr "端口间桥接"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:492
msgid "Buffered" msgid "Buffered"
msgstr "已缓冲" msgstr "已缓冲"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "Cache" msgid "Cache"
msgstr "缓存" msgstr "缓存"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:492
msgid "Cached" msgid "Cached"
msgstr "已缓存" msgstr "已缓存"
@@ -148,19 +148,27 @@ msgctxt "vantage-theme"
msgid "Chains no rule jumps to directly" msgid "Chains no rule jumps to directly"
msgstr "没有规则直接跳转到的链" msgstr "没有规则直接跳转到的链"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:544 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:587
msgid "Channel" msgid "Channel"
msgstr "信道" msgstr "信道"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1783
msgid "Check for updates"
msgstr "检查更新"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1798
msgid "Checking…"
msgstr "正在检查…"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:993
msgid "Collapse navigation" msgid "Collapse navigation"
msgstr "收起导航" msgstr "收起导航"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1678 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1746
msgid "Colour theme" msgid "Colour theme"
msgstr "颜色主题" msgstr "颜色主题"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1347 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1390
msgid "Command palette" msgid "Command palette"
msgstr "命令面板" msgstr "命令面板"
@@ -179,11 +187,15 @@ msgctxt "vantage-theme"
msgid "Continues in %s, then comes back here" msgid "Continues in %s, then comes back here"
msgstr "转入 %s,随后返回此处" msgstr "转入 %s,随后返回此处"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1714 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1817
msgid "Could not check for updates."
msgstr "无法检查更新。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1830
msgid "Could not save this for every browser. Try again." msgid "Could not save this for every browser. Try again."
msgstr "无法为所有浏览器保存此设置,请重试。" msgstr "无法为所有浏览器保存此设置,请重试。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1747
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Dark" msgid "Dark"
msgstr "深色" msgstr "深色"
@@ -207,8 +219,8 @@ msgstr "丢弃未匹配流量"
msgid "Egress" msgid "Egress"
msgstr "出口" msgstr "出口"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:993
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:105 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:107
msgid "Expand navigation" msgid "Expand navigation"
msgstr "展开导航" msgstr "展开导航"
@@ -227,7 +239,7 @@ msgstr ""
"流量卸载已开启:一条转发连接建立后,它的数据包可以跳过 prerouting、forward " "流量卸载已开启:一条转发连接建立后,它的数据包可以跳过 prerouting、forward "
"和 postrouting,因此这些链和它们的计数器只会看到该连接最初的数据包。" "和 postrouting,因此这些链和它们的计数器只会看到该连接最初的数据包。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1356
msgid "Follow system theme" msgid "Follow system theme"
msgstr "跟随系统主题" msgstr "跟随系统主题"
@@ -248,21 +260,25 @@ msgstr ""
"达 input、forward 或 output 链,这里也没有桥接链会过滤它们。(启用 " "达 input、forward 或 output 链,这里也没有桥接链会过滤它们。(启用 "
"br_netfilter 时内核可以把它们交给 IP 链;规则集不体现这一点。)" "br_netfilter 时内核可以把它们交给 IP 链;规则集不体现这一点。)"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:515
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:488 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:529
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Free" msgid "Free"
msgstr "空闲" msgstr "空闲"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1624
msgid "Grid packets" msgid "Grid packets"
msgstr "网格数据包" msgstr "网格数据包"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:100 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:101
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Home" msgid "Home"
msgstr "首页" msgstr "首页"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1750
msgid "Hostname"
msgstr "主机名"
#: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:63 #: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:63
msgid "IPv4 traffic table \"%h\"" msgid "IPv4 traffic table \"%h\""
msgstr "IPv4 流量表 \"%h\"" msgstr "IPv4 流量表 \"%h\""
@@ -275,9 +291,9 @@ msgstr "IPv4/IPv6 流量表 \"%h\""
msgid "IPv6 traffic table \"%h\"" msgid "IPv6 traffic table \"%h\""
msgstr "IPv6 流量表 \"%h\"" msgstr "IPv6 流量表 \"%h\""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:482 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:523
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:486 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:527
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "In use" msgid "In use"
msgstr "已用" msgstr "已用"
@@ -291,11 +307,11 @@ msgctxt "vantage-theme"
msgid "Input" msgid "Input"
msgstr "输入" msgstr "输入"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:52 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:54
msgid "Invalid username and/or password! Please try again." msgid "Invalid username and/or password! Please try again."
msgstr "用户名或密码错误,请重试。" msgstr "用户名或密码错误,请重试。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1386
msgid "Jump to a page or action…" msgid "Jump to a page or action…"
msgstr "跳转到页面或操作…" msgstr "跳转到页面或操作…"
@@ -304,31 +320,35 @@ msgctxt "vantage-theme"
msgid "Leaves unmatched unchanged" msgid "Leaves unmatched unchanged"
msgstr "未匹配流量保持原样" msgstr "未匹配流量保持原样"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1747
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Light" msgid "Light"
msgstr "浅色" msgstr "浅色"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:28 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:30
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:64 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:66
msgid "Log in" msgid "Log in"
msgstr "登录" msgstr "登录"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1712 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1828
msgid "Log in again to save Appearance for every browser." msgid "Log in again to save Appearance for every browser."
msgstr "重新登录后即可为所有浏览器保存外观设置。" msgstr "重新登录后即可为所有浏览器保存外观设置。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1365
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:135 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:139
msgid "Log out" msgid "Log out"
msgstr "退出登录" msgstr "退出登录"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1359
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1681 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1752
msgid "Login background" msgid "Login background"
msgstr "登录背景" msgstr "登录背景"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:99 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1750
msgid "Logo only"
msgstr "仅标志"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:100
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Main navigation" msgid "Main navigation"
msgstr "主导航" msgstr "主导航"
@@ -338,13 +358,17 @@ msgctxt "vantage-theme"
msgid "Masquerade" msgid "Masquerade"
msgstr "伪装(NAT)" msgstr "伪装(NAT)"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:457 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:472 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:513
msgid "Memory" msgid "Memory"
msgstr "内存" msgstr "内存"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1961 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1750
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1978 msgid "Model"
msgstr "型号"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2079
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2096
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "More actions" msgid "More actions"
msgstr "更多操作" msgstr "更多操作"
@@ -362,8 +386,8 @@ msgstr "网络"
msgid "Network device table \"%h\"" msgid "Network device table \"%h\""
msgstr "网络设备表 \"%h\"" msgstr "网络设备表 \"%h\""
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1363
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1670 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1738
msgid "Network path background" msgid "Network path background"
msgstr "网络路径背景" msgstr "网络路径背景"
@@ -372,7 +396,11 @@ msgctxt "vantage-theme"
msgid "Next rule" msgid "Next rule"
msgstr "下一条规则" msgstr "下一条规则"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1401 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1749
msgid "Next to the logo"
msgstr "标志旁显示"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1444
msgid "No matching pages" msgid "No matching pages"
msgstr "没有匹配的页面" msgstr "没有匹配的页面"
@@ -386,17 +414,17 @@ msgctxt "vantage-theme"
msgid "No rules: %s" msgid "No rules: %s"
msgstr "无规则:%s" msgstr "无规则:%s"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1624
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "None" msgid "None"
msgstr "无" msgstr "无"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1218 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1261
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:17 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:17
msgid "Not found" msgid "Not found"
msgstr "未找到" msgstr "未找到"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1238 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1281
msgid "Other pages in %s" msgid "Other pages in %s"
msgstr "%s 中的其他页面" msgstr "%s 中的其他页面"
@@ -410,7 +438,7 @@ msgctxt "vantage-theme"
msgid "Packet path" msgid "Packet path"
msgstr "数据包路径" msgstr "数据包路径"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:141 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:145
msgid "Page tabs" msgid "Page tabs"
msgstr "页面标签页" msgstr "页面标签页"
@@ -424,7 +452,7 @@ msgctxt "vantage-theme"
msgid "Passes unmatched" msgid "Passes unmatched"
msgstr "放行未匹配流量" msgstr "放行未匹配流量"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:61 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:63
msgid "Password" msgid "Password"
msgstr "密码" msgstr "密码"
@@ -453,7 +481,7 @@ msgctxt "vantage-theme"
msgid "Reject" msgid "Reject"
msgstr "拒绝" msgstr "拒绝"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1345 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1388
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Results" msgid "Results"
msgstr "结果" msgstr "结果"
@@ -473,17 +501,21 @@ msgctxt "vantage-theme"
msgid "Rules table" msgid "Rules table"
msgstr "规则表" msgstr "规则表"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1713 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1829
msgid "Saved in this browser only." msgid "Saved in this browser only."
msgstr "仅保存在此浏览器中。" msgstr "仅保存在此浏览器中。"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1386
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:123 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:127
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:125 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:129
msgid "Search pages" msgid "Search pages"
msgstr "搜索页面" msgstr "搜索页面"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1818
msgid "See the latest release"
msgstr "查看最新版本"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1365
msgid "Session" msgid "Session"
msgstr "会话" msgstr "会话"
@@ -493,47 +525,52 @@ msgid "Show %d more rule"
msgid_plural "Show %d more rules" msgid_plural "Show %d more rules"
msgstr[0] "再显示 %d 条规则" msgstr[0] "再显示 %d 条规则"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2118 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2236
msgid "Show full description" msgid "Show full description"
msgstr "显示完整描述" msgstr "显示完整描述"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2107 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2225
msgid "Show full help: %s" msgid "Show full help: %s"
msgstr "显示完整帮助:%s" msgstr "显示完整帮助:%s"
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:96 #: luci-theme-vantage/ucode/template/themes/vantage/header.ut:97
msgid "Skip to content" msgid "Skip to content"
msgstr "跳至正文" msgstr "跳至正文"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1624
msgid "Starfield" msgid "Starfield"
msgstr "星空" msgstr "星空"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:509 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:552
msgid "Storage" msgid "Storage"
msgstr "存储" msgstr "存储"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:495 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:538
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:497 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:540
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Swap" msgid "Swap"
msgstr "交换空间" msgstr "交换空间"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:462 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:493
msgid "Swap free" msgid "Swap free"
msgstr "空闲交换空间" msgstr "空闲交换空间"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:493
msgid "Swap used"
msgstr ""
#: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:249 #: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:249
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "This device" msgid "This device"
msgstr "本设备" msgstr "本设备"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:515
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "Total" msgid "Total"
msgstr "总计" msgstr "总计"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:461 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:491
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:499
msgid "Total Available" msgid "Total Available"
msgstr "总可用" msgstr "总可用"
@@ -547,19 +584,23 @@ msgctxt "vantage-theme"
msgid "Unchanged" msgid "Unchanged"
msgstr "不变" msgstr "不变"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1812
msgid "Up to date"
msgstr "已是最新版本"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1354
msgid "Use dark theme" msgid "Use dark theme"
msgstr "使用深色主题" msgstr "使用深色主题"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1355
msgid "Use light theme" msgid "Use light theme"
msgstr "使用浅色主题" msgstr "使用浅色主题"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:461 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:491
msgid "Used" msgid "Used"
msgstr "已用" msgstr "已用"
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:57 #: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:59
msgid "Username" msgid "Username"
msgstr "用户名" msgstr "用户名"
@@ -577,21 +618,29 @@ msgctxt "vantage-theme"
msgid "Verdict map %s" msgid "Verdict map %s"
msgstr "判定映射 %s" msgstr "判定映射 %s"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1809
msgid "Version %s is available."
msgstr "版本 %s 可用。"
#: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:204 #: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:204
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "View" msgid "View"
msgstr "视图" msgstr "视图"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:537 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1810
msgid "What's new"
msgstr "更新内容"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:580
msgid "Wireless" msgid "Wireless"
msgstr "无线" msgstr "无线"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:478 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:519
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "available" msgid "available"
msgstr "可用" msgstr "可用"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1353 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1396
msgid "close" msgid "close"
msgstr "关闭" msgstr "关闭"
@@ -605,12 +654,12 @@ msgctxt "vantage-theme"
msgid "drops unmatched %s" msgid "drops unmatched %s"
msgstr "丢弃未匹配的 %s" msgstr "丢弃未匹配的 %s"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
msgid "free" msgid "free"
msgstr "空闲" msgstr "空闲"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1351 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1394
msgid "navigate" msgid "navigate"
msgstr "移动" msgstr "移动"
@@ -629,7 +678,7 @@ msgctxt "vantage-theme"
msgid "on %s" msgid "on %s"
msgstr "接口 %s" msgstr "接口 %s"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1352 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1395
msgctxt "vantage-theme" msgctxt "vantage-theme"
msgid "open" msgid "open"
msgstr "打开" msgstr "打开"
@@ -639,9 +688,9 @@ msgctxt "vantage-theme"
msgid "priority %d" msgid "priority %d"
msgstr "优先级 %d" msgstr "优先级 %d"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
msgid "reclaimable" msgid "reclaimable"
msgstr "可回收" msgstr "可回收"
@@ -650,13 +699,13 @@ msgctxt "vantage-theme"
msgid "rules drop some" msgid "rules drop some"
msgstr "部分规则会丢弃" msgstr "部分规则会丢弃"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
msgid "total" msgid "total"
msgstr "总计" msgstr "总计"
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528 #: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
msgid "used" msgid "used"
msgstr "已用" msgstr "已用"
@@ -6,6 +6,7 @@
# option theme 'auto' auto, light or dark # option theme 'auto' auto, light or dark
# option login 'grid' the login page's background: grid, stars or none # option login 'grid' the login page's background: grid, stars or none
# option backdrop 'grid' the dashboard's network path background, likewise # option backdrop 'grid' the dashboard's network path background, likewise
# option brand 'none' next to the logo: none, hostname or model
# #
# The dashboard's Customize saves here too: its chart style (option chart) and # The dashboard's Customize saves here too: its chart style (option chart) and
# one layout per screen size (option layout_wide, layout_mid, layout_narrow and # one layout per screen size (option layout_wide, layout_mid, layout_narrow and
@@ -23,6 +23,7 @@
const look_theme = look('theme', [ 'auto', 'light', 'dark' ]); const look_theme = look('theme', [ 'auto', 'light', 'dark' ]);
const look_login = look('login', [ 'grid', 'stars', 'none' ]); const look_login = look('login', [ 'grid', 'stars', 'none' ]);
const look_backdrop = look('backdrop', [ 'grid', 'stars', 'none' ]); const look_backdrop = look('backdrop', [ 'grid', 'stars', 'none' ]);
const look_brand = look('brand', [ 'none', 'hostname', 'model' ]);
/* The dashboard's chart style and layouts, saved with it. A layout passes /* The dashboard's chart style and layouts, saved with it. A layout passes
only in the form vantage.layout reads, 'wide=hero:0:4 chart:0:2:h;mid=default' */ only in the form vantage.layout reads, 'wide=hero:0:4 chart:0:2:h;mid=default' */
const look_chart = look('chart', [ 'overlay', 'log', 'mirror', 'mirror-split', 'lanes' ]); const look_chart = look('chart', [ 'overlay', 'log', 'mirror', 'mirror-split', 'lanes' ]);
@@ -53,11 +54,9 @@
}); });
return would ? 'relogin' : 'no'; return would ? 'relogin' : 'no';
})(); })();
const boardinfo = ubus.call("system", "board");
const hostname = striptags(boardinfo?.hostname ?? '?');
-%} -%}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ lang }}" data-darkmode="true" data-vt-look-theme="{{ look_theme }}" data-vt-look-login="{{ look_login }}" data-vt-look-backdrop="{{ look_backdrop }}" data-vt-look-chart="{{ look_chart }}" data-vt-look-layout="{{ look_layout }}" data-vt-look-save="{{ look_save }}"> <html lang="{{ lang }}" data-darkmode="true" data-vt-look-theme="{{ look_theme }}" data-vt-look-login="{{ look_login }}" data-vt-look-backdrop="{{ look_backdrop }}" data-vt-look-brand="{{ look_brand }}" data-vt-look-chart="{{ look_chart }}" data-vt-look-layout="{{ look_layout }}" data-vt-look-save="{{ look_save }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
@@ -108,7 +107,7 @@ const hostname = striptags(boardinfo?.hostname ?? '?');
<circle cx="21.5" cy="10.5" r="1.9" class="vt-brand-dot"></circle> <circle cx="21.5" cy="10.5" r="1.9" class="vt-brand-dot"></circle>
<circle cx="21.5" cy="21.5" r="1.9" class="vt-brand-dot"></circle> <circle cx="21.5" cy="21.5" r="1.9" class="vt-brand-dot"></circle>
</svg> </svg>
<h3 class="vt-brand-word">{{ hostname }}</h3> <span class="vt-brand-name" id="vt-brand-name" hidden></span>
</a> </a>
<ul class="vt-rail-list" id="vt-rail-list"></ul> <ul class="vt-rail-list" id="vt-rail-list"></ul>
<div class="vt-rail-foot"> <div class="vt-rail-foot">
@@ -126,6 +125,8 @@ const hostname = striptags(boardinfo?.hostname ?? '?');
<span class="vt-brand vt-brand-static" aria-hidden="true"> <span class="vt-brand vt-brand-static" aria-hidden="true">
<svg viewBox="0 0 32 32" width="28" height="28"><rect x="1" y="1" width="30" height="30" rx="7" class="vt-brand-tile"/><path d="M9 10.5 16 22" class="vt-brand-l"/><path d="M16 22 23 10.5" class="vt-brand-r"/><circle cx="23" cy="10.5" r="1.9" class="vt-brand-dot"/></svg> <svg viewBox="0 0 32 32" width="28" height="28"><rect x="1" y="1" width="30" height="30" rx="7" class="vt-brand-tile"/><path d="M9 10.5 16 22" class="vt-brand-l"/><path d="M16 22 23 10.5" class="vt-brand-r"/><circle cx="23" cy="10.5" r="1.9" class="vt-brand-dot"/></svg>
</span> </span>
{% else %}
<span class="vt-top-name" id="vt-top-name" hidden></span>
{% endif %} {% endif %}
<nav class="vt-crumbs" id="vt-crumbs" aria-label="{{ entityencode(_('Breadcrumb', 'vantage-theme'), true) }}"><span class="vt-crumb vt-crumb-current">{{ entityencode(page_title ?? 'LuCI', true) }}</span></nav> <nav class="vt-crumbs" id="vt-crumbs" aria-label="{{ entityencode(_('Breadcrumb', 'vantage-theme'), true) }}"><span class="vt-crumb vt-crumb-current">{{ entityencode(page_title ?? 'LuCI', true) }}</span></nav>
<div class="vt-top-actions"> <div class="vt-top-actions">
@@ -1,7 +1,8 @@
{# {#
Vantage theme - login page. Vantage theme - login page.
Rendered with { duser, fuser } for visitors who are not logged in, so it Rendered with { duser, fuser } for visitors who are not logged in. Of the
shows nothing about the device: no hostname, model or firmware version. device it shows the hostname only, as the heading (LuCI's Bootstrap shows
it too): no model or firmware version.
fuser is attacker-controlled (formvalue also reads the query string): it is fuser is attacker-controlled (formvalue also reads the query string): it is
only tested, never echoed; the error is generic. The form posts back to the only tested, never echoed; the error is generic. The form posts back to the
requested URL (no action attribute). The inline script is data-free: the requested URL (no action attribute). The inline script is data-free: the
@@ -17,8 +18,7 @@
const look = (opt, ok) => { const v = uci.get('vantage_theme', 'appearance', opt); return (type(v) == 'string' && index(ok, v) >= 0) ? v : ''; }; const look = (opt, ok) => { const v = uci.get('vantage_theme', 'appearance', opt); return (type(v) == 'string' && index(ok, v) >= 0) ? v : ''; };
const look_theme = look('theme', [ 'auto', 'light', 'dark' ]); const look_theme = look('theme', [ 'auto', 'light', 'dark' ]);
const look_login = look('login', [ 'grid', 'stars', 'none' ]); const look_login = look('login', [ 'grid', 'stars', 'none' ]);
const boardinfo = ubus.call("system", "board"); const host = (() => { const h = uci.get_first('system', 'system', 'hostname'); return (type(h) == 'string' && length(h)) ? substr(h, 0, 63) : null; })();
const hostname = striptags(boardinfo?.hostname ?? '?');
-%} -%}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ entityencode(dispatcher.lang ?? 'en', true) }}" data-darkmode="true" data-vt-look-theme="{{ look_theme }}" data-vt-look-login="{{ look_login }}"> <html lang="{{ entityencode(dispatcher.lang ?? 'en', true) }}" data-darkmode="true" data-vt-look-theme="{{ look_theme }}" data-vt-look-login="{{ look_login }}">
@@ -54,10 +54,7 @@
<circle cx="21.5" cy="10.5" r="1.9" class="vt-brand-dot"></circle> <circle cx="21.5" cy="10.5" r="1.9" class="vt-brand-dot"></circle>
<circle cx="21.5" cy="21.5" r="1.9" class="vt-brand-dot"></circle> <circle cx="21.5" cy="21.5" r="1.9" class="vt-brand-dot"></circle>
</svg> </svg>
<div> <h1 class="vt-login-title">{{ entityencode(host ?? _('Authorization Required'), true) }}</h1>
<h1 class="vt-login-title">{{ hostname }}</h1>
<p class="vt-login-sub"><span class="vt-login-dot" aria-hidden="true"></span>{{ entityencode(_('Authorization Required'), true) }}</p>
</div>
</div> </div>
{% if (fuser): %} {% if (fuser): %}
+3 -3
View File
@@ -50,12 +50,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=wwand PKG_NAME:=wwand
PKG_VERSION:=1.9.0_pre3 PKG_VERSION:=1.9.0_pre10
PKG_RELEASE:=88 PKG_RELEASE:=89
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:=0a1f02375df53f21a105b375b9af051c36464530 PKG_SOURCE_VERSION:=e9fd1db72db20faced06e323c06e8a1062607bf2
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only PKG_LICENSE:=GPL-2.0-only