mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-09 05:27:59 +08:00
🐶 Sync 2026-11-08 23:51:11
This commit is contained in:
1 parent
5a5196e2d6
commit
534c8d6ab2
73 files changed
+12872
-2580
No files matched your search
Binary file not shown.
+2
-2
@@ -6,13 +6,13 @@ PKG_UPSTREAM_TAG:=v$(PKG_UPSTREAM_VERSION)
|
||||
PKG_SHORT_SHA:=3f27ef3
|
||||
|
||||
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_SUBDIR:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA)
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR)
|
||||
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_BUILD_VERSION:=$(PKG_UPSTREAM_TAG)
|
||||
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gately
|
||||
PKG_VERSION:=0.6.5.1
|
||||
PKG_RELEASE:=28
|
||||
PKG_VERSION:=0.6.5.2
|
||||
PKG_RELEASE:=29
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
+3
-3
@@ -5,11 +5,11 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=honk
|
||||
PKG_VERSION:=2026.11.07~73b1077
|
||||
PKG_RELEASE:=32
|
||||
PKG_VERSION:=2026.11.08~62b1390
|
||||
PKG_RELEASE:=33
|
||||
|
||||
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_FILES:=LICENSE
|
||||
|
||||
@@ -11,8 +11,8 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=istoreenhance
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=0.8.8
|
||||
PKG_RELEASE:=15
|
||||
PKG_SOURCE_DATE:=0.9.0
|
||||
PKG_RELEASE:=16
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
ARCH_HEXCODE=8664
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_KS_CLI:=$(ARCH)
|
||||
|
||||
PKG_NAME:=ks-cli
|
||||
PKG_VERSION:=0.8.9
|
||||
PKG_RELEASE:=6
|
||||
PKG_VERSION:=0.9.0
|
||||
PKG_RELEASE:=7
|
||||
|
||||
PKG_SOURCE:=ks-cli-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$(PKG_VERSION)/
|
||||
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=LuCI support for KSpeeder
|
||||
LUCI_DEPENDS:=+istoreenhance +luci-lib-linkeaseauth +linkease-app-entry
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=0.8.8-r1
|
||||
PKG_RELEASE:=9
|
||||
PKG_VERSION:=0.9.0-r1
|
||||
PKG_RELEASE:=10
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=KaiPlus
|
||||
PKG_VERSION:=1.0.9-r9
|
||||
PKG_RELEASE:=15
|
||||
PKG_VERSION:=1.0.11-r1
|
||||
PKG_RELEASE:=16
|
||||
LUCI_DEPENDS:=+kaiplus +luci-compat +luci-lib-taskd +luci-lib-linkeaseauth +linkease-app-entry
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-open-box
|
||||
PKG_VERSION:=0.1.414
|
||||
PKG_RELEASE:=69
|
||||
PKG_VERSION:=0.1.420
|
||||
PKG_RELEASE:=70
|
||||
|
||||
PKG_MAINTAINER:=liandu2024
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.11.3
|
||||
PKG_RELEASE:=370
|
||||
PKG_RELEASE:=371
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -336,6 +336,17 @@ local com = self.com
|
||||
font-weight: normal;
|
||||
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>
|
||||
|
||||
<div class="cbi-value">
|
||||
@@ -343,13 +354,13 @@ local com = self.com
|
||||
<span class="app-version-sub"><%=api.get_version()%></span>
|
||||
</label>
|
||||
<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%>"
|
||||
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%>"
|
||||
onclick="doUpdate(this, appname);" value="<%:Force update%>" style="display:none" />
|
||||
<span id="<%=api.appname%>-check_btn-detail"></span>
|
||||
<!--/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>
|
||||
</label>
|
||||
<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%>"
|
||||
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%>"
|
||||
onclick="doUpdate(this,'<%=k%>');" value="<%:Force update%>" style="display:none"/>
|
||||
<span id="_<%=k%>-check_btn-detail"></span>
|
||||
<!--/div-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%end%>
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.11.3
|
||||
PKG_RELEASE:=172
|
||||
PKG_RELEASE:=173
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -344,6 +344,17 @@ local com = self.com
|
||||
font-weight: normal;
|
||||
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>
|
||||
|
||||
<div class="cbi-value">
|
||||
@@ -351,7 +362,7 @@ local com = self.com
|
||||
<span class="app-version-sub"><%=api.get_version()%></span>
|
||||
</label>
|
||||
<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%>"
|
||||
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%>"
|
||||
@@ -370,7 +381,7 @@ local com = self.com
|
||||
<span class="app-version-sub"><span class="com_version" key="<%=k%>"><%=translate("Null")%></span></span>
|
||||
</label>
|
||||
<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%>"
|
||||
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%>"
|
||||
|
||||
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-ssr-plus
|
||||
PKG_VERSION:=198
|
||||
PKG_RELEASE:=101
|
||||
PKG_RELEASE:=102
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
|
||||
@@ -20,8 +20,7 @@ local CLASH_RULES_DIR = "/etc/ssrplus/clash"
|
||||
local SUPPORTED_COMPONENTS = {
|
||||
mainprogram = true,
|
||||
xray = true,
|
||||
mihomo = true,
|
||||
naiveproxy = true
|
||||
mihomo = true
|
||||
}
|
||||
local SUPPORTED_GEO_COMPONENTS = {
|
||||
{ name = "country_mmdb", require = nil },
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
mihomo: {
|
||||
label: '<%:Mihomo%>',
|
||||
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%>',
|
||||
'xray binary not found in archive': '<%:xray 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%>',
|
||||
'Installed binary failed to run': '<%:Installed binary failed to run%>',
|
||||
'Unsupported package manager': '<%:Unsupported package manager%>',
|
||||
@@ -468,7 +463,6 @@
|
||||
<option value="mainprogram"><%:SSR Plus+ Main Program%></option>
|
||||
<option value="xray"><%:Xray-core%></option>
|
||||
<option value="mihomo"><%:Mihomo%></option>
|
||||
<option value="naiveproxy"><%:NaiveProxy%></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2677,12 +2677,6 @@ msgid ""
|
||||
"package for this system from fw876/helloworld releases."
|
||||
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:871
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:89
|
||||
@@ -3265,10 +3259,6 @@ msgstr ""
|
||||
msgid "mihomo binary not found in archive"
|
||||
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
|
||||
msgid "native UDP characteristics"
|
||||
msgstr ""
|
||||
|
||||
@@ -2701,13 +2701,6 @@ msgid ""
|
||||
msgstr ""
|
||||
"在线升级会从 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:871
|
||||
#: 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"
|
||||
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
|
||||
msgid "native UDP characteristics"
|
||||
msgstr "原生 UDP 特性"
|
||||
|
||||
@@ -103,17 +103,6 @@ version_gt() {
|
||||
[ "$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() {
|
||||
local arch
|
||||
|
||||
@@ -163,22 +152,6 @@ find_mihomo_binary() {
|
||||
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() {
|
||||
if [ ! -x "$XRAY_BINARY" ]; then
|
||||
return 1
|
||||
@@ -211,29 +184,6 @@ get_mihomo_current_version() {
|
||||
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() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
@@ -1130,71 +1080,6 @@ get_mihomo_latest_info() {
|
||||
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() {
|
||||
local pm="$1"
|
||||
local version
|
||||
@@ -1522,54 +1407,6 @@ mihomo_info() {
|
||||
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() {
|
||||
local pm current installed arch
|
||||
|
||||
@@ -1614,28 +1451,6 @@ mihomo_local_info() {
|
||||
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() {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
# 主程序相关
|
||||
@@ -1890,16 +1631,6 @@ case "${1:-}" in
|
||||
mihomo_upgrade)
|
||||
mihomo_upgrade
|
||||
;;
|
||||
# Naiveproxy 相关
|
||||
naiveproxy_info)
|
||||
naiveproxy_info
|
||||
;;
|
||||
naiveproxy_local_info)
|
||||
naiveproxy_local_info
|
||||
;;
|
||||
naiveproxy_upgrade)
|
||||
naiveproxy_upgrade
|
||||
;;
|
||||
# Country MMDB 相关
|
||||
country_mmdb_info)
|
||||
country_mmdb_info
|
||||
@@ -1942,7 +1673,7 @@ case "${1:-}" in
|
||||
;;
|
||||
*)
|
||||
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
|
||||
;;
|
||||
esac
|
||||
@@ -1,3 +1,4 @@
|
||||
.deploy/target.env
|
||||
.deploy/.ssh/
|
||||
.private
|
||||
/artifacts/
|
||||
@@ -9,8 +9,11 @@ LUCI_TITLE:=LuCI for Tailscale (NG)
|
||||
LUCI_DEPENDS:=+tailscale
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.1
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=0.1.2
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
- [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
|
||||
|
||||
|
||||
@@ -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
|
||||
#
|
||||
# Extracted with: build/i18n-scan.pl from the openwrt/luci repository
|
||||
# Language: zh_CN / zh-cn
|
||||
# Language: zh_Hans (Simplified Chinese)
|
||||
msgid ""
|
||||
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"
|
||||
"POT-Creation-Date: 2026-08-15 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-08-15 00:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese (Simplified) <zh-cn>\n"
|
||||
"Language: zh_CN\n"
|
||||
"Language-Team: Chinese (Simplified)\n"
|
||||
"Language: zh_Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
+114
@@ -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"
|
||||
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-trafficctl
|
||||
PKG_VERSION:=1.26.4
|
||||
PKG_RELEASE:=22
|
||||
PKG_VERSION:=1.28.0
|
||||
PKG_RELEASE:=23
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
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.
|
||||
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 } }')
|
||||
RATE_LIM=$(echo "$RL_DUMP" | grep -oE '[0-9]+kbit' | head -1 | sed 's/kbit//')
|
||||
RATE_LIM_UP=$(echo "$RL_DUMP" | grep -E 'rl_ratelimit.*_ul' | 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' \
|
||||
| awk "$TCTL_NFT_RATE_AWK"'{ r = tctl_ipt_first_rate(); if (r >= 0) { print r; exit } }')
|
||||
fi
|
||||
[ -z "$RATE_LIM" ] && RATE_LIM=0
|
||||
[ -z "$RATE_LIM_UP" ] && RATE_LIM_UP=0
|
||||
|
||||
@@ -279,12 +279,14 @@ tctl_ratelimit_add() {
|
||||
TCTL_RL_UPLOAD6_OK=0
|
||||
# shellcheck disable=SC2034
|
||||
TCTL_RL_DOWNLOAD6_OK=0
|
||||
# shellcheck disable=SC2046 # deliberate split into the option words
|
||||
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}" \
|
||||
-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 \
|
||||
--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" \
|
||||
-j DROP -m comment --comment "${comment}_ul" 2>/dev/null
|
||||
fi
|
||||
@@ -382,8 +384,50 @@ function tctl_rate_kbit(n, u, b) {
|
||||
# Rounded here, so a reader printing %d agrees with one printing %.0f.
|
||||
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() {
|
||||
if [ "$TCTL_FW" = "nft" ]; then
|
||||
nft list table netdev tm_ratelimit 2>/dev/null
|
||||
|
||||
@@ -115,6 +115,8 @@ BEGIN {
|
||||
print "# TYPE trafficctl_device_blocked gauge"
|
||||
print "# HELP trafficctl_device_limit_kbit Applied throttle in kbit/s (0 = none)."
|
||||
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 "# TYPE trafficctl_device_wifi_blocked gauge"
|
||||
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_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")
|
||||
# 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.
|
||||
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")
|
||||
|
||||
@@ -154,13 +154,15 @@ pfw_add_limit() {
|
||||
fi
|
||||
else
|
||||
if [ "$scope" = "input" ]; then
|
||||
# shellcheck disable=SC2046 # deliberate split into the option words
|
||||
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")" \
|
||||
-j DROP -m comment --comment "$cmt" 2>/dev/null
|
||||
else
|
||||
# shellcheck disable=SC2046 # deliberate split into the option words
|
||||
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")" \
|
||||
-j DROP -m comment --comment "$cmt" 2>/dev/null
|
||||
fi
|
||||
@@ -372,7 +374,7 @@ do_list() {
|
||||
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 } }')
|
||||
[ -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"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -65,7 +65,7 @@ else
|
||||
# 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
|
||||
# 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
|
||||
bytes = $2
|
||||
@@ -77,14 +77,9 @@ else
|
||||
}
|
||||
if (i == 9) ip = $i
|
||||
}
|
||||
# extract rate from hashlimit-above
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i ~ /^[0-9]+kbit/) {
|
||||
gsub(/kbit.*/, "", $i)
|
||||
rate = $i
|
||||
break
|
||||
}
|
||||
}
|
||||
# the "above X" ceiling, in iptables units (see tctl_ipt_rate_kbit)
|
||||
rate = tctl_ipt_first_rate()
|
||||
if (rate < 0) rate = 0
|
||||
if (ip != "" && ip != "0.0.0.0/0") {
|
||||
if (!(ip in seen)) { seen[ip] = 1; order[++n] = ip }
|
||||
rate_of[ip] = rate
|
||||
|
||||
@@ -343,7 +343,7 @@ lookup_rate_limit() {
|
||||
# way; the upload one is the only "_ul" name.
|
||||
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 } }' \
|
||||
| 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
|
||||
echo "${r:-0}"
|
||||
}
|
||||
|
||||
@@ -450,13 +450,34 @@ build_device_keyboard() {
|
||||
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() {
|
||||
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")
|
||||
wifi_blocked=$(get_device_field "$devices" "$ip" "wifi_blocked")
|
||||
rl_kbit=$(get_device_field "$devices" "$ip" "rate_limit_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")
|
||||
|
||||
local kb='{"inline_keyboard":['
|
||||
@@ -487,7 +508,7 @@ build_action_keyboard() {
|
||||
# limiter
|
||||
if [ "$TG_BTN_LIMIT" = "1" ]; 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
|
||||
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\"}],"
|
||||
@@ -497,7 +518,7 @@ build_action_keyboard() {
|
||||
# shaper
|
||||
if [ "$TG_BTN_SHAPE" = "1" ]; 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
|
||||
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\"}],"
|
||||
@@ -556,7 +577,7 @@ handle_devices_edit() {
|
||||
}
|
||||
|
||||
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=""
|
||||
devices=$(get_devices)
|
||||
[ -z "$devices" ] || [ "$devices" = "[]" ] && { tg_send "No active devices"; return; }
|
||||
@@ -566,13 +587,15 @@ handle_status() {
|
||||
wifi_blocked=$(get_device_field "$devices" "$ip" "wifi_blocked")
|
||||
rl_kbit=$(get_device_field "$devices" "$ip" "rate_limit_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")
|
||||
|
||||
local flags=""
|
||||
[ "$blocked" = "true" ] || [ "$blocked" = "1" ] && flags="${flags} 🚫inet"
|
||||
[ "$wifi_blocked" = "true" ] || [ "$wifi_blocked" = "1" ] && flags="${flags} 📵wifi"
|
||||
[ "${rl_kbit:-0}" -gt 0 ] 2>/dev/null && flags="${flags} ⚡${rl_kbit}k"
|
||||
[ "${shape_kbit:-0}" -gt 0 ] 2>/dev/null && flags="${flags} 🔧${shape_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} 🔧$(tg_ceiling_short "$shape_kbit" "$shape_up")"
|
||||
|
||||
[ -n "$flags" ] && result="${result}${name:-?} (${ip}):${flags}\n"
|
||||
done
|
||||
@@ -616,17 +639,19 @@ handle_callback() {
|
||||
invalidate_cache
|
||||
devices=$(get_devices)
|
||||
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")
|
||||
wifi_blocked=$(get_device_field "$devices" "$ip" "wifi_blocked")
|
||||
rl_kbit=$(get_device_field "$devices" "$ip" "rate_limit_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=""
|
||||
[ "$blocked" = "true" ] || [ "$blocked" = "1" ] && status_line="${status_line}🚫 Internet 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"
|
||||
[ "${shape_kbit:-0}" -gt 0 ] 2>/dev/null && status_line="${status_line}🔧 Shaper: ${shape_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: $(tg_ceiling "$shape_kbit" "$shape_up")\n"
|
||||
[ -z "$status_line" ] && status_line="✅ No restrictions\n"
|
||||
|
||||
local text
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-vantage
|
||||
PKG_VERSION:=1.0.16
|
||||
PKG_RELEASE:=30
|
||||
PKG_VERSION:=1.0.17
|
||||
PKG_RELEASE:=31
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
LUCI_TITLE:=Vantage dashboard
|
||||
|
||||
@@ -584,16 +584,21 @@ h2, h3{
|
||||
--vt-oc: var(--vt-ok);
|
||||
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 {
|
||||
content: ""; position: absolute; inset: 0; border-radius: 50%;
|
||||
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; }
|
||||
/* 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-warn { --vt-oc: var(--vt-warn); }
|
||||
.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-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-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)); }
|
||||
/* the beat is a ring that scales and fades (compositor only; an animated
|
||||
box-shadow repainted every frame), only while reconnecting: a live page
|
||||
keeps a still dot. It sits behind the dot (z-index -1 in the pill's
|
||||
box-shadow repainted every frame): slowly while live, at the full rate
|
||||
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
|
||||
unless the animation runs: nothing shows without motion. */
|
||||
.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-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; }
|
||||
@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 {
|
||||
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 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 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 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 running() { return !!scene && !still && !hidden && focused && inView && usable(); }
|
||||
function running() { return !!scene && !still && !paused && !hidden && focused && inView && usable(); }
|
||||
function halt() {
|
||||
if (raf) cancelAnimationFrame(raf);
|
||||
if (timer) clearTimeout(timer);
|
||||
@@ -407,6 +407,8 @@ function mount(host, band, style) {
|
||||
run();
|
||||
},
|
||||
running: running,
|
||||
/* the page's refresh is paused: the last frame stays */
|
||||
pause: function(on) { paused = !!on; run(); },
|
||||
stop: function() {
|
||||
halt(); scene = null;
|
||||
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 SWITCH_MS = 30 * 1000; /* LuCI runs swconfig for each switch's link state: at most this often */
|
||||
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 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);
|
||||
}
|
||||
|
||||
/* 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() {
|
||||
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) {
|
||||
@@ -901,7 +911,7 @@ return view.extend({
|
||||
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); }); });
|
||||
});
|
||||
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);
|
||||
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.refreshFast, this), FAST_S);
|
||||
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();
|
||||
});
|
||||
/* 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));
|
||||
window.setInterval(function() { self.paintLive(); }, 1000);
|
||||
if (window.ResizeObserver) {
|
||||
@@ -1668,6 +1682,9 @@ return view.extend({
|
||||
paintHead: function() {
|
||||
var s = this.st.summary, clients = this.m.clients.length;
|
||||
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.subline, this.identity(this.m.device).concat(
|
||||
this.m.wired ? [] : [ clients === 1 ? _('1 client') : _('%d clients').format(clients) ]).join(' · '));
|
||||
@@ -1688,15 +1705,36 @@ return view.extend({
|
||||
paintLive: function() {
|
||||
if (!this.el || !this.el.live) return;
|
||||
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 { state = 'ok'; label = _('Live') + ' · ' + fmt.ago(Date.now() - st.lastOk); }
|
||||
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
|
||||
live: then this pill shows only when it is not */
|
||||
this.el.live.hidden = state === 'ok' && vantageTheme();
|
||||
live, or that its refresh is paused: then this pill shows only
|
||||
when neither */
|
||||
this.el.live.hidden = (state === 'ok' || stopped) && vantageTheme();
|
||||
this.el.live.setAttribute('title', 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 */
|
||||
@@ -2302,6 +2340,7 @@ return view.extend({
|
||||
/* 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' });
|
||||
a.playbackRate = rate;
|
||||
if (this.motionOn === false) a.pause();
|
||||
g.dots.push(dot);
|
||||
g.anims.push(a);
|
||||
}
|
||||
@@ -2428,7 +2467,8 @@ return view.extend({
|
||||
}));
|
||||
patch(el.fw, [
|
||||
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
@@ -13710,3 +13710,18 @@ msgstr ""
|
||||
|
||||
msgid "Wireless scan failed: %s"
|
||||
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 ""
|
||||
@@ -14238,3 +14238,18 @@ msgstr "启用 CSA"
|
||||
|
||||
msgid "Announce channel switches to clients. Automatic channel selection remains available when disabled."
|
||||
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 "导出分区"
|
||||
@@ -1,17 +1,21 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=Fluent dashboard for LuCI
|
||||
LUCI_DEPENDS:=+luci-base +libiwinfo
|
||||
LUCI_DESCRIPTION:=A fixed-card, theme-independent LuCI status dashboard with system, network, wireless, DHCP lease, and wireless client information.
|
||||
LUCI_DEPENDS:=+luci-base +libiwinfo +rpcd-mod-ucode +ucode-mod-fs +ucode-mod-uci
|
||||
LUCI_DESCRIPTION:=A theme-independent configurable dashboard workbench with shared layouts and live system, network, wireless and client widgets.
|
||||
LUCI_MAINTAINER:=LazuliKao
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
PKG_NAME:=luci-mod-fluentdashboard
|
||||
PKG_VERSION:=1.0.9
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_BUILD_DEPENDS:=iwinfo
|
||||
|
||||
define Package/luci-mod-fluentdashboard/conffiles
|
||||
/etc/config/fluentdashboard
|
||||
endef
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
+394
-3
@@ -1,7 +1,7 @@
|
||||
.fluent-dashboard {
|
||||
--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-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)));
|
||||
color: var(--fluent-text, CanvasText);
|
||||
container-type: inline-size;
|
||||
@@ -12,6 +12,7 @@
|
||||
}
|
||||
|
||||
.fluent-dashboard__header {
|
||||
z-index: 0;
|
||||
color: highlighttext;
|
||||
background: linear-gradient(120deg, Highlight, color-mix(in srgb, Highlight 68%, CanvasText));
|
||||
border-radius: .75rem;
|
||||
@@ -21,9 +22,8 @@
|
||||
margin-block-end: 1rem;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fluent-dashboard__header h1, .fluent-dashboard__header p {
|
||||
@@ -239,3 +239,394 @@
|
||||
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%;
|
||||
}
|
||||
+8914
-193
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" ],
|
||||
"luci": [ "getVersion" ],
|
||||
"luci-rpc": [ "getDHCPLeases" ],
|
||||
"luci.fluentdashboard": [ "load" ],
|
||||
"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) =>
|
||||
L.resolveDefault(callIwinfoInfoCompat(name), null).then((info) => [ name, info ])
|
||||
)).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 = {};
|
||||
for (const [ name, info ] of entries) {
|
||||
if (info?.hardware?.id?.[0] != 0x14c3 || !/^phy\d+$/.test(info?.phy || ''))
|
||||
@@ -948,13 +938,13 @@ function getFrequencyListBand(entry, hwmode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeIwinfoBitRate(rate) {
|
||||
function normalizeIwinfoBitRate(rate, inKbit = false) {
|
||||
rate = +rate;
|
||||
|
||||
if (isNaN(rate) || rate <= 0)
|
||||
return null;
|
||||
|
||||
return (rate > 100000) ? (rate / 1000) : rate;
|
||||
return (inKbit || rate > 100000) ? (rate / 1000) : rate;
|
||||
}
|
||||
|
||||
function getIwinfoInfoCandidates(radioNet) {
|
||||
@@ -982,9 +972,10 @@ function getIwinfoInfoCandidates(radioNet) {
|
||||
}
|
||||
|
||||
function getDisplayIwinfoBitRate(radioNet) {
|
||||
const inKbit = isQcaWifiHwtype(uci.get('wireless', radioNet.getWifiDeviceName(), 'type'));
|
||||
for (const candidate of getIwinfoInfoCandidates(radioNet)) {
|
||||
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)
|
||||
return rate;
|
||||
@@ -1022,21 +1013,16 @@ function getDisplayTheoreticalBitRate(radioNet) {
|
||||
return null;
|
||||
|
||||
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)
|
||||
return rate;
|
||||
}
|
||||
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) {
|
||||
if (isQcaWifiHwtype(uci.get('wireless', radioNet.getWifiDeviceName(), 'type')) &&
|
||||
radioNet.getMode() == 'ap')
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"/proc/sys/net/ipv6/conf/*/mtu": [ "read" ],
|
||||
"/proc/sys/net/ipv6/conf/*/hop_limit": [ "read" ],
|
||||
"/usr/sbin/iw": [ "exec" ],
|
||||
"/usr/sbin/cfg80211tool ath* get_maxrate": [ "exec" ],
|
||||
"/usr/libexec/luci-peeraddr": [ "exec" ],
|
||||
"/usr/sbin/wlanconfig": [ "exec" ],
|
||||
"/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: {} };
|
||||
|
||||
return view.extend({
|
||||
@@ -145,7 +169,8 @@ return view.extend({
|
||||
fs.trimmed('/proc/sys/kernel/hostname'),
|
||||
fs.trimmed('/proc/mtd'),
|
||||
fs.trimmed('/proc/partitions'),
|
||||
fs.trimmed('/proc/mounts')
|
||||
fs.trimmed('/proc/mounts'),
|
||||
loadEmmcPartitions()
|
||||
];
|
||||
|
||||
return Promise.all(tasks);
|
||||
@@ -243,18 +268,19 @@ return view.extend({
|
||||
.finally(function() { btn.firstChild.data = _('Upload archive...') });
|
||||
},
|
||||
|
||||
handleBlock(hostname, ev) {
|
||||
const mtdblock = dom.parent(ev.target, '.cbi-section').querySelector('[data-name="mtdselect"] select');
|
||||
const mtdnumber = mtdblock.value;
|
||||
const mtdname = mtdblock.selectedOptions[0].text.replace(/([^a-zA-Z0-9]+)/g, '-');
|
||||
handleBlock(hostname, selector, ev) {
|
||||
const block = dom.parent(ev.target, '.cbi-section').querySelector('[data-name="%s"] select'.format(selector));
|
||||
const device = selector == 'mtdselect' ? 'mtdblock' + block.value : block.value;
|
||||
const name = block.selectedOptions[0].text.replace(/([^a-zA-Z0-9]+)/g, '-');
|
||||
const id = selector == 'mtdselect' ? 'mtd' + block.value : device;
|
||||
const form = E('form', {
|
||||
'method': 'post',
|
||||
'action': L.env.cgi_base + '/cgi-download',
|
||||
'enctype': 'application/x-www-form-urlencoded'
|
||||
}, [
|
||||
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': 'filename', 'value': '%s.mtd%d.%s.bin'.format(hostname, mtdnumber, mtdname) })
|
||||
E('input', { 'type': 'hidden', 'name': 'path', 'value': '/dev/' + device }),
|
||||
E('input', { 'type': 'hidden', 'name': 'filename', 'value': '%s.%s.%s.bin'.format(hostname, id, name) })
|
||||
]);
|
||||
|
||||
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_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || (procmounts.match("overlayfs:/overlay / ") != null);
|
||||
const storage_size = findStorageSize(procmtd, procpart, procmounts);
|
||||
@@ -509,10 +535,24 @@ return view.extend({
|
||||
o = ss.option(form.Button, 'mtddownload', _('Download mtdblock'));
|
||||
o.inputstyle = 'action important';
|
||||
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'),
|
||||
has_sysupgrade
|
||||
? _('Upload a sysupgrade-compatible image here to replace the running firmware.')
|
||||
|
||||
@@ -152,6 +152,12 @@
|
||||
"cgi-io": [ "backup", "download" ],
|
||||
"file": {
|
||||
"/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" ],
|
||||
"/lib/upgrade/platform.sh": [ "list" ],
|
||||
"/proc/[0-9]*/mounts": [ "read" ],
|
||||
@@ -162,7 +168,7 @@
|
||||
"/sbin/sysupgrade --list-backup": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "exec", "read", "stat" ]
|
||||
"file": [ "exec", "list", "read", "stat" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
|
||||
@@ -9,7 +9,7 @@ LUCI_TITLE:=Design Theme
|
||||
LUCI_DEPENDS:=
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_VERSION:=7.1
|
||||
PKG_RELEASE:=12
|
||||
PKG_RELEASE:=13
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
CONFIG_LUCI_CSSTIDY:=
|
||||
|
||||
@@ -4803,6 +4803,444 @@ body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"
|
||||
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) {
|
||||
|
||||
code, pre {
|
||||
|
||||
@@ -66,10 +66,6 @@
|
||||
}
|
||||
|
||||
function getOverviewPortColumns(width, cardCount) {
|
||||
if (width <= 480) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (width <= 768) {
|
||||
return Math.min(cardCount, 2);
|
||||
}
|
||||
@@ -80,6 +76,7 @@
|
||||
function syncOverviewPortStatus() {
|
||||
var view = document.getElementById('view');
|
||||
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 iconMap = {
|
||||
'alias': 'ethernet',
|
||||
@@ -148,7 +145,7 @@
|
||||
grid.style &&
|
||||
grid.style.display === 'grid' &&
|
||||
grid.style.gridTemplateColumns.indexOf('minmax(70px, 1fr)') !== -1 &&
|
||||
grid.querySelector('img[src*="/luci-static/resources/icons/port_"]');
|
||||
grid.querySelector(portIconSelector);
|
||||
|
||||
if (!isPortGrid) {
|
||||
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 slot = card ? card.closest('.port-status-slot') : 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() {
|
||||
if (document.body.getAttribute('data-page') !== 'admin-network-wireless') {
|
||||
return;
|
||||
}
|
||||
|
||||
syncWirelessIcons();
|
||||
syncWirelessCards();
|
||||
|
||||
new MutationObserver(syncWirelessIcons).observe(document.body, {
|
||||
new MutationObserver(function () {
|
||||
syncWirelessIcons();
|
||||
syncWirelessCards();
|
||||
}).observe(document.body, {
|
||||
childList: 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() {
|
||||
applySafeAreaFix();
|
||||
initIndicators();
|
||||
initOverviewPortStatus();
|
||||
initNetworkInterfaceIcons();
|
||||
initWirelessIcons();
|
||||
initResponsiveTables();
|
||||
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_openclash_icon = access('/etc/config/openclash');
|
||||
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');
|
||||
-%}
|
||||
|
||||
@@ -2,11 +2,11 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-theme-fluent
|
||||
PKG_VERSION:=1.0.9
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_TITLE:=Fluent Theme for LuCI
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_DEPENDS:=+luci-base +flock
|
||||
LUCI_MINIFY_CSS:=0
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/fluent
|
||||
|
||||
@@ -6676,6 +6676,52 @@ cbi-dropdown[open] > ul.dropdown:not(.preview) > li .create-item-input, .cbi-dro
|
||||
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 {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
@@ -6841,12 +6887,12 @@ cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:dis
|
||||
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;
|
||||
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=);
|
||||
content: "" !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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
background: none !important;
|
||||
padding: 0 !important;
|
||||
@@ -10048,6 +10098,17 @@ html[dir="rtl"] .fluent-menu-editor__category-header:after, body[data-fluent-dir
|
||||
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 {
|
||||
color: var(--fluent-text);
|
||||
margin: 8px 0 4px;
|
||||
@@ -10124,23 +10185,6 @@ html[dir="rtl"] .fluent-menu-editor__category-header:after, body[data-fluent-dir
|
||||
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 {
|
||||
border-radius: var(--fluent-radius-sm);
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
align-items: center;
|
||||
gap: var(--fluent-space-xs);
|
||||
|
||||
@@ -1670,28 +1670,32 @@ function setupSelectionPause() {
|
||||
;// CONCATENATED MODULE: ./web/resources/utils/select-dropdown.tsx
|
||||
|
||||
|
||||
let select_dropdown_r = !1;
|
||||
function select_dropdown_s() {
|
||||
let select_dropdown_l = !1, select_dropdown_a = !1, select_dropdown_s = new WeakMap(), select_dropdown_d = 0;
|
||||
function select_dropdown_c() {
|
||||
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")) {
|
||||
let t = e.previousElementSibling;
|
||||
t instanceof HTMLSelectElement && select_dropdown_f(e, t);
|
||||
} else select_dropdown_a(e);
|
||||
t instanceof HTMLSelectElement && select_dropdown_h(e, t);
|
||||
} else select_dropdown_u(e);
|
||||
});
|
||||
}
|
||||
function select_dropdown_a(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);
|
||||
e.setAttribute("data-open-direction", a ? "up" : "down"), e.style.setProperty("--fluent-dropdown-max-height", `${Math.min(select_dropdown_c, s, d)}px`);
|
||||
function select_dropdown_u(e) {
|
||||
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", s ? "up" : "down"), e.style.setProperty("--fluent-dropdown-max-height", `${Math.min(select_dropdown_b, a, d)}px`);
|
||||
}
|
||||
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)=>{
|
||||
if ("attributes" === e.type && "open" === e.attributeName) {
|
||||
let t = e.target;
|
||||
if (t.hasAttribute("open")) if (t.classList.contains("fluent-custom-select")) {
|
||||
let e = t.previousElementSibling;
|
||||
e instanceof HTMLSelectElement && select_dropdown_f(t, e);
|
||||
} else ("CBI-DROPDOWN" === t.tagName || t.classList.contains("cbi-dropdown")) && select_dropdown_a(t);
|
||||
e instanceof HTMLSelectElement && select_dropdown_h(t, e);
|
||||
} else ("CBI-DROPDOWN" === t.tagName || t.classList.contains("cbi-dropdown")) && select_dropdown_u(t);
|
||||
}
|
||||
});
|
||||
}).observe(document.body, {
|
||||
@@ -1700,55 +1704,59 @@ function setupFluentSelects() {
|
||||
"open"
|
||||
],
|
||||
subtree: !0
|
||||
})), "0" === document.body.getAttribute("data-theme-custom-select") || (document.querySelectorAll("select").forEach((e)=>{
|
||||
select_dropdown_h(e);
|
||||
})), "0" === document.body.getAttribute("data-theme-custom-select") || select_dropdown_a || (select_dropdown_a = !0, document.querySelectorAll("select").forEach((e)=>{
|
||||
select_dropdown_v(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)=>{
|
||||
let t = new Set();
|
||||
e.forEach((e)=>{
|
||||
e.addedNodes.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)=>{
|
||||
select_dropdown_h(e);
|
||||
e.removedNodes.forEach((e)=>{
|
||||
e instanceof HTMLSelectElement ? t.add(e) : e instanceof Element && e.querySelectorAll("select").forEach((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.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, {
|
||||
childList: !0,
|
||||
subtree: !0
|
||||
}));
|
||||
}
|
||||
function select_dropdown_d(e) {
|
||||
function select_dropdown_p(e) {
|
||||
let t;
|
||||
(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)");
|
||||
n && !n.classList.contains("dropdown") && n.classList.add("dropdown");
|
||||
}
|
||||
let select_dropdown_c = 350, select_dropdown_u = 8, select_dropdown_p = 2;
|
||||
function select_dropdown_b(e, t) {
|
||||
let n = t.offsetTop, o = n + t.offsetHeight, l = e.scrollTop, i = l + e.clientHeight;
|
||||
n < l ? e.scrollTop = n : o > i && (e.scrollTop = o - e.clientHeight);
|
||||
}
|
||||
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);
|
||||
let select_dropdown_b = 350, select_dropdown_m = 8, select_dropdown_f = 2;
|
||||
function select_dropdown_h(e, t) {
|
||||
let l = e.getBoundingClientRect(), a = getEffectiveDocumentDirection(), s = getViewportInlineSize(), d = window.innerHeight, c = d - l.bottom, u = l.top;
|
||||
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);
|
||||
e.setAttribute("data-open-direction", h ? "up" : "down"), e.setAttribute("data-fluent-floating", "modal"), setInlineCssCustomProperties(e.style, {
|
||||
inlineStart: y,
|
||||
inlineSize: w
|
||||
}, s, a, {
|
||||
inlineStart: w,
|
||||
inlineSize: v
|
||||
}, a, s, {
|
||||
inlineStart: "--fluent-dropdown-left",
|
||||
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) {
|
||||
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");
|
||||
function select_dropdown_y(e) {
|
||||
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");
|
||||
t instanceof HTMLElement && "function" == typeof t.hidePopover && t.matches(":popover-open") && t.hidePopover();
|
||||
}
|
||||
function select_dropdown_h(n) {
|
||||
if (n?.tagName !== "SELECT" || "true" === n.getAttribute("data-fluent-transformed") || n.closest(".cbi-dropdown") || n.multiple || function(e) {
|
||||
function select_dropdown_v(n) {
|
||||
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") || "";
|
||||
if (/\bdisplay\s*:\s*none/i.test(t) || "none" === e.style.display) return !0;
|
||||
if (!e.isConnected) return !1;
|
||||
@@ -1765,163 +1773,207 @@ function select_dropdown_h(n) {
|
||||
t = t.parentElement;
|
||||
}
|
||||
if (!n) return !0;
|
||||
let o = e.cloneNode(!1);
|
||||
o.style.display = "", document.body.appendChild(o);
|
||||
let l = window.getComputedStyle(o), i = "none" === l.display;
|
||||
return document.body.removeChild(o), i;
|
||||
let r = e.cloneNode(!1);
|
||||
r.style.display = "", document.body.appendChild(r);
|
||||
let i = window.getComputedStyle(r), o = "none" === i.display;
|
||||
return document.body.removeChild(r), o;
|
||||
}
|
||||
} catch (e) {}
|
||||
return !1;
|
||||
}(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");
|
||||
let l = jsx("li", {});
|
||||
l.setAttribute("selected", "");
|
||||
let i = jsx("ul", {
|
||||
let a = jsx("ul", {
|
||||
class: "dropdown"
|
||||
}), r = jsx("span", {
|
||||
});
|
||||
a.id = `fluent-select-list-${++select_dropdown_d}`, a.setAttribute("role", "listbox");
|
||||
let c = jsx("span", {
|
||||
class: "open"
|
||||
}), s = jsxs("div", {
|
||||
}), u = jsxs("div", {
|
||||
class: "fluent-custom-select",
|
||||
children: [
|
||||
jsx("ul", {
|
||||
children: l
|
||||
}),
|
||||
r,
|
||||
i
|
||||
c,
|
||||
a
|
||||
]
|
||||
});
|
||||
if (s.setAttribute("tabindex", "0"), o) {
|
||||
let e = o.replace(/\bdisplay\s*:\s*[^;]+(;|$)/gi, "").trim();
|
||||
e && s.setAttribute("style", e);
|
||||
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 = r.replace(/\bdisplay\s*:\s*[^;]+(;|$)/gi, "").trim();
|
||||
e && u.setAttribute("style", e);
|
||||
}
|
||||
n.disabled && (s.setAttribute("disabled", ""), s.removeAttribute("tabindex"));
|
||||
let a = ()=>{
|
||||
i.innerHTML = "";
|
||||
let t = "", o = !1;
|
||||
if (Array.from(n.options).forEach((n)=>{
|
||||
let l = jsx("li", {
|
||||
children: n.text
|
||||
});
|
||||
l.setAttribute("data-value", n.value), n.selected && (l.setAttribute("selected", ""), t = n.text, o = !0), n.disabled && l.setAttribute("disabled", ""), i.appendChild(l);
|
||||
}), !o && n.options.length > 0) {
|
||||
let e = n.options[n.selectedIndex >= 0 ? n.selectedIndex : 0];
|
||||
t = e.text;
|
||||
let o = i.querySelector(`li[data-value="${e.value}"]`);
|
||||
o?.setAttribute("selected", "");
|
||||
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)=>{
|
||||
if ((b = e && p.has(e) && E(e) ? e : null) && u.hasAttribute("open")) {
|
||||
let e = p.get(b);
|
||||
if (e) {
|
||||
let t, n, r, i;
|
||||
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);
|
||||
}
|
||||
} else u.removeAttribute("aria-activedescendant");
|
||||
}, S = ()=>{
|
||||
let e = n.matches(":disabled");
|
||||
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");
|
||||
let t = g.map((e)=>e.id).filter(Boolean), r = n.getAttribute("aria-labelledby");
|
||||
if (r || t.length) u.setAttribute("aria-labelledby", r || t.join(" ")), u.removeAttribute("aria-label");
|
||||
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)=>{
|
||||
if (s.hasAttribute("disabled")) return;
|
||||
let t = e.target.closest("ul.dropdown > li");
|
||||
if (t) {
|
||||
let o = t.getAttribute("data-value");
|
||||
null === o || t.hasAttribute("disabled") || (n.value = o, n.dispatchEvent(new Event("change", {
|
||||
bubbles: !0
|
||||
})), n.dispatchEvent(new Event("input", {
|
||||
bubbles: !0
|
||||
}))), select_dropdown_m(s), e.stopPropagation();
|
||||
M(), n.parentNode?.insertBefore(u, n.nextSibling), u.addEventListener("click", (e)=>{
|
||||
if (u.hasAttribute("disabled")) return;
|
||||
let t = e.target.closest('[role="option"]');
|
||||
if (t && a.contains(t)) {
|
||||
let n = Array.from(p).find(([, e])=>e === t)?.[0];
|
||||
n && E(n) && k(n), select_dropdown_y(u), e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (s.hasAttribute("open")) select_dropdown_m(s);
|
||||
else {
|
||||
document.querySelectorAll("cbi-dropdown[open], .cbi-dropdown[open], .fluent-custom-select[open]").forEach((e)=>{
|
||||
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"));
|
||||
}), select_dropdown_f(s, n), s.setAttribute("open", ""), s.closest(".cbi-value-field, .cbi-value")?.classList.add("cbi-dropdown-open");
|
||||
let e = i.querySelector("li[selected]");
|
||||
e && select_dropdown_b(i, e);
|
||||
if (!e.target.closest('[role="group"]')) {
|
||||
if (u.hasAttribute("open")) select_dropdown_y(u);
|
||||
else {
|
||||
document.querySelectorAll("cbi-dropdown[open], .cbi-dropdown[open], .fluent-custom-select[open]").forEach((e)=>{
|
||||
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"));
|
||||
}), 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);
|
||||
let e = ()=>{
|
||||
if (!u.hasAttribute("open")) {
|
||||
m = 0;
|
||||
return;
|
||||
}
|
||||
S(), m = requestAnimationFrame(e);
|
||||
};
|
||||
m || (m = requestAnimationFrame(e));
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
let d = (e)=>{
|
||||
!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"));
|
||||
}), u.addEventListener("keydown", (e)=>{
|
||||
if (u.hasAttribute("disabled")) return;
|
||||
let t = u.hasAttribute("open"), n = L(), r = b ? n.indexOf(b) : -1;
|
||||
switch(e.key){
|
||||
case "Enter":
|
||||
case " ":
|
||||
if (e.preventDefault(), t) {
|
||||
let e = o[l];
|
||||
e && e.click();
|
||||
} else s.click();
|
||||
e.preventDefault(), t ? b && (k(b), select_dropdown_y(u)) : u.click();
|
||||
break;
|
||||
case "Escape":
|
||||
t && (e.preventDefault(), select_dropdown_m(s));
|
||||
t && (e.preventDefault(), select_dropdown_y(u));
|
||||
break;
|
||||
case "ArrowDown":
|
||||
if (e.preventDefault(), t) {
|
||||
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();
|
||||
e.preventDefault(), t ? n.length > 0 && x(n[(r + 1) % n.length]) : u.click();
|
||||
break;
|
||||
case "ArrowUp":
|
||||
if (e.preventDefault(), t) {
|
||||
if (o.length > 0) {
|
||||
let e = (l - 1 + o.length) % 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();
|
||||
e.preventDefault(), t ? n.length > 0 && x(n[(r - 1 + n.length) % n.length]) : u.click();
|
||||
break;
|
||||
case "Home":
|
||||
case "End":
|
||||
e.preventDefault(), t || u.click(), x("Home" === e.key ? n[0] || null : n[n.length - 1] || null);
|
||||
break;
|
||||
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(()=>{
|
||||
a();
|
||||
let H = ()=>{
|
||||
S(), x(null);
|
||||
};
|
||||
n.addEventListener("change", H), n.addEventListener("input", H), u.addEventListener("focus", ()=>{
|
||||
P(), M();
|
||||
});
|
||||
c.observe(n, {
|
||||
childList: !0
|
||||
let N = new MutationObserver((e)=>{
|
||||
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)=>{
|
||||
e.forEach((e)=>{
|
||||
"disabled" === e.attributeName && (n.disabled ? (s.setAttribute("disabled", ""), s.removeAttribute("tabindex"), select_dropdown_m(s)) : (s.removeAttribute("disabled"), s.setAttribute("tabindex", "0")));
|
||||
});
|
||||
});
|
||||
u.observe(n, {
|
||||
N.observe(n, {
|
||||
childList: !0,
|
||||
subtree: !0,
|
||||
characterData: !0,
|
||||
attributes: !0,
|
||||
attributeFilter: [
|
||||
"disabled"
|
||||
"disabled",
|
||||
"hidden",
|
||||
"label",
|
||||
"selected",
|
||||
"value",
|
||||
"required",
|
||||
"aria-label",
|
||||
"aria-labelledby",
|
||||
"id",
|
||||
"form"
|
||||
]
|
||||
});
|
||||
let p = new MutationObserver((e)=>{
|
||||
e.forEach((e)=>{
|
||||
e.removedNodes.forEach((e)=>{
|
||||
(e === n || e.contains?.(n)) && (document.removeEventListener("click", d, !0), c.disconnect(), u.disconnect(), p.disconnect(), s.remove());
|
||||
});
|
||||
});
|
||||
});
|
||||
n.parentNode && p.observe(n.parentNode, {
|
||||
childList: !0
|
||||
}), P(), select_dropdown_s.set(n, {
|
||||
dropdown: u,
|
||||
refreshAncestors: P,
|
||||
dispose: ()=>{
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -93,169 +93,81 @@ function jsxDEV(t, n) {
|
||||
|
||||
|
||||
;// CONCATENATED MODULE: ./web/resources/utils/update.ts
|
||||
let update_t = L.rpc;
|
||||
const callGetVersion = update_t.declare({
|
||||
const callGetVersion = L.rpc.declare({
|
||||
object: "luci.fluent",
|
||||
method: "get_version"
|
||||
});
|
||||
const callStartDownload = update_t.declare({
|
||||
const callStartUpdate = L.rpc.declare({
|
||||
object: "luci.fluent",
|
||||
method: "start_download",
|
||||
method: "start_update",
|
||||
params: [
|
||||
"url",
|
||||
"i18n_url",
|
||||
"i18n_urls"
|
||||
"manifest"
|
||||
]
|
||||
});
|
||||
const callCheckDownload = update_t.declare({
|
||||
const callUpdateStatus = L.rpc.declare({
|
||||
object: "luci.fluent",
|
||||
method: "check_download"
|
||||
});
|
||||
const callDoInstall = update_t.declare({
|
||||
object: "luci.fluent",
|
||||
method: "do_install",
|
||||
method: "update_status",
|
||||
params: [
|
||||
"hash",
|
||||
"i18n_hash",
|
||||
"i18n_hashes"
|
||||
"job"
|
||||
]
|
||||
});
|
||||
class GitHubAPIError extends Error {
|
||||
status;
|
||||
constructor(t, e){
|
||||
super(t), this.name = "GitHubAPIError", this.status = e;
|
||||
super(t), this.status = e, this.name = "GitHubAPIError";
|
||||
}
|
||||
}
|
||||
function matchI18nAsset(t, e, l) {
|
||||
let s = e.toLowerCase().trim();
|
||||
for (let e of t){
|
||||
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;
|
||||
function matchI18nAsset(t, e, a) {
|
||||
let r = `luci-i18n-fluent-${e.toLowerCase().trim()}${"apk" === a ? "-" : "_"}`;
|
||||
return t.find((t)=>t.name.toLowerCase().startsWith(r) && t.name.endsWith(`.${a}`)) ?? null;
|
||||
}
|
||||
async function fetchLatestRelease(t, e, l = [], s) {
|
||||
let a = {};
|
||||
s && (a.Authorization = `token ${s}`);
|
||||
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", {
|
||||
headers: a
|
||||
}), n = await r.json();
|
||||
if (!r.ok) {
|
||||
let t = n?.message ? `: ${n.message}` : "";
|
||||
throw new GitHubAPIError(`GitHub API returned status ${r.status}${t}`, r.status);
|
||||
}
|
||||
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);
|
||||
}
|
||||
for (let t of l){
|
||||
let l = matchI18nAsset(i, t, e);
|
||||
l && !u.includes(l) && u.push(l);
|
||||
}
|
||||
function updateManifest(t, e = "") {
|
||||
return JSON.stringify({
|
||||
packages: t.map((t)=>{
|
||||
let a = t.digest ?? "";
|
||||
if (!/^sha256:[a-f\d]{64}$/i.test(a)) throw Error(`Missing SHA-256 digest: ${t.name}`);
|
||||
if (!t.browser_download_url.startsWith("https://")) throw Error(`Invalid package URL: ${t.name}`);
|
||||
return {
|
||||
url: e + t.browser_download_url,
|
||||
sha256: a.slice(7).toLowerCase()
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
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"}`, {
|
||||
headers: r ? {
|
||||
Authorization: `Bearer ${r}`
|
||||
} : {},
|
||||
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 {
|
||||
tag_name: String(n.tag_name || ""),
|
||||
published_at: String(n.published_at || ""),
|
||||
body: String(n.body || ""),
|
||||
html_url: String(n.html_url || ""),
|
||||
package_asset: o,
|
||||
tag_name: String(n.tag_name ?? ""),
|
||||
published_at: String(n.published_at ?? ""),
|
||||
body: String(n.body ?? ""),
|
||||
package_asset: l,
|
||||
i18n_assets: u
|
||||
};
|
||||
}
|
||||
|
||||
;// 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({
|
||||
renderWidget: (l, u, p)=>{
|
||||
let h = "1.0.1", b = "ipk", f = [], g = jsxs("div", {
|
||||
class: "fluent-about-logo",
|
||||
children: [
|
||||
jsx("img", {
|
||||
src: `${L.media()}/img/fluent.svg`,
|
||||
alt: "Fluent Theme Logo"
|
||||
}),
|
||||
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", {
|
||||
let about_d = L.form.DummyValue.extend({
|
||||
renderWidget: ()=>{
|
||||
let d, c = jsx("span", {
|
||||
children: "\u2026"
|
||||
}), u = jsx("span", {
|
||||
children: "\u2026"
|
||||
}), h = jsxs("select", {
|
||||
id: "fluent-update-channel",
|
||||
class: "cbi-input-select",
|
||||
id: "update-channel-select",
|
||||
children: [
|
||||
jsx("option", {
|
||||
value: "stable",
|
||||
@@ -266,284 +178,238 @@ let about_u = about_l.DummyValue.extend({
|
||||
children: _("Nightly Channel (Prerelease)")
|
||||
})
|
||||
]
|
||||
}), w = jsxs("select", {
|
||||
}), b = jsxs("select", {
|
||||
id: "fluent-update-method",
|
||||
class: "cbi-input-select",
|
||||
id: "update-method-select",
|
||||
children: [
|
||||
jsx("option", {
|
||||
value: "backend_official",
|
||||
value: "",
|
||||
children: _("Backend (Official GitHub)")
|
||||
}),
|
||||
jsx("option", {
|
||||
value: "backend_ghproxy",
|
||||
value: "https://ghfast.top/",
|
||||
children: _("Backend (GHProxy Acceleration)")
|
||||
})
|
||||
]
|
||||
}), v = jsx("button", {
|
||||
class: "btn cbi-button cbi-button-action",
|
||||
}), p = jsx("button", {
|
||||
type: "button",
|
||||
class: "btn cbi-button-action",
|
||||
children: _("Check for updates")
|
||||
}), k = jsxs("div", {
|
||||
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", {
|
||||
}), f = jsx("p", {
|
||||
class: "fluent-update-status",
|
||||
style: "display: none"
|
||||
}), T = 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", {
|
||||
role: "status"
|
||||
}), g = jsx("div", {
|
||||
class: "fluent-update-card",
|
||||
style: "display: none"
|
||||
}), F = jsxs("div", {
|
||||
hidden: !0
|
||||
}), 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",
|
||||
children: [
|
||||
g,
|
||||
y,
|
||||
jsxs("div", {
|
||||
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", {
|
||||
class: "fluent-about-divider"
|
||||
}),
|
||||
jsx("h3", {
|
||||
children: _("Software Update")
|
||||
}),
|
||||
k,
|
||||
x,
|
||||
$,
|
||||
C,
|
||||
S
|
||||
jsxs("div", {
|
||||
class: "fluent-update-controls",
|
||||
children: [
|
||||
jsx("label", {
|
||||
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, [
|
||||
document.createTextNode(e)
|
||||
]), x.className = `fluent-update-status status-${t}`, x.style.display = "block";
|
||||
}, A = (e, t, a)=>{
|
||||
C.style.display = "block", S.style.display = "block", T.style.width = `${t}%`, T.className = `fluent-progress-bar__fill fill-${e}`, dom.content(S, [
|
||||
document.createTextNode(`${a} (${t}%)`)
|
||||
]);
|
||||
}, z = ()=>{
|
||||
C.style.display = "none", S.style.display = "none";
|
||||
};
|
||||
(async ()=>{
|
||||
});
|
||||
function y(e, t = "info") {
|
||||
f.textContent = e, f.className = `fluent-update-status status-${t}`;
|
||||
}
|
||||
function k(e) {
|
||||
h.disabled = b.disabled = p.disabled = e, g.querySelectorAll("button").forEach((t)=>{
|
||||
t.disabled = e;
|
||||
});
|
||||
}
|
||||
async function C(e) {
|
||||
let t = e, a = Date.now() + 1200000;
|
||||
k(!0);
|
||||
try {
|
||||
let e = await callGetVersion();
|
||||
h = e.version, b = e.pkg_type, f = e.installed_i18n || (e.i18n_zh_cn_installed ? [
|
||||
"zh-cn"
|
||||
] : []);
|
||||
let t = y.querySelector(".fluent-about-current-version");
|
||||
t && (t.textContent = `v${h}`);
|
||||
let a = y.querySelector(".fluent-about-pkg-type");
|
||||
a && (a.textContent = "apk" === b ? "APK (OpenWrt 25.12+)" : "IPK (OpenWrt 24.10)");
|
||||
for(; v.isConnected;){
|
||||
if (0 !== t.result) throw Error(t.message || _("Unable to read update status."));
|
||||
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;
|
||||
return;
|
||||
}
|
||||
if ("error" === t.stage) return void y(_("Update failed. Check the installation log below."), "error");
|
||||
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) {
|
||||
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)=>{
|
||||
let s = m.value;
|
||||
}
|
||||
async function x(n) {
|
||||
k(!0), g.hidden = !0, y(_("Checking for updates..."));
|
||||
try {
|
||||
let c = await fetchLatestRelease(s, b, f, l);
|
||||
console.log(c), v.disabled = !1;
|
||||
let u = h.replace(/^v/, "").trim(), p = c.tag_name.replace(/^v/, "").trim(), g = L.naturalCompare(p, u) > 0, y = !g && "nightly" !== s;
|
||||
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",
|
||||
let a = await fetchLatestRelease(h.value, d.pkg_type, d.installed_i18n, n), i = a.package_asset;
|
||||
if (!i) throw Error(_("No matching theme package found in this release."));
|
||||
let o = d.version.replace(/^v/, ""), c = a.tag_name.replace(/^v/, ""), u = "stable" === h.value && 0 >= L.naturalCompare(c, o), p = jsx("button", {
|
||||
type: "button",
|
||||
style: "white-space: nowrap;",
|
||||
class: "btn cbi-button-save",
|
||||
children: _("Download and Install")
|
||||
}), T = [
|
||||
});
|
||||
dom.content(g, [
|
||||
jsxs("div", {
|
||||
class: "fluent-update-header",
|
||||
children: [
|
||||
jsx("span", {
|
||||
class: "fluent-update-badge",
|
||||
children: "nightly" === p ? "Nightly" : `v${p}`
|
||||
children: a.tag_name
|
||||
}),
|
||||
jsx("span", {
|
||||
class: "fluent-update-date",
|
||||
children: c.published_at.split("T")[0]
|
||||
children: a.published_at.split("T")[0]
|
||||
})
|
||||
]
|
||||
})
|
||||
];
|
||||
k && T.push(jsx("pre", {
|
||||
class: "fluent-update-changelog",
|
||||
children: k
|
||||
})), y || T.push(jsxs("div", {
|
||||
class: "fluent-update-footer",
|
||||
style: "display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 15px; flex-wrap: wrap;",
|
||||
children: [
|
||||
w,
|
||||
x
|
||||
}),
|
||||
jsx("pre", {
|
||||
class: "fluent-update-changelog",
|
||||
children: a.body
|
||||
}),
|
||||
...u ? [] : [
|
||||
p
|
||||
]
|
||||
})), dom.content($, T), $.style.display = "block", x.addEventListener("click", async ()=>{
|
||||
let l = c.package_asset;
|
||||
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")
|
||||
})
|
||||
})
|
||||
]
|
||||
})
|
||||
]);
|
||||
};
|
||||
]), g.hidden = !1, y(u ? _("Your theme is up to date!") : _("A new version is available!"), u ? "success" : "info"), p.addEventListener("click", async ()=>{
|
||||
k(!0), m.hidden = w.hidden = !0;
|
||||
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) {
|
||||
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) {
|
||||
if (v.disabled = !1, console.error("Failed checking updates", l), l instanceof GitHubAPIError && 403 === l.status) {
|
||||
} catch (n) {
|
||||
if (y(String(n), "error"), n instanceof GitHubAPIError && (403 === n.status || 429 === n.status)) {
|
||||
let n = jsx("input", {
|
||||
type: "text",
|
||||
type: "password",
|
||||
class: "cbi-input-text",
|
||||
style: "width: 100%",
|
||||
placeholder: "ghp_..."
|
||||
}), 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)
|
||||
"aria-label": _("GitHub token"),
|
||||
autocomplete: "off"
|
||||
});
|
||||
L.ui.showModal(_("GitHub Token Required"), jsxs(Fragment, {
|
||||
children: [
|
||||
r,
|
||||
i,
|
||||
s,
|
||||
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.")
|
||||
}),
|
||||
n,
|
||||
jsxs("div", {
|
||||
class: "right",
|
||||
@@ -559,7 +425,7 @@ let about_u = about_l.DummyValue.extend({
|
||||
class: "btn cbi-button-save",
|
||||
onclick: ()=>{
|
||||
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")
|
||||
})
|
||||
@@ -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", ()=>{
|
||||
P(_("Checking for updates..."), "info"), v.disabled = !0, $.style.display = "none", z(), N();
|
||||
}), F;
|
||||
}
|
||||
return k(!0), (async ()=>{
|
||||
try {
|
||||
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)=>{
|
||||
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
|
||||
|
||||
+125
@@ -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
|
||||
|
||||
. /lib/functions.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 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
|
||||
"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")
|
||||
list)
|
||||
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_cleanup
|
||||
;;
|
||||
"remove")
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var filename "filename"
|
||||
json_cleanup
|
||||
if dirname "$filename" | grep -q ".."; then
|
||||
echo '{ "result": 255 }'
|
||||
exit 255
|
||||
fi
|
||||
rm -f "$bg_path/$filename"
|
||||
echo '{ "result": 0 }'
|
||||
;;
|
||||
"rename")
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var newname "newname"
|
||||
json_cleanup
|
||||
if dirname "$newname" | grep -q ".."; then
|
||||
echo '{ "result": 255 }'
|
||||
exit 255
|
||||
fi
|
||||
mkdir -p "$bg_path"
|
||||
if mv "$tmp_path" "$bg_path/$newname" 2>"/dev/null"; then
|
||||
chmod 0644 "$bg_path/$newname"
|
||||
echo '{ "result": 0 }'
|
||||
else
|
||||
echo '{ "result": 1 }'
|
||||
fi
|
||||
;;
|
||||
"set_mode")
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var mode "mode"
|
||||
json_cleanup
|
||||
case "$mode" in
|
||||
light|dark|auto) ;;
|
||||
*)
|
||||
echo '{ "result": 255 }'
|
||||
exit 255
|
||||
call)
|
||||
case "$2" in
|
||||
start_update|update_status)
|
||||
exec /bin/sh /usr/libexec/fluent/update "$2"
|
||||
;;
|
||||
avail)
|
||||
json_init
|
||||
json_add_int avail "$(df | grep -E '/$' | awk '{print $4}')"
|
||||
json_dump
|
||||
;;
|
||||
remove|rename)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
if [ "$2" = remove ]; then
|
||||
json_get_var name filename
|
||||
else
|
||||
json_get_var name newname
|
||||
fi
|
||||
case "$name" in
|
||||
''|.|..|*/*) echo '{"result":255}'; exit 0 ;;
|
||||
esac
|
||||
if [ "$2" = remove ]; then
|
||||
rm -f "$bg_path/$name"
|
||||
result=$?
|
||||
else
|
||||
mkdir -p "$bg_path"
|
||||
mv "$tmp_path" "$bg_path/$name" && chmod 0644 "$bg_path/$name"
|
||||
result=$?
|
||||
fi
|
||||
printf '{"result":%s}\n' "$result"
|
||||
;;
|
||||
set_mode)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var mode mode
|
||||
case "$mode" in
|
||||
light|dark|auto) ;;
|
||||
*) echo '{"result":255}'; exit 0 ;;
|
||||
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
|
||||
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
|
||||
|
||||
@@ -6,18 +6,16 @@
|
||||
"/www/luci-static/fluent/background/*": [ "list" ]
|
||||
},
|
||||
"ubus": {
|
||||
"luci.fluent": [ "avail", "remove", "rename", "set_mode", "get_version", "check_download", "get_install_log" ]
|
||||
"luci.fluent": [ "avail", "get_version", "update_status" ]
|
||||
},
|
||||
"uci": [ "fluent" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/tmp/fluent_background.tmp": [ "write" ],
|
||||
"/tmp/fluent-update.ipk": [ "write" ],
|
||||
"/tmp/fluent-update.apk": [ "write" ]
|
||||
"/tmp/fluent_background.tmp": [ "write" ]
|
||||
},
|
||||
"ubus": {
|
||||
"luci.fluent": [ "set_mode", "do_install", "start_download" ]
|
||||
"luci.fluent": [ "remove", "rename", "set_mode", "start_update" ]
|
||||
},
|
||||
"uci": [ "fluent" ]
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-theme-vantage
|
||||
PKG_VERSION:=1.0.16
|
||||
PKG_RELEASE:=35
|
||||
PKG_VERSION:=1.0.17
|
||||
PKG_RELEASE:=36
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
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
|
||||
key, also what the dashboard reads under another theme) counts until
|
||||
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 = {
|
||||
theme: { own: 'vantage.theme.own', before: THEME_KEY, ok: [ 'auto', 'light', 'dark' ] },
|
||||
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_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; } }
|
||||
|
||||
/* '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 theme knows, else the first choice (the default); the templates'
|
||||
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);
|
||||
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 === '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) {} }
|
||||
|
||||
@@ -452,20 +480,33 @@ function replaceTable(table, block) {
|
||||
}
|
||||
|
||||
/* 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() {
|
||||
statusSections(_('Memory')).forEach(section => {
|
||||
const table = pendingTable(section);
|
||||
if (!table) return;
|
||||
const rows = barRows(table), get = l => (rows.find(r => r.label === l) || {}).pair;
|
||||
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;
|
||||
if (!total || !used) return;
|
||||
|
||||
const free = Math.max(0, total - used.value);
|
||||
let available = avail ? avail.value : free + (buffered ? buffered.value : 0) + (cached ? cached.value : 0);
|
||||
available = Math.min(total, Math.max(available, free));
|
||||
let free, available;
|
||||
/* the row decides, not its value: LuCI shows "?" when it has none */
|
||||
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 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) {
|
||||
const swapUsed = swap.total - swap.value;
|
||||
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('div', { 'class': 'v-meter-bar is-thin', 'role': 'img',
|
||||
'aria-label': '%s: %s / %s'.format(_('Swap', 'vantage-theme'), fmtBytes(swapUsed), fmtBytes(swap.total)) },
|
||||
segment('v-seg-used', swapUsed, swap.total)),
|
||||
E('span', { 'class': 'v-meter-val' }, [ E('b', {}, [ fmtBytes(swapUsed) ]), ' / ' + fmtBytes(swap.total) ])
|
||||
'aria-label': '%s: %s / %s'.format(_('Swap', 'vantage-theme'), fmtBytes(swap.used), fmtBytes(swap.total)) },
|
||||
segment('v-seg-used', swap.used, 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();
|
||||
},
|
||||
|
||||
/* 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 */
|
||||
loginPref() {
|
||||
return this.lookPref('login');
|
||||
@@ -1620,6 +1686,7 @@ return baseclass.extend({
|
||||
}
|
||||
this.applyTheme();
|
||||
this.applyBackdrop();
|
||||
this.applyBrand();
|
||||
});
|
||||
/* the dashboard's own picker (Customize), saved as Appearance's */
|
||||
window.addEventListener(LOOK_SET_EVENT, ev => {
|
||||
@@ -1641,6 +1708,7 @@ return baseclass.extend({
|
||||
});
|
||||
this.applyTheme();
|
||||
this.applyBackdrop();
|
||||
this.applyBrand();
|
||||
},
|
||||
|
||||
/* Appearance, under the colour-theme button: the colour theme, the
|
||||
@@ -1651,7 +1719,7 @@ return baseclass.extend({
|
||||
scrolling the page close it. */
|
||||
/* one of Appearance's radio groups */
|
||||
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' }, [
|
||||
E('legend', {}, [ legend ]),
|
||||
E('div', { 'class': 'vt-appear-track' }, opts.map(o => {
|
||||
@@ -1678,16 +1746,22 @@ return baseclass.extend({
|
||||
group('vt-colour', _('Colour theme'), [
|
||||
[ 'auto', _('Auto', 'vantage-theme'), 'auto' ], [ 'light', _('Light', 'vantage-theme'), 'sun' ], [ 'dark', _('Dark', 'vantage-theme'), 'moon' ]
|
||||
], 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))
|
||||
].concat(this.hasDashboard() ? [ this.backdropGroup() ] : [], [
|
||||
/* 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 => {
|
||||
if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); this.closeAppearance(true); }
|
||||
else if (ev.key === 'Tab') {
|
||||
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); }
|
||||
}
|
||||
});
|
||||
@@ -1704,9 +1778,51 @@ return baseclass.extend({
|
||||
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() {
|
||||
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); });
|
||||
const state = this.lookState();
|
||||
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;
|
||||
const product = host.productName(board);
|
||||
this.hostname = board.hostname;
|
||||
this.board = board;
|
||||
this.updateTitle();
|
||||
this.applyBrand();
|
||||
if (!chip) return;
|
||||
chip.textContent = '';
|
||||
/* 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
|
||||
* window is in the background. With reduced motion it draws one still
|
||||
* 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 () {
|
||||
'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;
|
||||
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];
|
||||
|
||||
@@ -393,6 +393,15 @@ html[data-darkmode="true"] .vt-brand-tile { fill: #16181b; }
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.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 {
|
||||
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 */
|
||||
.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-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-track { display: flex; background: var(--seg-bg); border-radius: var(--r-lg); box-shadow: inset 0 0 0 1px var(--line); }
|
||||
.vt-appear-opt {
|
||||
@@ -672,12 +687,38 @@ a.vt-crumb:hover { color: var(--text); }
|
||||
}
|
||||
#indicators > span[data-clickable] { cursor: pointer; }
|
||||
#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
|
||||
distracted); paused: a square */
|
||||
/* auto-refresh on: a dot with a slow, soft ring (#100); fast while the
|
||||
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 {
|
||||
content: ""; width: 7px; height: 7px; border-radius: 50%;
|
||||
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 {
|
||||
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-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; }
|
||||
/* 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 */
|
||||
.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-label, .vt-search .vt-kbd { display: none; }
|
||||
.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) {
|
||||
.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;
|
||||
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="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
|
||||
sign while it refreshes, a play sign while paused */
|
||||
#indicators > span[data-indicator="poll-status"][data-style="active"]::before {
|
||||
|
||||
@@ -13,7 +13,7 @@ msgid_plural "%d more chains at this hook"
|
||||
msgstr[0] ""
|
||||
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"
|
||||
msgid "%d result"
|
||||
msgid_plural "%d results"
|
||||
@@ -56,7 +56,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Accept"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -65,22 +65,22 @@ msgctxt "nft match any traffic"
|
||||
msgid "Any packet"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1677
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:132
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1354
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1355
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1356
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1359
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1363
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1745
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:136
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Appearance"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:48
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:50
|
||||
msgid "Authorization Required"
|
||||
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"
|
||||
msgid "Auto"
|
||||
msgstr ""
|
||||
@@ -95,7 +95,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Back to the calling chain"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:120
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:124
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Breadcrumb"
|
||||
msgstr ""
|
||||
@@ -113,17 +113,17 @@ msgctxt "vantage-theme"
|
||||
msgid "Bridged between ports"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
msgid "Cache"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -142,19 +142,27 @@ msgctxt "vantage-theme"
|
||||
msgid "Chains no rule jumps to directly"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -173,11 +181,15 @@ msgctxt "vantage-theme"
|
||||
msgid "Continues in %s, then comes back here"
|
||||
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."
|
||||
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"
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
@@ -201,8 +213,8 @@ msgstr ""
|
||||
msgid "Egress"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:105
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:993
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:107
|
||||
msgid "Expand navigation"
|
||||
msgstr ""
|
||||
|
||||
@@ -219,7 +231,7 @@ msgid ""
|
||||
"their counters see only its first packets."
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -237,21 +249,25 @@ msgid ""
|
||||
"if br_netfilter is enabled; the ruleset doesn't show that.)"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:488
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:515
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:529
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Free"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:100
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:101
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Home"
|
||||
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
|
||||
msgid "IPv4 traffic table \"%h\""
|
||||
msgstr ""
|
||||
@@ -264,9 +280,9 @@ msgstr ""
|
||||
msgid "IPv6 traffic table \"%h\""
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:482
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:486
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:523
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:527
|
||||
msgctxt "vantage-theme"
|
||||
msgid "In use"
|
||||
msgstr ""
|
||||
@@ -280,11 +296,11 @@ msgctxt "vantage-theme"
|
||||
msgid "Input"
|
||||
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."
|
||||
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…"
|
||||
msgstr ""
|
||||
|
||||
@@ -293,31 +309,35 @@ msgctxt "vantage-theme"
|
||||
msgid "Leaves unmatched unchanged"
|
||||
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"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:28
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:64
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:30
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:66
|
||||
msgid "Log in"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:135
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1365
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:139
|
||||
msgid "Log out"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1681
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1359
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1752
|
||||
msgid "Login background"
|
||||
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"
|
||||
msgid "Main navigation"
|
||||
msgstr ""
|
||||
@@ -327,13 +347,17 @@ msgctxt "vantage-theme"
|
||||
msgid "Masquerade"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:457
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:472
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:513
|
||||
msgid "Memory"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1961
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1978
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1750
|
||||
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"
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
@@ -351,8 +375,8 @@ msgstr ""
|
||||
msgid "Network device table \"%h\""
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1670
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1363
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1738
|
||||
msgid "Network path background"
|
||||
msgstr ""
|
||||
|
||||
@@ -361,7 +385,11 @@ msgctxt "vantage-theme"
|
||||
msgid "Next rule"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -375,17 +403,17 @@ msgctxt "vantage-theme"
|
||||
msgid "No rules: %s"
|
||||
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"
|
||||
msgid "None"
|
||||
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
|
||||
msgid "Not found"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -399,7 +427,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Packet path"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:141
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:145
|
||||
msgid "Page tabs"
|
||||
msgstr ""
|
||||
|
||||
@@ -413,7 +441,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Passes unmatched"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:61
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:63
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
@@ -442,7 +470,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Reject"
|
||||
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"
|
||||
msgid "Results"
|
||||
msgstr ""
|
||||
@@ -462,17 +490,21 @@ msgctxt "vantage-theme"
|
||||
msgid "Rules table"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:123
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:125
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1386
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:127
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:129
|
||||
msgid "Search pages"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -483,47 +515,52 @@ msgid_plural "Show %d more rules"
|
||||
msgstr[0] ""
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:495
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:497
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:538
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:540
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Swap"
|
||||
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"
|
||||
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
|
||||
msgctxt "vantage-theme"
|
||||
msgid "This device"
|
||||
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"
|
||||
msgid "Total"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -537,19 +574,23 @@ msgctxt "vantage-theme"
|
||||
msgid "Unchanged"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:57
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:59
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
@@ -565,21 +606,29 @@ msgctxt "vantage-theme"
|
||||
msgid "Verdict map %s"
|
||||
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
|
||||
msgctxt "vantage-theme"
|
||||
msgid "View"
|
||||
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"
|
||||
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"
|
||||
msgid "available"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -593,12 +642,12 @@ msgctxt "vantage-theme"
|
||||
msgid "drops unmatched %s"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
|
||||
msgid "free"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
@@ -617,7 +666,7 @@ msgctxt "vantage-theme"
|
||||
msgid "on %s"
|
||||
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"
|
||||
msgid "open"
|
||||
msgstr ""
|
||||
@@ -627,9 +676,9 @@ msgctxt "vantage-theme"
|
||||
msgid "priority %d"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
msgid "reclaimable"
|
||||
msgstr ""
|
||||
|
||||
@@ -638,12 +687,12 @@ msgctxt "vantage-theme"
|
||||
msgid "rules drop some"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
|
||||
msgid "total"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
|
||||
msgid "used"
|
||||
msgstr ""
|
||||
@@ -21,7 +21,7 @@ msgid "%d more chain at this hook"
|
||||
msgid_plural "%d more chains at this hook"
|
||||
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"
|
||||
msgid "%d result"
|
||||
msgid_plural "%d results"
|
||||
@@ -62,7 +62,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Accept"
|
||||
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"
|
||||
msgstr "操作"
|
||||
|
||||
@@ -71,22 +71,22 @@ msgctxt "nft match any traffic"
|
||||
msgid "Any packet"
|
||||
msgstr "任意数据包"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1311
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1312
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1677
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:132
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1354
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1355
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1356
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1359
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1363
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1745
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:136
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Appearance"
|
||||
msgstr "外观"
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:48
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:50
|
||||
msgid "Authorization Required"
|
||||
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"
|
||||
msgid "Auto"
|
||||
msgstr "自动"
|
||||
@@ -101,7 +101,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Back to the calling chain"
|
||||
msgstr "返回调用链"
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:120
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:124
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Breadcrumb"
|
||||
msgstr "面包屑"
|
||||
@@ -119,17 +119,17 @@ msgctxt "vantage-theme"
|
||||
msgid "Bridged between ports"
|
||||
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"
|
||||
msgstr "已缓冲"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
msgid "Cache"
|
||||
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"
|
||||
msgstr "已缓存"
|
||||
|
||||
@@ -148,19 +148,27 @@ msgctxt "vantage-theme"
|
||||
msgid "Chains no rule jumps to directly"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
msgstr "命令面板"
|
||||
|
||||
@@ -179,11 +187,15 @@ msgctxt "vantage-theme"
|
||||
msgid "Continues in %s, then comes back here"
|
||||
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."
|
||||
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"
|
||||
msgid "Dark"
|
||||
msgstr "深色"
|
||||
@@ -207,8 +219,8 @@ msgstr "丢弃未匹配流量"
|
||||
msgid "Egress"
|
||||
msgstr "出口"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:950
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:105
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:993
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:107
|
||||
msgid "Expand navigation"
|
||||
msgstr "展开导航"
|
||||
|
||||
@@ -227,7 +239,7 @@ msgstr ""
|
||||
"流量卸载已开启:一条转发连接建立后,它的数据包可以跳过 prerouting、forward "
|
||||
"和 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"
|
||||
msgstr "跟随系统主题"
|
||||
|
||||
@@ -248,21 +260,25 @@ msgstr ""
|
||||
"达 input、forward 或 output 链,这里也没有桥接链会过滤它们。(启用 "
|
||||
"br_netfilter 时内核可以把它们交给 IP 链;规则集不体现这一点。)"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:474
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:488
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:515
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:529
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Free"
|
||||
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"
|
||||
msgstr "网格数据包"
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:100
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:101
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Home"
|
||||
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
|
||||
msgid "IPv4 traffic table \"%h\""
|
||||
msgstr "IPv4 流量表 \"%h\""
|
||||
@@ -275,9 +291,9 @@ msgstr "IPv4/IPv6 流量表 \"%h\""
|
||||
msgid "IPv6 traffic table \"%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:482
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:486
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:523
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:527
|
||||
msgctxt "vantage-theme"
|
||||
msgid "In use"
|
||||
msgstr "已用"
|
||||
@@ -291,11 +307,11 @@ msgctxt "vantage-theme"
|
||||
msgid "Input"
|
||||
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."
|
||||
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…"
|
||||
msgstr "跳转到页面或操作…"
|
||||
|
||||
@@ -304,31 +320,35 @@ msgctxt "vantage-theme"
|
||||
msgid "Leaves unmatched unchanged"
|
||||
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"
|
||||
msgid "Light"
|
||||
msgstr "浅色"
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:28
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:64
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:30
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:66
|
||||
msgid "Log in"
|
||||
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."
|
||||
msgstr "重新登录后即可为所有浏览器保存外观设置。"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1322
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:135
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1365
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:139
|
||||
msgid "Log out"
|
||||
msgstr "退出登录"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1681
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1359
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1752
|
||||
msgid "Login background"
|
||||
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"
|
||||
msgid "Main navigation"
|
||||
msgstr "主导航"
|
||||
@@ -338,13 +358,17 @@ msgctxt "vantage-theme"
|
||||
msgid "Masquerade"
|
||||
msgstr "伪装(NAT)"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:457
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:472
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:513
|
||||
msgid "Memory"
|
||||
msgstr "内存"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1961
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1978
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1750
|
||||
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"
|
||||
msgid "More actions"
|
||||
msgstr "更多操作"
|
||||
@@ -362,8 +386,8 @@ msgstr "网络"
|
||||
msgid "Network device table \"%h\""
|
||||
msgstr "网络设备表 \"%h\""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1670
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1363
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1738
|
||||
msgid "Network path background"
|
||||
msgstr "网络路径背景"
|
||||
|
||||
@@ -372,7 +396,11 @@ msgctxt "vantage-theme"
|
||||
msgid "Next rule"
|
||||
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"
|
||||
msgstr "没有匹配的页面"
|
||||
|
||||
@@ -386,17 +414,17 @@ msgctxt "vantage-theme"
|
||||
msgid "No rules: %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"
|
||||
msgid "None"
|
||||
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
|
||||
msgid "Not found"
|
||||
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"
|
||||
msgstr "%s 中的其他页面"
|
||||
|
||||
@@ -410,7 +438,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Packet path"
|
||||
msgstr "数据包路径"
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:141
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:145
|
||||
msgid "Page tabs"
|
||||
msgstr "页面标签页"
|
||||
|
||||
@@ -424,7 +452,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Passes unmatched"
|
||||
msgstr "放行未匹配流量"
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:61
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:63
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
|
||||
@@ -453,7 +481,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Reject"
|
||||
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"
|
||||
msgid "Results"
|
||||
msgstr "结果"
|
||||
@@ -473,17 +501,21 @@ msgctxt "vantage-theme"
|
||||
msgid "Rules table"
|
||||
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."
|
||||
msgstr "仅保存在此浏览器中。"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1343
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:123
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:125
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1386
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:127
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:129
|
||||
msgid "Search pages"
|
||||
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"
|
||||
msgstr "会话"
|
||||
|
||||
@@ -493,47 +525,52 @@ msgid "Show %d more rule"
|
||||
msgid_plural "Show %d more rules"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
msgstr "存储"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:495
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:497
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:538
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:540
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Swap"
|
||||
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"
|
||||
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
|
||||
msgctxt "vantage-theme"
|
||||
msgid "This device"
|
||||
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"
|
||||
msgid "Total"
|
||||
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"
|
||||
msgstr "总可用"
|
||||
|
||||
@@ -547,19 +584,23 @@ msgctxt "vantage-theme"
|
||||
msgid "Unchanged"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
msgstr "已用"
|
||||
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:57
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:59
|
||||
msgid "Username"
|
||||
msgstr "用户名"
|
||||
|
||||
@@ -577,21 +618,29 @@ msgctxt "vantage-theme"
|
||||
msgid "Verdict map %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
|
||||
msgctxt "vantage-theme"
|
||||
msgid "View"
|
||||
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"
|
||||
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"
|
||||
msgid "available"
|
||||
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"
|
||||
msgstr "关闭"
|
||||
|
||||
@@ -605,12 +654,12 @@ msgctxt "vantage-theme"
|
||||
msgid "drops unmatched %s"
|
||||
msgstr "丢弃未匹配的 %s"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
|
||||
msgid "free"
|
||||
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"
|
||||
msgstr "移动"
|
||||
|
||||
@@ -629,7 +678,7 @@ msgctxt "vantage-theme"
|
||||
msgid "on %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"
|
||||
msgid "open"
|
||||
msgstr "打开"
|
||||
@@ -639,9 +688,9 @@ msgctxt "vantage-theme"
|
||||
msgid "priority %d"
|
||||
msgstr "优先级 %d"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:473
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:483
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:487
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:514
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
msgid "reclaimable"
|
||||
msgstr "可回收"
|
||||
|
||||
@@ -650,13 +699,13 @@ msgctxt "vantage-theme"
|
||||
msgid "rules drop some"
|
||||
msgstr "部分规则会丢弃"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
|
||||
msgid "total"
|
||||
msgstr "总计"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:524
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:528
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:567
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:571
|
||||
msgid "used"
|
||||
msgstr "已用"
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# option theme 'auto' auto, light or dark
|
||||
# option login 'grid' the login page's background: grid, stars or none
|
||||
# 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
|
||||
# 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_login = look('login', [ '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
|
||||
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' ]);
|
||||
@@ -53,11 +54,9 @@
|
||||
});
|
||||
return would ? 'relogin' : 'no';
|
||||
})();
|
||||
const boardinfo = ubus.call("system", "board");
|
||||
const hostname = striptags(boardinfo?.hostname ?? '?');
|
||||
-%}
|
||||
<!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>
|
||||
<meta charset="utf-8">
|
||||
<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="21.5" r="1.9" class="vt-brand-dot"></circle>
|
||||
</svg>
|
||||
<h3 class="vt-brand-word">{{ hostname }}</h3>
|
||||
<span class="vt-brand-name" id="vt-brand-name" hidden></span>
|
||||
</a>
|
||||
<ul class="vt-rail-list" id="vt-rail-list"></ul>
|
||||
<div class="vt-rail-foot">
|
||||
@@ -126,6 +125,8 @@ const hostname = striptags(boardinfo?.hostname ?? '?');
|
||||
<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>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="vt-top-name" id="vt-top-name" hidden></span>
|
||||
{% 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>
|
||||
<div class="vt-top-actions">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{#
|
||||
Vantage theme - login page.
|
||||
Rendered with { duser, fuser } for visitors who are not logged in, so it
|
||||
shows nothing about the device: no hostname, model or firmware version.
|
||||
Rendered with { duser, fuser } for visitors who are not logged in. Of the
|
||||
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
|
||||
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
|
||||
@@ -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_theme = look('theme', [ 'auto', 'light', 'dark' ]);
|
||||
const look_login = look('login', [ 'grid', 'stars', 'none' ]);
|
||||
const boardinfo = ubus.call("system", "board");
|
||||
const hostname = striptags(boardinfo?.hostname ?? '?');
|
||||
const host = (() => { const h = uci.get_first('system', 'system', 'hostname'); return (type(h) == 'string' && length(h)) ? substr(h, 0, 63) : null; })();
|
||||
-%}
|
||||
<!DOCTYPE html>
|
||||
<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="21.5" r="1.9" class="vt-brand-dot"></circle>
|
||||
</svg>
|
||||
<div>
|
||||
<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>
|
||||
<h1 class="vt-login-title">{{ entityencode(host ?? _('Authorization Required'), true) }}</h1>
|
||||
</div>
|
||||
|
||||
{% if (fuser): %}
|
||||
|
||||
+3
-3
@@ -50,12 +50,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand
|
||||
PKG_VERSION:=1.9.0_pre3
|
||||
PKG_RELEASE:=88
|
||||
PKG_VERSION:=1.9.0_pre10
|
||||
PKG_RELEASE:=89
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||
PKG_SOURCE_VERSION:=0a1f02375df53f21a105b375b9af051c36464530
|
||||
PKG_SOURCE_VERSION:=e9fd1db72db20faced06e323c06e8a1062607bf2
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
Reference in new issue
Block a user