💋 Sync 2026-10-16 23:55:30

This commit is contained in:
github-actions[bot] committed 2026-10-16 23:55:30 +08:00
1 parent 76f10c2a74
commit 1849cc6156
254 files changed
+25484 -8840

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=7z
PKG_VERSION:=26.03
PKG_RELEASE:=4
PKG_VERSION:=26.04
PKG_RELEASE:=5
PKG_SOURCE:=$(PKG_NAME)$(subst .,,$(PKG_VERSION))-src.tar.xz
PKG_SOURCE_URL:=https://7-zip.org/a/
+2 -2
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=ariang
PKG_VERSION:=1.3.15
PKG_RELEASE:=5
PKG_RELEASE:=6
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).zip
PKG_SOURCE_URL:=https://github.com/mayswind/AriaNg/releases/download/$(PKG_VERSION)
@@ -38,7 +38,7 @@ endef
define Package/ariang-nginx
$(Package/ariang/default)
DEPENDS += +nginx
EXTRA_DEPENDS:=nginx$(if $(CONFIG_USE_APK), (>=0))
TITLE += for nginx webserver
endef
+2 -2
View File
@@ -5,13 +5,13 @@ PKG_SHORT_SHA:=94b13ae
PKG_COMMIT_DATE:=2026.10.13
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
PKG_RELEASE:=13
PKG_RELEASE:=14
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=4b487e294f46e98f51548f8c5253df804d7d4b38
PKG_SOURCE_VERSION:=4cc682daee8110a41887b5fb2d5a4b9fb5f6a696
PKG_HASH:=skip
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
+140 -28
View File
@@ -1481,7 +1481,7 @@ _remap_dns_system_nameserver() {
ip_rules() {
#===========================================================================================================================
sh /usr/share/clashoo/runtime/iprules.sh 2>/dev/null
CLASHOO_CONFIG_YAML="$CONFIG_YAML" sh /usr/share/clashoo/runtime/iprules.sh 2>/dev/null
#===========================================================================================================================
}
@@ -1691,15 +1691,21 @@ _lan_dns_split_enabled() {
yml_dns_change(){
#===========================================================================================================================
dns_port=$(sed -n 's/^[[:space:]]*listen:.*:[[:space:]]*['"'"'"]*\([0-9]\{1,\}\)['"'"'"]*[[:space:]]*$/\1/p' /etc/clashoo/config.yaml 2>/dev/null | head -n1)
dns_port=$(sed -n 's/^[[:space:]]*listen:.*:[[:space:]]*['"'"'"]*\([0-9]\{1,\}\)['"'"'"]*[[:space:]]*$/\1/p' "$CONFIG_YAML" 2>/dev/null | head -n1)
listen_port=$(uci get clashoo.config.listen_port 2>/dev/null)
[ -z "$listen_port" ] && listen_port=5300
if [ -z "$dns_port" ]; then
dns_port="$listen_port"
if grep -q '^dns:' /etc/clashoo/config.yaml && ! grep -q "^ \{0,\}listen:" /etc/clashoo/config.yaml; then
sed -i "/^dns:/a\ listen: 0.0.0.0:${dns_port}" /etc/clashoo/config.yaml 2>/dev/null
if grep -q '^dns:' "$CONFIG_YAML" && ! grep -q "^ \{0,\}listen:" "$CONFIG_YAML"; then
sed -i "/^dns:/a\ listen: 0.0.0.0:${dns_port}" "$CONFIG_YAML" 2>/dev/null
fi
fi
if [ "${CLASHOO_RELOAD_CANDIDATE:-0}" = "1" ]; then
if [ "${dns_port:-0}" -eq 53 ]; then
sed -i "s@^\([[:space:]]*listen:[[:space:]]*\)0\.0\.0\.0:53@\10.0.0.0:${listen_port}@g" "$CONFIG_YAML" || return 1
fi
return 0
fi
dnsforwader=$(uci get clashoo.config.dnsforwader 2>/dev/null)
dnscache=$(uci get clashoo.config.dnscache 2>/dev/null)
if [ "$(uci -q get clashoo.config.enable_dns 2>/dev/null)" = "0" ]; then
@@ -2036,26 +2042,29 @@ _start_validate_core() {
}
_start_prepare_config() {
local _reload_candidate="${CLASHOO_RELOAD_CANDIDATE:-0}"
# Core-only: run the imported config verbatim. select_config already copied
# it to /etc/clashoo/config.yaml, so skip every mixin/DNS/rule rewrite below.
[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] && return 0
check >/dev/null 2>&1
check >/dev/null 2>&1 || return 1
_migrate_dns_roles >/dev/null 2>&1
[ "$_reload_candidate" = "1" ] || _migrate_dns_roles >/dev/null 2>&1
enable_dns=$(uci get clashoo.config.enable_dns 2>/dev/null)
if [ -z "$(grep -E "^ {0,}listen:" /etc/clashoo/config.yaml)" ] || [ -z "$(grep -E "^ {0,}enhanced-mode:" /etc/clashoo/config.yaml)" ] || [ -z "$(grep -E "^ {0,}dns:" /etc/clashoo/config.yaml)" ];then
if [ -z "$(grep -E "^ {0,}listen:" "$CONFIG_YAML")" ] || [ -z "$(grep -E "^ {0,}enhanced-mode:" "$CONFIG_YAML")" ] || [ -z "$(grep -E "^ {0,}dns:" "$CONFIG_YAML")" ];then
if [ "${enable_dns:-0}" -eq 0 ];then
[ "$_reload_candidate" = "1" ] && return 1
uci set clashoo.config.enable_dns="1" && uci commit clashoo
log_msg "Enabling Custom Dns" "启用自定义DNS"
fi
fi
if { egrep -q '^ {0,}rule-providers:' "/etc/clashoo/config.yaml" || egrep -q '^ {0,}script:' "/etc/clashoo/config.yaml"; } && ! core_supports_advanced_features; then
if { egrep -q '^ {0,}rule-providers:' "$CONFIG_YAML" || egrep -q '^ {0,}script:' "$CONFIG_YAML"; } && ! core_supports_advanced_features; then
[ "$_reload_candidate" = "1" ] && return 1
if [ -x /usr/bin/mihomo ]; then
uci set clashoo.config.core="3" && uci commit clashoo
elif [ -x /usr/bin/mihomo-stable ]; then
@@ -2073,6 +2082,7 @@ _start_prepare_config() {
fi
if [ "$(uci get clashoo.config.p_mode 2>/dev/null)" = "script" ]; then
[ "$_reload_candidate" = "1" ] && return 1
uci set clashoo.config.p_mode="rule" && uci commit clashoo
fi
@@ -2082,7 +2092,7 @@ _start_prepare_config() {
fi
_adapt_dns_loopback_resolver >/dev/null 2>&1
# stage bundled cn.mrs into the core dir; mihomo rejects rule-set paths outside /etc/clashoo
if [ -f /usr/share/clashoo/ruleset/cn.mrs ]; then
if [ "$_reload_candidate" != "1" ] && [ -f /usr/share/clashoo/ruleset/cn.mrs ]; then
if [ ! -f /etc/clashoo/ruleset/cn.mrs ] || [ /usr/share/clashoo/ruleset/cn.mrs -nt /etc/clashoo/ruleset/cn.mrs ]; then
mkdir -p /etc/clashoo/ruleset
cp -f /usr/share/clashoo/ruleset/cn.mrs /etc/clashoo/ruleset/cn.mrs 2>/dev/null
@@ -2117,12 +2127,13 @@ _start_prepare_config() {
log_msg "GeoIP database not found, continue without local GeoIP DB" "未找到本地 GeoIP 数据库,继续启动(可在系统页更新 GeoIP)"
fi
_prepare_geo_bootstrap_mode
[ "$_reload_candidate" = "1" ] || _prepare_geo_bootstrap_mode
_remap_dns_system_nameserver >/dev/null 2>&1
ensure_dns_cn_policy_fallback >/dev/null 2>&1
if [ ! -f /etc/clashoo/config.yaml ];then
if [ ! -f "$CONFIG_YAML" ];then
[ "$_reload_candidate" = "1" ] && return 1
log_msg "Cannot find config.yaml" "找不到config.yaml"
backend_remove_rules >/dev/null 2>&1
sleep 1
@@ -2130,10 +2141,10 @@ _start_prepare_config() {
exit 0
fi
yml_dns_change >/dev/null 2>&1
yml_dns_change >/dev/null 2>&1 || return 1
# Sync processed config (with DNS listen injected) to mihomo working directory
cp -f "$CONFIG_YAML" "${CLASH_CONFIG}/config.yaml" 2>/dev/null
[ "$_reload_candidate" = "1" ] || cp -f "$CONFIG_YAML" "${CLASH_CONFIG}/config.yaml" 2>/dev/null
ip_rules >/dev/null 2>&1
}
@@ -2465,6 +2476,52 @@ stop_service(){
#===========================================================================================================================
}
_mihomo_reload_api() {
local _path="$1" _response="$2" _dp _ds _lip
_dp="$(uci -q get clashoo.config.dash_port)"; [ -n "$_dp" ] || _dp=9090
_ds="$(uci -q get clashoo.config.dash_pass)"
_lip="$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}')"
[ -n "$_lip" ] || _lip=127.0.0.1
MIHOMO_RELOAD_CURL_RC=0
MIHOMO_RELOAD_HTTP_CODE="$(curl -sS -m 5 -o "$_response" -w '%{http_code}' \
-H "Authorization: Bearer ${_ds}" -H "Content-Type: application/json" \
-X PUT -d '{"path":"'"$_path"'"}' \
"http://${_lip}:${_dp}/configs?force=true" 2>/dev/null)" || MIHOMO_RELOAD_CURL_RC=$?
[ "$MIHOMO_RELOAD_CURL_RC" -eq 0 ] || return 1
case "$MIHOMO_RELOAD_HTTP_CODE" in
2??) return 0 ;;
*) return 1 ;;
esac
}
_mihomo_reload_save_dns() {
MIHOMO_DHCP_MODE="$(stat -c %a /etc/config/dhcp)" || return 1
cp /etc/config/dhcp "$1"
}
_mihomo_reload_restore_dns() {
cp "$1" /etc/config/dhcp && chmod "$MIHOMO_DHCP_MODE" /etc/config/dhcp &&
uci -q revert dhcp && _reload_dnsmasq >/dev/null 2>&1
}
_mihomo_reload_restore() {
local _previous="$1" _response="$2" _dns_backup="$3" _peripheral_changed="$4" _restore _failed=0
if ! _mihomo_reload_api "$_previous" "$_response"; then
return 1
fi
if [ "$_peripheral_changed" = "1" ]; then
if _restore="$(mktemp "${CLASH_CONFIG}/.reload-restore.XXXXXX")"; then
cp "$_previous" "$_restore" && mv -f "$_restore" "$CONFIG_YAML" || { rm -f "$_restore"; _failed=1; }
else
_failed=1
fi
_mihomo_reload_restore_dns "$_dns_backup" || _failed=1
backend_remove_rules >/dev/null 2>&1 || _failed=1
backend_apply_rules >/dev/null 2>&1 || _failed=1
fi
return "$_failed"
}
reload_service()
{
if is_singbox_selected; then
@@ -2477,23 +2534,78 @@ reload_service()
start
fi
elif _core_running; then
local _candidate _previous _response _dns_backup _source _pf_rc _error _peripheral_changed
_error=""
resolve_effective_tp_modes
runtime_state_record_preflight "mihomo"
backend_remove_rules 2>/dev/null
( select_config && _start_prepare_config ) >/dev/null 2>&1
# hot-reload via mihomo REST API (config + addtype rules), no process restart or tunnel drop
local _dp _ds _lip
_dp="$(uci -q get clashoo.config.dash_port)"; [ -n "$_dp" ] || _dp=9090
_ds="$(uci -q get clashoo.config.dash_pass)"
_lip="$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}')"
[ -n "$_lip" ] || _lip=127.0.0.1
curl -m 5 -H "Authorization: Bearer ${_ds}" -H "Content-Type: application/json" \
-X PUT -d '{"path":"'"$CONFIG_YAML"'"}' \
"http://${_lip}:${_dp}/configs?force=true" >/dev/null 2>&1
backend_apply_rules 2>/dev/null
if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ]; then
_reload_dnsmasq >/dev/null 2>&1
_candidate="$(mktemp "${CLASH_CONFIG}/.reload-candidate.XXXXXX")" || { _log_runtime_error "Mihomo reload failed: cannot stage candidate"; return 1; }
_previous="$(mktemp "${CLASH_CONFIG}/.reload-previous.XXXXXX")" || { rm -f "$_candidate"; _log_runtime_error "Mihomo reload failed: cannot back up current config"; return 1; }
_response="$(mktemp /tmp/clashoo-reload-response.XXXXXX)" || { rm -f "$_candidate" "$_previous"; _log_runtime_error "Mihomo reload failed: cannot stage API response"; return 1; }
_dns_backup="$(mktemp "${CLASH_CONFIG}/.reload-dhcp.XXXXXX")" || { rm -f "$_candidate" "$_previous" "$_response"; _log_runtime_error "Mihomo reload failed: cannot back up DNS state"; return 1; }
_source="$(uci -q get clashoo.config.use_config 2>/dev/null)"
if [ ! -s "$_source" ]; then
_error="selected config is missing or empty"
elif ! cp "$_source" "$_candidate" || ! cp "$CONFIG_YAML" "$_previous" ||
! _mihomo_reload_save_dns "$_dns_backup"; then
_error="cannot back up current config or DNS state"
elif ! ( CONFIG_YAML="$_candidate"; CLASHOO_RELOAD_CANDIDATE=1; _start_prepare_config ) >/dev/null 2>&1 ||
[ ! -s "$_candidate" ]; then
_error="candidate generation failed; check DNS and core settings"
fi
rm -f "${_candidate}.mixin" 2>/dev/null
if [ -n "$_error" ]; then
_log_runtime_error "Mihomo reload failed: ${_error}; current config preserved"
rm -f "$_candidate" "$_previous" "$_response" "$_dns_backup"
return 1
fi
if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ]; then
SAFE_PATHS=/ _run_with_timeout 15 "$CLASH" -d "$CLASH_CONFIG" -f "$_candidate" -t >> "$CORE_LOG_PATH" 2>&1
else
_run_with_timeout 15 "$CLASH" -d "$CLASH_CONFIG" -f "$_candidate" -t >> "$CORE_LOG_PATH" 2>&1
fi
_pf_rc=$?
if [ "$_pf_rc" -ne 0 ]; then
if [ "$_pf_rc" -eq 124 ]; then
_error="Mihomo reload failed: candidate preflight timed out"
else
_error="Mihomo reload failed: candidate config invalid (exit ${_pf_rc}; see core log)"
fi
_log_runtime_error "$_error; current config preserved"
rm -f "$_candidate" "$_previous" "$_response" "$_dns_backup"
return 1
fi
if ! _mihomo_reload_api "$_candidate" "$_response"; then
_error="Mihomo reload failed (curl ${MIHOMO_RELOAD_CURL_RC}, HTTP ${MIHOMO_RELOAD_HTTP_CODE})"
if _mihomo_reload_restore "$_previous" "$_response" "$_dns_backup" 0; then
_log_runtime_error "${_error}; previous config restored"
rm -f "$_candidate" "$_previous" "$_response" "$_dns_backup"
else
_log_runtime_error "${_error}; reload outcome unknown; candidate and backup retained"
rm -f "$_response" "$_dns_backup"
fi
return 1
fi
_peripheral_changed=0
if ! mv -f "$_candidate" "$CONFIG_YAML"; then
_error="Mihomo reload failed: cannot commit candidate config"
elif [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ]; then
_peripheral_changed=1
if ! backend_remove_rules >/dev/null 2>&1 || ! backend_apply_rules >/dev/null 2>&1 ||
! yml_dns_change >/dev/null 2>&1 || ! _reload_dnsmasq >/dev/null 2>&1; then
_error="Mihomo reload failed: firewall or DNS update failed"
fi
fi
if [ -n "$_error" ]; then
if _mihomo_reload_restore "$_previous" "$_response" "$_dns_backup" "$_peripheral_changed"; then
_log_runtime_error "${_error}; previous config and settings restored"
rm -f "$_candidate" "$_previous" "$_response" "$_dns_backup"
else
_log_runtime_error "${_error}; rollback incomplete; previous config retained for recovery"
rm -f "$_candidate" "$_response"
fi
return 1
fi
rm -f "$_previous" "$_response" "$_dns_backup" "${CONFIG_YAML}.dns-leak-nameserver"
runtime_state_record_preflight "mihomo"
runtime_run_health_check_settle >/dev/null 2>&1
fi
}
@@ -145,132 +145,70 @@ dns_mihomo_apply_respect_rules() {
# Use fallback DNS for queries; keep bootstrap DNS unchanged.
dns_mihomo_apply_leak_nameservers() {
local cfg="$1" enabled="${2:-0}" state="${1}.dns-leak-nameserver"
local selected="${1}.dns-leak-selected" current="${1}.dns-leak-current" tmp="${1}.$$"
local selected="${1}.dns-leak-selected" tmp="${1}.$$"
# The config is rebuilt from the selected subscription on every run.
# A saved nameserver from an earlier run must never replace its DNS.
rm -f "$state" 2>/dev/null
[ "$enabled" = "1" ] || return 0
[ -f "$cfg" ] || return 0
command -v yq >/dev/null 2>&1 || return 0
if [ "$enabled" = "1" ]; then
yq e -r '.dns.fallback[]' "$cfg" > "$selected" 2>/dev/null || true
if [ ! -s "$selected" ]; then
printf '%s\n' 'https://1.1.1.1/dns-query' 'https://8.8.8.8/dns-query' > "$selected"
fi
yq e -r '.dns.nameserver[]' "$cfg" > "$current" 2>/dev/null || true
if [ ! -f "$state" ] ||
! grep -q 'clashoo:dns_leak_protect_respect_rules' "$cfg" ||
! cmp -s "$current" "$selected"; then
awk '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
/^dns:[[:space:]]*$/ { in_dns = 1; dns_indent = indent_len($0); next }
in_dns && /^[^[:space:]#][^:]*:/ { exit }
in_dns && /^[[:space:]]*nameserver:/ {
print
rest = $0
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
sub(/[[:space:]]*(#.*)?$/, "", rest)
if (rest == "") { in_ns = 1; ns_indent = indent_len($0) }
next
}
in_ns {
cur_indent = indent_len($0)
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) {
print
next
}
exit
}
' "$cfg" > "$state"
[ -s "$state" ] || printf '__ABSENT__\n' > "$state"
fi
awk -v selected="$selected" '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
function spaces(n, out,i) {
out = ""
for (i = 0; i < n; i++) out = out " "
return out
}
function print_selected(sp, line) {
print sp "nameserver:"
while ((getline line < selected) > 0)
if (line != "") print sp " - " line
close(selected)
inserted = 1
}
/^dns:[[:space:]]*$/ {
in_dns = 1
dns_indent = indent_len($0)
child_indent = -1
print
next
}
in_dns && /^[^[:space:]#][^:]*:/ {
if (!inserted) print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
in_dns = 0
print
next
}
in_dns && /^[[:space:]]*[^#[:space:]][^:]*:/ && child_indent < 0 {
child_indent = indent_len($0)
}
in_dns && /^[[:space:]]*nameserver:/ {
ns_indent = indent_len($0)
rest = $0
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
sub(/[[:space:]]*(#.*)?$/, "", rest)
print_selected(spaces(ns_indent))
if (rest == "") skip_ns = 1
next
}
skip_ns {
cur_indent = indent_len($0)
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
skip_ns = 0
}
{ print }
END {
if (in_dns && !inserted)
print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
}
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
elif [ -f "$state" ]; then
awk -v state="$state" '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
function print_original( line) {
if ((getline line < state) > 0 && line != "__ABSENT__") {
print line
while ((getline line < state) > 0) print line
}
close(state)
restored = 1
}
/^dns:[[:space:]]*$/ { in_dns = 1; print; next }
in_dns && /^[^[:space:]#][^:]*:/ {
if (!restored) print_original()
in_dns = 0
print
next
}
in_dns && /^[[:space:]]*nameserver:/ {
ns_indent = indent_len($0)
rest = $0
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
sub(/[[:space:]]*(#.*)?$/, "", rest)
print_original()
if (rest == "") skip_ns = 1
next
}
skip_ns {
cur_indent = indent_len($0)
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
skip_ns = 0
}
{ print }
END { if (in_dns && !restored) print_original() }
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
rm -f "$state"
yq e -r '.dns.fallback[]' "$cfg" > "$selected" 2>/dev/null || true
if [ ! -s "$selected" ]; then
printf '%s\n' 'https://1.1.1.1/dns-query' 'https://8.8.8.8/dns-query' > "$selected"
fi
rm -f "$selected" "$current" "$tmp" 2>/dev/null
awk -v selected="$selected" '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
function spaces(n, out,i) {
out = ""
for (i = 0; i < n; i++) out = out " "
return out
}
function print_selected(sp, line) {
print sp "nameserver:"
while ((getline line < selected) > 0)
if (line != "") print sp " - " line
close(selected)
inserted = 1
}
/^dns:[[:space:]]*$/ {
in_dns = 1
dns_indent = indent_len($0)
child_indent = -1
print
next
}
in_dns && /^[^[:space:]#][^:]*:/ {
if (!inserted) print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
in_dns = 0
print
next
}
in_dns && /^[[:space:]]*[^#[:space:]][^:]*:/ && child_indent < 0 {
child_indent = indent_len($0)
}
in_dns && /^[[:space:]]*nameserver:/ {
ns_indent = indent_len($0)
rest = $0
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
sub(/[[:space:]]*(#.*)?$/, "", rest)
print_selected(spaces(ns_indent))
if (rest == "") skip_ns = 1
next
}
skip_ns {
cur_indent = indent_len($0)
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
skip_ns = 0
}
{ print }
END {
if (in_dns && !inserted)
print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
}
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
rm -f "$selected" "$tmp" 2>/dev/null
}
@@ -6,7 +6,7 @@ TMP_PREFIX="/tmp/clashoo_iprules.$$"
CUSTOM_RULE_FILE="${TMP_PREFIX}.ipadd.conf"
RULE="${TMP_PREFIX}.rules_conf.yaml"
CLASH="${TMP_PREFIX}.conf.yaml"
CONFIG_YAML="/etc/clashoo/config.yaml"
CONFIG_YAML="${CLASHOO_CONFIG_YAML:-/etc/clashoo/config.yaml}"
CLASH_CONFIG="${TMP_PREFIX}.config.yaml"
trap 'rm -f "$CUSTOM_RULE_FILE" "$RULE" "$CLASH" "$CLASH_CONFIG"' EXIT
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=ifb-shaper
PKG_VERSION:=0.1.1
PKG_RELEASE:=1
PKG_RELEASE:=2
include $(INCLUDE_DIR)/package.mk
+2 -2
View File
@@ -86,6 +86,6 @@ logread -e ifb-shaper # errors reported by tc
`tc`, `ip`, `kmod-ifb` (pulls in `kmod-sched-core`).
## Todo
## Luci Application
- Luci app.
- [`luci-app-ifb-shaper`](../../../luci/applications/luci-app-ifb-shaper)
+1 -1
View File
@@ -1 +1 @@
snapshot
both
@@ -1 +1 @@
26.287.39313~74c485a
26.288.56989~75a306d
+9
View File
@@ -0,0 +1,9 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for ifb-shaper
LUCI_DEPENDS:=+luci-base +ifb-shaper
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,253 @@
'use strict';
'require dom';
'require form';
'require ui';
'require uci';
'require view';
'require tools.widgets as widgets';
const RATE_UNITS = [ 'kbit', 'Mbit', 'Gbit' ];
/* split a tc rate like "500Mbit" into [ '500', 'Mbit' ]; unit is null if unparsable */
function splitRate(value) {
const str = String(value ?? '').trim();
const m = str.match(/^(\d+(?:\.\d+)?)\s*([A-Za-z]*)$/);
if (!m)
return [ str, null ];
/* a bare number is bit/s for tc, other units are kept as they are */
return [ m[1], RATE_UNITS.find(u => u.toLowerCase() === m[2].toLowerCase()) ?? (m[2] || 'bit') ];
}
/* number input + unit selector, the value is stored as e.g. "500Mbit" */
const RateInput = ui.AbstractElement.extend({
__init__(value, options) {
this.value = value;
this.options = Object.assign({ optional: true }, options);
},
render() {
const [ num, unit ] = splitRate(this.value);
const [ pnum, punit ] = splitRate(this.options.placeholder);
const inputEl = E('input', {
'type': 'text',
'class': 'cbi-input-text',
'inputmode': 'decimal',
'style': 'min-width:5em',
'placeholder': pnum,
'disabled': this.options.disabled ? '' : null,
'value': num
});
const selectEl = E('select', {
'class': 'cbi-input-select',
'style': 'flex:0 0 auto; width:5.5em; min-width:0; max-width:none',
'disabled': this.options.disabled ? '' : null
});
this.fillUnits(selectEl, unit ?? punit ?? RATE_UNITS[1]);
return this.bind(E('div', { 'id': this.options.id }, [
E('div', { 'class': 'control-group' }, [ inputEl, selectEl ])
]));
},
/* units outside of RATE_UNITS (bps, kibit, ...) are kept so they survive a save */
fillUnits(selectEl, current) {
const units = RATE_UNITS.slice();
if (!units.includes(current))
units.push(current);
dom.content(selectEl, units.map(u => E('option', { 'value': u }, u)));
selectEl.value = current;
},
bind(frameEl) {
const inputEl = frameEl.querySelector('input');
const selectEl = frameEl.querySelector('select');
this.node = frameEl;
this.setUpdateEvents(inputEl, 'keyup', 'blur');
this.setChangeEvents(inputEl, 'change');
this.setChangeEvents(selectEl, 'change');
this.registerEvents(selectEl, 'widget-update', [ 'change' ]);
dom.bindClassInstance(frameEl, this);
return frameEl;
},
getValue() {
const num = this.node.querySelector('input').value.trim();
return (num === '') ? '' : num + this.node.querySelector('select').value;
},
setValue(value) {
const [ num, unit ] = splitRate(value);
this.node.querySelector('input').value = num;
if (unit != null)
this.fillUnits(this.node.querySelector('select'), unit);
}
});
const RateValue = form.Value.extend({
renderWidget(section_id, option_index, cfgvalue) {
return new RateInput((cfgvalue != null) ? cfgvalue : this.default, {
id: this.cbid(section_id),
optional: this.optional || this.rmempty,
datatype: 'ufloat',
placeholder: this.placeholder,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
}).render();
}
});
return view.extend({
load: function() {
return uci.load('ifb-shaper');
},
render: function() {
let m, s, o;
m = new form.Map('ifb-shaper', _('IFB Shaper'),
_('Per-client traffic shaper. Download is shaped on the egress of the interface device, upload on an IFB device (ifb-&lt;device&gt;) that is created automatically.'));
/* shapers */
s = m.section(form.GridSection, 'shaper', _('Shapers'),
_('A shaper is bound to a logical interface; clients refer to it by the name of this section.'));
s.addremove = true;
s.anonymous = false;
s.nodescriptions = true;
s.modaltitle = _('Shaper');
s.renderSectionAdd = function(extra_class) {
const el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments);
const nameEl = el.querySelector('.cbi-section-create-name');
ui.addValidator(nameEl, 'uciname', true, function(v) {
if (uci.get('ifb-shaper', v) != null)
return _('This name is already used');
return true;
}, 'blur', 'keyup');
return el;
};
o = s.option(form.Flag, 'enabled', _('Enabled'));
o.default = o.enabled;
o.editable = true;
o = s.option(widgets.NetworkSelect, 'interface', _('Interface'));
o.nocreate = true;
o.rmempty = false;
o = s.option(form.DynamicList, 'bypass', _('Bypass networks'),
_('Traffic to and from these networks skips the client limits and goes to the local class.'));
o.datatype = 'or(cidr4,cidr6)';
o.placeholder = '192.168.0.0/24';
o.modalonly = true;
o = s.option(RateValue, 'total', _('Total rate'));
o.placeholder = '1000Mbit';
o.modalonly = true;
o = s.option(RateValue, 'down_total', _('Download total'),
_('Aggregate rate of all clients.'));
o.placeholder = '400Mbit';
o.modalonly = true;
o = s.option(RateValue, 'up_total', _('Upload total'),
_('Aggregate rate of all clients.'));
o.placeholder = '300Mbit';
o.modalonly = true;
o = s.option(RateValue, 'local_rate', _('Local class rate'));
o.placeholder = '50Mbit';
o.modalonly = true;
o = s.option(RateValue, 'local_ceil', _('Local class ceil'));
o.placeholder = '500Mbit';
o.modalonly = true;
o = s.option(RateValue, 'default_rate', _('Default class rate'),
_('Traffic that matches no client.'));
o.placeholder = '1Mbit';
o.modalonly = true;
o = s.option(RateValue, 'default_ceil', _('Default class ceil'));
o.placeholder = '15Mbit';
o.modalonly = true;
o = s.option(form.Value, 'leaf_qdisc', _('Leaf qdisc'),
_('tc arguments of the qdisc attached to every client sub-class, or <code>none</code>.'));
o.placeholder = 'fq_codel limit 1024 flows 128';
o.modalonly = true;
/* clients */
s = m.section(form.GridSection, 'client', _('Clients'),
_('Clients are matched from top to bottom: put specific hosts before wide subnets that contain them.'));
s.addremove = true;
s.anonymous = false;
s.nodescriptions = true;
s.sortable = true;
s.modaltitle = _('Client');
s.renderSectionAdd = function(extra_class) {
const el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments);
const nameEl = el.querySelector('.cbi-section-create-name');
ui.addValidator(nameEl, 'uciname', true, function(v) {
if (uci.get('ifb-shaper', v) != null)
return _('This name is already used');
return true;
}, 'blur', 'keyup');
return el;
};
o = s.option(form.ListValue, 'shaper', _('Shaper'));
o.rmempty = false;
o.load = function(section_id) {
this.keylist = [];
this.vallist = [];
uci.sections('ifb-shaper', 'shaper', sh => this.value(sh['.name']));
return form.ListValue.prototype.load.apply(this, [ section_id ]);
};
o = s.option(form.DynamicList, 'address', _('Addresses'),
_('Hosts or networks (IPv4 and/or IPv6) sharing one limit.'));
o.datatype = 'ipmask';
o.placeholder = '192.168.1.0/24';
o.rmempty = false;
o = s.option(RateValue, 'down_ceil', _('Download max'));
o.placeholder = '50Mbit';
o.rmempty = false;
o = s.option(RateValue, 'down_rate', _('Download guaranteed'));
o.placeholder = '1Mbit';
o.modalonly = true;
o = s.option(RateValue, 'up_ceil', _('Upload max'));
o.placeholder = '20Mbit';
o.rmempty = false;
o = s.option(RateValue, 'up_rate', _('Upload guaranteed'));
o.placeholder = '1Mbit';
o.modalonly = true;
return m.render();
}
});
+95
View File
@@ -0,0 +1,95 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language: ru\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "A shaper is bound to a logical interface; clients refer to it by the name of this section."
msgstr "Шейпер привязан к логическому интерфейсу; клиенты ссылаются на него по имени этой секции."
msgid "Addresses"
msgstr "Адреса"
msgid "Aggregate rate of all clients."
msgstr "Суммарная скорость всех клиентов."
msgid "Bypass networks"
msgstr "Сети в обход"
msgid "Client"
msgstr "Клиент"
msgid "Clients"
msgstr "Клиенты"
msgid "Clients are matched from top to bottom: put specific hosts before wide subnets that contain them."
msgstr "Клиенты сопоставляются сверху вниз: конкретные хосты ставьте выше широких подсетей, которые их содержат."
msgid "Default class ceil"
msgstr "Максимум класса по умолчанию"
msgid "Default class rate"
msgstr "Скорость класса по умолчанию"
msgid "Download guaranteed"
msgstr "Загрузка: гарантированная"
msgid "Download max"
msgstr "Загрузка: максимум"
msgid "Download total"
msgstr "Загрузка: общая"
msgid "Enabled"
msgstr "Включено"
msgid "Hosts or networks (IPv4 and/or IPv6) sharing one limit."
msgstr "Хосты или сети (IPv4 и/или IPv6) с одним общим лимитом."
msgid "IFB Shaper"
msgstr "IFB шейпер"
msgid "Interface"
msgstr "Интерфейс"
msgid "Leaf qdisc"
msgstr "Конечный qdisc"
msgid "Local class ceil"
msgstr "Максимум локального класса"
msgid "Local class rate"
msgstr "Скорость локального класса"
msgid "Per-client traffic shaper. Download is shaped on the egress of the interface device, upload on an IFB device (ifb-&lt;device&gt;) that is created automatically."
msgstr "Шейпер трафика по клиентам. Загрузка ограничивается на egress устройства интерфейса, отдача — на IFB-устройстве (ifb-&lt;device&gt;), которое создаётся автоматически."
msgid "Shaper"
msgstr "Шейпер"
msgid "Shapers"
msgstr "Шейперы"
msgid "This name is already used"
msgstr "Это имя уже используется"
msgid "Total rate"
msgstr "Общая скорость"
msgid "Traffic that matches no client."
msgstr "Трафик, не попавший ни под одного клиента."
msgid "Traffic to and from these networks skips the client limits and goes to the local class."
msgstr "Трафик в эти сети и из них минует лимиты клиентов и попадает в локальный класс."
msgid "Upload guaranteed"
msgstr "Отдача: гарантированная"
msgid "Upload max"
msgstr "Отдача: максимум"
msgid "Upload total"
msgstr "Отдача: общая"
msgid "tc arguments of the qdisc attached to every client sub-class, or <code>none</code>."
msgstr "Аргументы tc для qdisc, который вешается на каждый подкласс клиента, либо <code>none</code>."
@@ -0,0 +1,14 @@
{
"admin/network/ifb-shaper": {
"title": "IFB Shaper",
"order": 60,
"action": {
"type": "view",
"path": "ifb-shaper/config"
},
"depends": {
"acl": [ "luci-app-ifb-shaper" ],
"uci": { "ifb-shaper": true }
}
}
}
@@ -0,0 +1,11 @@
{
"luci-app-ifb-shaper": {
"description": "Grant UCI access for luci-app-ifb-shaper",
"read": {
"uci": [ "ifb-shaper" ]
},
"write": {
"uci": [ "ifb-shaper" ]
}
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.334
PKG_RELEASE:=31
PKG_VERSION:=0.1.336
PKG_RELEASE:=32
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.16
PKG_RELEASE:=334
PKG_RELEASE:=335
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -710,13 +710,12 @@ if has_smartdns then
o:value("notice", "Notice")
o:value("info", "Info")
o:value("debug", "Debug")
o:depends({dns_shunt = "smartdns", log_smartdns = "1"})
o:depends("log_smartdns", "1")
o = s:taboption("log", Value, "smartdns_log_size", translate("Log Size") .. " (MB)")
o.default = "1"
o.datatype = "and(uinteger,min(1))"
o.rmempty = false
o:depends({dns_shunt = "smartdns", log_smartdns = "1"})
o:depends("log_smartdns", "1")
o = s:taboption("log", DummyValue, "_smartdns_log", translate("Log File"))
o.rawhtml = true
@@ -734,7 +733,7 @@ if has_smartdns then
end
return s
end
o:depends({dns_shunt = "smartdns", log_smartdns = "1"})
o:depends("log_smartdns", "1")
if api.fs.access("/usr/lib/smartdns_ui.so") or api.fs.access("/usr/lib/smartdns/smartdns_ui.so") then
o = s:taboption("log", Flag, "smartdns_ui", "SmartDNS " .. translate("Enable WebUI"))
@@ -745,12 +744,10 @@ if has_smartdns then
o = s:taboption("log", Value, "smartdns_ui_port", translate("WebUI Port"))
o.default = "9880"
o.datatype = "range(1,65535)"
o.rmempty = false
o:depends({dns_shunt = "smartdns", smartdns_ui = "1"})
o:depends("smartdns_ui", "1")
o = s:taboption("log", Value, "smartdns_ui_data_dir", translate("WebUI Database Directory"))
o.default = api.CACHE_PATH .. "/smartdns"
o.rmempty = false
o.validate = function(self, value, section)
value = api.trim(value)
if value:sub(1, 1) ~= "/" or value:find("%s") then
@@ -758,13 +755,12 @@ if has_smartdns then
end
return value
end
o:depends({dns_shunt = "smartdns", smartdns_ui = "1"})
o:depends("smartdns_ui", "1")
o = s:taboption("log", Value, "smartdns_ui_log_max_age", translate("WebUI Log Retention Days"))
o.default = "7"
o.datatype = "and(uinteger,min(1))"
o.rmempty = false
o:depends({dns_shunt = "smartdns", smartdns_ui = "1"})
o:depends("smartdns_ui", "1")
o = s:taboption("log", DummyValue, "_smartdns_ui_link", "WebUI")
o.rawhtml = true
@@ -776,7 +772,7 @@ if has_smartdns then
translate("Open WebUI"), port
)
end
o:depends({dns_shunt = "smartdns", smartdns_ui = "1"})
o:depends("smartdns_ui", "1")
end
end
+5 -3
View File
@@ -178,9 +178,11 @@ function get_cache_var(key)
return val
end
function get_new_port()
local cmd_format = ". /usr/share/passwall/utils.sh ; echo -n $(get_new_port %s tcp,udp)"
return tonumber(sys.exec(string.format(cmd_format, "auto")))
function get_new_port(start_port, protocol)
start_port = tonumber(start_port) or "auto"
if protocol ~= "tcp" and protocol ~= "udp" then protocol = "tcp,udp" end
local cmd_format = ". /usr/share/passwall/utils.sh ; echo -n $(get_new_port %s %s)"
return tonumber(sys.exec(string.format(cmd_format, util.shellquote(tostring(start_port)), util.shellquote(protocol))))
end
function exec_call(cmd)
@@ -17,12 +17,15 @@
position: sticky;
bottom: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .3);
color: #525f7f;
background: #fff;
z-index: 99;
box-sizing: border-box;
}
html[data-darkmode="true"] .status-bar {
background: #2f2f2fcc;
}
.status-bar .inner {
padding: 0.5em;
}
@@ -181,6 +181,15 @@ local only_global = (DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_G
if only_global == 1 then
DEFAULT_DNS_GROUP = "proxy"
end
local AUTO_DIRECT_PORT = 0
if DEFAULT_DNS_GROUP == AUTO_GROUP then
AUTO_DIRECT_PORT = api.get_new_port(15350)
-- 避开同一进程尚未启动的主监听和直连监听端口。
for _ = 1, 2 do
if not AUTO_DIRECT_PORT or (AUTO_DIRECT_PORT ~= tonumber(LISTEN_PORT) and AUTO_DIRECT_PORT ~= LOCAL_PORT) then break end
AUTO_DIRECT_PORT = api.get_new_port(AUTO_DIRECT_PORT + 1)
end
end
local force_https_soa = api.uci_get_c("@global[0]", "force_https_soa") or 0
local force_qtype_soa = tonumber(force_https_soa) == 1 and "force-qtype-SOA 65" or "force-qtype-SOA -,65"
local proxy_server_name = "psw-proxy-server"
@@ -191,6 +200,7 @@ config_lines = {
tonumber(LISTEN_PORT) ~= 0 and ("bind-tcp " .. listen_address) or "",
LOCAL_PORT > 0 and ("bind [::]:" .. LOCAL_PORT .. "@lo -group direct -no-rules") or "",
LOCAL_PORT > 0 and ("bind-tcp [::]:" .. LOCAL_PORT .. "@lo -group direct -no-rules") or "",
AUTO_DIRECT_PORT > 0 and ("bind [::]:" .. AUTO_DIRECT_PORT .. "@lo -group auto-direct") or "",
"log-level " .. LOG_LEVEL,
"log-num " .. (LOG and "1" or "0"),
LOG and string.format("log-file %q", FLAG_PATH .. "/smartdns.log") or "",
@@ -236,10 +246,13 @@ if DNS_REDIRECT then
end
for _, server_param in ipairs(DIRECT_SERVERS) do
table.insert(config_lines, server_param .. " -group direct -exclude-default-group")
if DEFAULT_DNS_GROUP == AUTO_GROUP then
-- 自动组的直连副本仅接受大陆 IP,普通直连组保持默认行为。
table.insert(auto_config_lines, server_param .. " -group " .. AUTO_GROUP .. " -exclude-default-group -whitelist-ip")
end
end
if DEFAULT_DNS_GROUP == AUTO_GROUP then
-- 自动组先通过独立入口去除 CNAME,再按原逻辑筛选 IP。
table.insert(auto_config_lines, "server 127.0.0.1:" .. AUTO_DIRECT_PORT .. " -group " .. AUTO_GROUP .. " -exclude-default-group -whitelist-ip")
table.insert(auto_config_lines, "group-begin auto-direct -inherit none")
merge_array(auto_config_lines, DIRECT_SERVERS)
merge_array(auto_config_lines, {"force-no-CNAME yes", "group-end"})
end
-- 由下方 DNS_MODE 分支调用,服务器参数来自 PassWall 的 REMOTE_DNS 或 TUN_DNS。
@@ -60,9 +60,26 @@ local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria")
local DEFAULT_ALLOWINSECURE = true
local DEFAULT_FILTER_KEYWORD_MODE = uci_get("@global_subscribe[0]", "filter_keyword_mode") or "0"
local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci_get("@global_subscribe[0]", "filter_discard_list") or {}
local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci_get("@global_subscribe[0]", "filter_keep_list") or {}
-- 展开 DynamicList 每行中的关键字,保留原有空格并忽略空项和重复项。
local function split_filter_keywords(list)
local keywords, seen = {}, {}
if type(list) == "string" then list = {list} end
for _, row in ipairs(list or {}) do
for keyword in row:gmatch("[^|]+") do
if not seen[keyword] then
seen[keyword] = true
keywords[#keywords + 1] = keyword
end
end
end
return keywords
end
local DEFAULT_FILTER_KEYWORD = {
mode = uci_get("@global_subscribe[0]", "filter_keyword_mode") or "0",
discard_list = split_filter_keywords(uci_get("@global_subscribe[0]", "filter_discard_list")),
keep_list = split_filter_keywords(uci_get("@global_subscribe[0]", "filter_keep_list"))
}
-- 取节点使用core类型(节点订阅页面未设置时,自动取默认)
local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
@@ -75,71 +92,38 @@ local core_has = {
["shadowsocks-rust"] = has_ss_rust,
["hysteria2"] = has_hysteria2
}
-- 判断是否过滤节点关键字
local function is_filter_keyword(sub_cfg, value)
local mode = DEFAULT_FILTER_KEYWORD_MODE
local discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST
local keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST
if sub_cfg then
local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global
if filter_keyword_mode == "0" then
mode = "0"
elseif filter_keyword_mode == "1" then
mode = "1"
discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "2" then
mode = "2"
keep_list = sub_cfg.filter_keep_list or {}
elseif filter_keyword_mode == "3" then
mode = "3"
keep_list = sub_cfg.filter_keep_list or {}
discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "4" then
mode = "4"
keep_list = sub_cfg.filter_keep_list or {}
discard_list = sub_cfg.filter_discard_list or {}
-- 每个订阅只准备一次过滤配置,5 / 未设置时继承全局。
local function prepare_keyword_filter(sub_cfg)
local mode = sub_cfg and sub_cfg.filter_keyword_mode
if mode == "0" or mode == "1" or mode == "2" or mode == "3" or mode == "4" then
return {
mode = mode,
discard_list = split_filter_keywords(sub_cfg.filter_discard_list),
keep_list = split_filter_keywords(sub_cfg.filter_keep_list)
}
end
return DEFAULT_FILTER_KEYWORD
end
local function match_filter_keywords(value, keywords)
for _, keyword in ipairs(keywords) do
if value:find(keyword, 1, true) then
return true
end
end
if mode == "1" then
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
return true
end
end
elseif mode == "2" then
local result = true
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif mode == "3" then
local result = false
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
end
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif mode == "4" then
local result = true
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
end
return result
return false
end
-- 节点解析出备注后,再按当前订阅的配置判断是否丢弃。
local function is_filter_keyword(filter, value)
if filter.mode == "1" then
return match_filter_keywords(value, filter.discard_list)
elseif filter.mode == "2" then
return not match_filter_keywords(value, filter.keep_list)
elseif filter.mode == "3" then
return match_filter_keywords(value, filter.discard_list) and not match_filter_keywords(value, filter.keep_list)
elseif filter.mode == "4" then
return match_filter_keywords(value, filter.discard_list) or not match_filter_keywords(value, filter.keep_list)
end
return false
end
@@ -2146,6 +2130,7 @@ local function parse_link(raw, add_mode, group, sub_cfg)
if sub_cfg then
cfgid = sub_cfg[".name"]
end
local keyword_filter = add_mode == "2" and prepare_keyword_filter(sub_cfg)
local nodes, szType
local node_list = {}
-- SSD 似乎是这种格式 ssd:// 开头的
@@ -2202,7 +2187,7 @@ local function parse_link(raw, add_mode, group, sub_cfg)
log('丢弃节点:' .. result.remarks .. " ,原因:" .. result.error_msg)
elseif not result.type then
log('丢弃节点:' .. result.remarks .. " ,找不到可使用二进制。")
elseif (add_mode == "2" and is_filter_keyword(sub_cfg, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
elseif (add_mode == "2" and is_filter_keyword(keyword_filter, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
(not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then
log('丢弃过滤节点:' .. result.type .. ' 节点,' .. result.remarks)
else
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.10.1
PKG_RELEASE:=142
PKG_RELEASE:=143
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -10,6 +10,14 @@ s = m:section(NamedSection, "@global_app[0]", "global_app", translate("App Updat
s:appendTemplate("/app_update/app_version", {com = com})
o = s:option(ListValue, "gh_proxy_url", translate("GitHub Proxy"), translate("Use GitHub instead of proxy nodes for component updates."))
o.default = ""
o:value("", translate("Close(Not use)"))
o:value("https://gh-proxy.org/", "gh-proxy.org")
o:value("https://ghproxy.cc/", "ghproxy.cc")
o:value("https://mirror.ghproxy.com/", "ghproxy.com")
o:value("https://ghfast.top/", "ghfast.top")
local k, v
for k, v in pairs(com) do
o = s:option(Value, k:gsub("%-","_") .. "_file", translatef("%s App Path", v.name))
+24 -3
View File
@@ -1170,7 +1170,14 @@ local default_file_tree = {
}
function get_api_json(url)
local return_code, content = curl_auto(url, nil, curl_args)
local gh_proxy = uci_get_c("@global_app[0]", "gh_proxy_url") or ""
local return_code, content
if gh_proxy ~= "" then
url = gh_proxy .. url
return_code, content = curl_direct(url, nil, curl_args)
else
return_code, content = curl_auto(url, nil, curl_args)
end
if return_code ~= 0 or content == "" then return {} end
return jsonc.parse(content) or {}
end
@@ -1293,7 +1300,14 @@ function to_download(app_name, url, size, task_id)
local _curl_args = clone(curl_args)
table.insert(_curl_args, "--speed-limit 51200 --speed-time 15 --max-time 300")
local return_code, result = curl_auto(url, tmp_file, _curl_args)
local gh_proxy = uci_get_c("@global_app[0]", "gh_proxy_url") or ""
local return_code, result
if gh_proxy ~= "" then
url = gh_proxy .. url
return_code, result = curl_direct(url, tmp_file, _curl_args)
else
return_code, result = curl_auto(url, tmp_file, _curl_args)
end
result = return_code == 0
if not result then
@@ -1461,7 +1475,14 @@ end
function to_check_self()
local url = "https://raw.githubusercontent.com/Openwrt-Passwall/openwrt-passwall2/main/luci-app-passwall2/Makefile"
local tmp_file = "/tmp/passwall2_makefile"
local return_code, result = curl_auto(url, tmp_file, curl_args)
local gh_proxy = uci_get_c("@global_app[0]", "gh_proxy_url") or ""
local return_code, result
if gh_proxy ~= "" then
url = gh_proxy .. url
return_code, result = curl_direct(url, tmp_file, curl_args)
else
return_code, result = curl_auto(url, tmp_file, curl_args)
end
result = return_code == 0
if not result then
exec("/bin/rm", {"-f", tmp_file})
@@ -17,12 +17,15 @@
position: sticky;
bottom: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .3);
color: #525f7f;
background: #fff;
z-index: 99;
box-sizing: border-box;
}
html[data-darkmode="true"] .status-bar {
background: #2f2f2fcc;
}
.status-bar .inner {
padding: 0.5em;
}
+10 -1
View File
@@ -442,7 +442,7 @@ msgid "Failure Tolerance (%)"
msgstr "تحمل شکست (%)"
msgid "The maximum acceptable speed test failure rate. For example, 1 means allowing a 1% failure rate."
msgstr "حداکثر نرخ شکست قابل قبول در تست سرعت. برای مثال، عدد ۱ به معنای مجاز بودن نرخ شکست ۱٪ است.
msgstr "حداکثر نرخ شکست قابل قبول در تست سرعت. برای مثال، عدد ۱ به معنای مجاز بودن نرخ شکست ۱٪ است."
msgid "Shunt"
msgstr "شنت (Shunt)"
@@ -1009,6 +1009,15 @@ msgstr "در حال انتقال..."
msgid "App Update"
msgstr "به‌روزرسانی برنامه"
msgid "GitHub Proxy"
msgstr "پروکسی معکوس GitHub"
msgid "Use GitHub instead of proxy nodes for component updates."
msgstr "برای بررسی و دانلود به‌روزرسانی اجزا، مستقیماً از سرویس پروکسی معکوس GitHub استفاده کنید، بدون عبور از گره‌های پروکسی."
msgid "Close(Not use)"
msgstr "خاموش (عدم استفاده)"
msgid "if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."
msgstr "اگر می‌خواهید از حافظه اجرا کنید، مسیر را تغییر دهید، با /tmp شروع شود سپس برنامه را ذخیره و دستی به‌روزرسانی کنید."
+10 -1
View File
@@ -1010,6 +1010,15 @@ msgstr "Перемещение..."
msgid "App Update"
msgstr "Обновление компонентов"
msgid "GitHub Proxy"
msgstr "Обратный прокси для GitHub"
msgid "Use GitHub instead of proxy nodes for component updates."
msgstr "При обновлении компонентов проверять и загружать их напрямую через сервис обратного прокси для GitHub, без использования прокси-узлов."
msgid "Close(Not use)"
msgstr "Отключено (не использовать)"
msgid "if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."
msgstr "Если вы хотите запускать из оперативной памяти, измените путь на начинающийся с /tmp, затем сохраните настройки и обновите компонент вручную."
@@ -2443,7 +2452,7 @@ msgid "Gecko Packet Size (max)"
msgstr "Размер упаковки Gecko (макс)"
msgid "Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."
msgstr "Включите сопоставление портов UPnP/NAT-PMP на вашем шлюзе, чтобы повысить вероятность успешного выполнения операций "пробивания дыр"."
msgstr "Включите сопоставление портов UPnP/NAT-PMP на вашем шлюзе, чтобы повысить вероятность успешного выполнения операций \"пробивания дыр\"."
msgid "valid time (hh:mm)"
msgstr "Время действия (чч:мм)"
+9
View File
@@ -1001,6 +1001,15 @@ msgstr "移动中"
msgid "App Update"
msgstr "组件更新"
msgid "GitHub Proxy"
msgstr "GitHub 反向代理"
msgid "Use GitHub instead of proxy nodes for component updates."
msgstr "组件更新时使用 GitHub 反向代理服务直连检查和下载,不通过代理节点。"
msgid "Close(Not use)"
msgstr "关闭(不使用)"
msgid "if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."
msgstr "如果你希望从内存中运行,请更改路径,/tmp 开头,然后保存应用后,再手动更新。"
+9
View File
@@ -1001,6 +1001,15 @@ msgstr "移動中"
msgid "App Update"
msgstr "组件更新"
msgid "GitHub Proxy"
msgstr "GitHub 反向代理"
msgid "Use GitHub instead of proxy nodes for component updates."
msgstr "元件更新時使用 GitHub 反向代理服務直接檢查與下載,不經過代理節點。"
msgid "Close(Not use)"
msgstr "關閉(不使用)"
msgid "if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."
msgstr "如果你希望从內存中運行,請更改路径,/tmp 開头,然後保存應用後,再手動更新。"
@@ -1,35 +1,39 @@
#!/bin/sh
[ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ] || exit 0
[ -n "$DEVICE" ] || exit 0
. /usr/share/passwall2/utils.sh
([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && {
[ -f "${LOCK_PATH}/${CONFIG}_ready.lock" ] || exit 0
[ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ] || exit 0
default_device=$(ip route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
default6_device=$(ip -6 route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
( [ "$default_device" = "$DEVICE" ] || [ "$default6_device" = "$DEVICE" ] ) && {
default_device=$(ip route show default 2>/dev/null | awk '{ for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); break } exit }')
default6_device=$(ip -6 route show default 2>/dev/null | awk '{ for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); break } exit }')
[ "$default_device" = "$DEVICE" ] || [ "$default6_device" = "$DEVICE" ] || exit 0
if [ "$ACTION" = "ifupdate" ]; then
USE_TABLES=$(get_cache_var "USE_TABLES")
[ -n "$USE_TABLES" ] && { ${APP_PATH}/${USE_TABLES}.sh update_wan_sets >/dev/null 2>&1 & }
exit 0
fi
if [ "$ACTION" = "ifupdate" ]; then
USE_TABLES=$(get_cache_var "USE_TABLES")
[ -n "$USE_TABLES" ] && { "${APP_PATH}/${USE_TABLES}.sh" update_wan_sets >/dev/null 2>&1 & }
exit 0
fi
[ ! -d ${LOCK_PATH} ] && mkdir -p ${LOCK_PATH}
LOCK_FILE="${LOCK_PATH}/${CONFIG}_ifup.lock"
if [ -s ${LOCK_FILE} ]; then
SPID=$(cat ${LOCK_FILE})
if [ -e /proc/${SPID}/status ]; then
exit 1
fi
cat /dev/null > ${LOCK_FILE}
fi
echo $$ > ${LOCK_FILE}
/etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
logger -p notice -t network -s "${CONFIG}: restart when $INTERFACE ifup"
rm -rf ${LOCK_FILE}
}
mkdir -p "$LOCK_PATH" || {
logger -p err -t network -s "${CONFIG}: cannot create lock directory $LOCK_PATH"
exit 1
}
(
command exec 9>"${LOCK_PATH}/${CONFIG}_ifup.lock" || {
logger -p err -t network -s "${CONFIG}: cannot open ifup lock"
exit 1
}
flock -xn 9 || exit 0
# Keep this lock file: removing it would allow locks on different inodes.
# The worker holds the lock until restart returns; children must not inherit it.
logger -p notice -t network -s "${CONFIG}: restart when $INTERFACE ifup" 9>&-
"/etc/init.d/${CONFIG}" restart 9>&-
) </dev/null >/dev/null 2>&1 &
exit 0
@@ -30,9 +30,26 @@ local has_ssr = api.is_finded("ssr-local") and api.is_finded("ssr-redir")
local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray")
local DEFAULT_ALLOWINSECURE = true
local DEFAULT_FILTER_KEYWORD_MODE = uci_get("@global_subscribe[0]", "filter_keyword_mode") or "0"
local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci_get("@global_subscribe[0]", "filter_discard_list") or {}
local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci_get("@global_subscribe[0]", "filter_keep_list") or {}
-- Expand each DynamicList row, preserving spaces and ignoring empty or duplicate keywords.
local function split_filter_keywords(list)
local keywords, seen = {}, {}
if type(list) == "string" then list = {list} end
for _, row in ipairs(list or {}) do
for keyword in row:gmatch("[^|]+") do
if not seen[keyword] then
seen[keyword] = true
keywords[#keywords + 1] = keyword
end
end
end
return keywords
end
local DEFAULT_FILTER_KEYWORD = {
mode = uci_get("@global_subscribe[0]", "filter_keyword_mode") or "0",
discard_list = split_filter_keywords(uci_get("@global_subscribe[0]", "filter_discard_list")),
keep_list = split_filter_keywords(uci_get("@global_subscribe[0]", "filter_keep_list"))
}
-- Nodes should be retrieved using the core type (if not set on the node subscription page, the default type will be used automatically).
local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
@@ -44,71 +61,38 @@ local core_has = {
["sing-box"] = has_singbox,
["shadowsocks-rust"] = has_ss_rust,
}
-- Determine whether to filter node keywords
local function is_filter_keyword(sub_cfg, value)
local mode = DEFAULT_FILTER_KEYWORD_MODE
local discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST
local keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST
if sub_cfg then
local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global
if filter_keyword_mode == "0" then
mode = "0"
elseif filter_keyword_mode == "1" then
mode = "1"
discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "2" then
mode = "2"
keep_list = sub_cfg.filter_keep_list or {}
elseif filter_keyword_mode == "3" then
mode = "3"
keep_list = sub_cfg.filter_keep_list or {}
discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "4" then
mode = "4"
keep_list = sub_cfg.filter_keep_list or {}
discard_list = sub_cfg.filter_discard_list or {}
-- Prepare once per subscription; mode 5 or an unset mode inherits the global filter.
local function prepare_keyword_filter(sub_cfg)
local mode = sub_cfg and sub_cfg.filter_keyword_mode
if mode == "0" or mode == "1" or mode == "2" or mode == "3" or mode == "4" then
return {
mode = mode,
discard_list = split_filter_keywords(sub_cfg.filter_discard_list),
keep_list = split_filter_keywords(sub_cfg.filter_keep_list)
}
end
return DEFAULT_FILTER_KEYWORD
end
local function match_filter_keywords(value, keywords)
for _, keyword in ipairs(keywords) do
if value:find(keyword, 1, true) then
return true
end
end
if mode == "1" then
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
return true
end
end
elseif mode == "2" then
local result = true
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif mode == "3" then
local result = false
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
end
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif mode == "4" then
local result = true
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
end
return result
return false
end
-- Decide whether to discard a parsed node using the current subscription's filter.
local function is_filter_keyword(filter, value)
if filter.mode == "1" then
return match_filter_keywords(value, filter.discard_list)
elseif filter.mode == "2" then
return not match_filter_keywords(value, filter.keep_list)
elseif filter.mode == "3" then
return match_filter_keywords(value, filter.discard_list) and not match_filter_keywords(value, filter.keep_list)
elseif filter.mode == "4" then
return match_filter_keywords(value, filter.discard_list) or not match_filter_keywords(value, filter.keep_list)
end
return false
end
@@ -2520,6 +2504,7 @@ local function parse_link(raw, add_mode, group, sub_cfg)
if sub_cfg then
cfgid = sub_cfg[".name"]
end
local keyword_filter = add_mode == "2" and prepare_keyword_filter(sub_cfg)
local nodes, szType, clashTable
local node_list = {}
-- ssd appear to be in this format, starting with ssd://.
@@ -2563,7 +2548,7 @@ local function parse_link(raw, add_mode, group, sub_cfg)
log(2, i18n.translatef("Discard node: %s, Reason:", node.remarks) .. " " .. node.error_msg)
elseif not node.type then
log(2, i18n.translatef("Discard node: %s, Reason:", node.remarks) .. " " .. i18n.translatef("No usable binary was found."))
elseif (add_mode == "2" and is_filter_keyword(sub_cfg, node.remarks)) or not node.address or node.remarks == "NULL" or node.address == "127.0.0.1" or
elseif (add_mode == "2" and is_filter_keyword(keyword_filter, node.remarks)) or not node.address or node.remarks == "NULL" or node.address == "127.0.0.1" or
(not datatypes.hostname(node.address) and not (api.is_ip(node.address))) then
log(2, i18n.translatef("Discard filter nodes: %s type node %s", node.type, node.remarks))
else
+1 -1
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196
PKG_RELEASE:=89
PKG_RELEASE:=90
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -17,12 +17,15 @@
position: sticky;
bottom: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .3);
color: #525f7f;
background: #fff;
z-index: 99;
box-sizing: border-box;
}
html[data-darkmode="true"] .status-bar {
background: #2f2f2fcc;
}
.status-bar .inner {
padding: 0.5em;
}
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-zerotier
PKG_VERSION:=2.2
PKG_RELEASE:=9
PKG_RELEASE:=10
PKG_LICENSE:=Apache-2.0
LUCI_TITLE:=LuCI for ZeroTier
+27 -1
View File
@@ -8,6 +8,8 @@ EEOF
}
# luci-app-zerotier (JavaScript Edition)
**English** | [简体中文](README_zh-CN.md)
A modern LuCI interface for ZeroTier VPN on OpenWrt, converted from the original Lua version and continuously improved.
Forked from https://github.com/zhengmz/luci-app-zerotier, initially converted to JavaScript using AUGMENT Code plugin in VS Code, then further refined and bug-fixed using [OpenCode](https://opencode.ai) + GLM-5.1.
@@ -52,7 +54,7 @@ The custom `luci-zerotier` RPC object provides these methods (no `luci.exec` nee
| `remote_network_list` | read | Every network's full record in a single tunnel round trip |
| `remote_peer_list` | read | The controller's live `/peer` list (status, version, latency, paths) |
| `remote_moon_plan` | read | The exact remote command a moon creation would run, plus a digest |
| `remote_host_set` | write | Add/update a remote host; writes the private key to a `0600` file |
| `remote_host_set` | write | Add/update a remote host; generates a dedicated key pair when none is stored and returns the public key (pasting a private key still works for API callers) |
| `remote_host_del` | write | Remove a remote host and its key file |
| `remote_network_set` | write | Create or update a network on the remote controller |
| `remote_network_del` | write | Delete a network on the remote controller |
@@ -257,6 +259,30 @@ stderr and continues. Two consequences are handled explicitly:
16px row font). Single adaptation from the supplied SVG: the stroke
follows `currentColor` instead of hard black so the glyph stays visible
under the dark theme.
- Host list polish (no release bump): each configured host's clickable
name now renders at the page-title scale -- it is a real h2 (argon:
1.25rem, same as the "ZeroTier - Remote Controller" heading) with only
the card chrome (padding/white background/shadow) neutralised inline,
so typography stays the theme's. The monospace connection line below
is unchanged, and host blocks gain margin-bottom so consecutive hosts
no longer touch.
- Add-host flow redesigned (no release bump): pasting an SSH private key
into the browser is gone. Saving an SSH-mode host with no stored key now
generates a dedicated ed25519 pair on the router (dropbear's ssh-keygen
compat binary, verified present in every default OpenWrt image via the
dropbear package's unconditional ALTERNATIVES entry) and returns the
public half, which a modal shows as a ready-to-copy
`echo '<pub>' >> ~/.ssh/authorized_keys` command -- the operator runs it
on the remote host, which they already have their own access to. The
private half never crosses the browser. Editing a host shows its stored
public key instead. The controller-port field is likewise gone from SSH
mode: with nothing stored, the helper reads the remote daemon's
`zerotier-one.port` over the same SSH access (the controller API shares
that port), commits the finding to UCI, and every later call reads it
like a typed value. Direct mode still requires the port (no SSH to
discover with). Verified end to end on-device: key generation, clean-HOME
authentication with the generated key, controller read through the app
stack, and the discovered port (27893) landing in UCI. 32/32 in-browser.
- Semantics confirmed (no release bump): the trash can and the
authorized checkbox are DELIBERATELY different. The checkbox posts
`{authorized:false}` only -- name and IP assignments survive the
File diff suppressed because it is too large. Load diff
@@ -59,6 +59,50 @@ function yn(v, t, f) {
/* ------------------------------------------------------------------ hosts */
/* LuCI on the LAN is plain http, where the async clipboard API is withheld
* (secure-context only): fall back to select + execCommand. The API path
* also degrades to the fallback when the browser denies the permission. */
function copyText(t) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(t).catch(function() { copyTextFallback(t); });
return;
}
copyTextFallback(t);
}
function copyTextFallback(t) {
var ta = E('textarea', { 'style': 'position:fixed; top:-100px; left:-100px;' });
ta.value = t;
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
}
/* Shown after a save that generated a fresh key pair: the private half never
* crossed the browser, and the operator adds the public half to the remote's
* authorized_keys -- which they can do because they already have their own
* access to that host. */
function pubkeyModal(pub, name) {
var cmd = "echo '" + pub.replace(/\s+$/, '') + "' >> ~/.ssh/authorized_keys";
var cmdI = E('input', { 'type': 'text', 'readonly': 'readonly', 'value': cmd,
'style': 'width:100%; box-sizing:border-box; font-family:monospace; font-size:12px; padding:6px;' });
var msg = E('div', { 'style': 'color:green; min-height:18px; margin-top:4px;' });
var copyBtn = E('button', { 'class': 'cbi-button cbi-button-action' }, [ _('Copy command') ]);
copyBtn.addEventListener('click', function() {
copyText(cmd);
msg.textContent = _('Copied. Run it on the remote host.');
});
var doneBtn = E('button', { 'class': 'cbi-button', 'click': function() { ui.hideModal(); } }, [ _('Done') ]);
ui.showModal(_('Authorize this router on the remote host'), E('div', { 'style': 'max-width:560px;' }, [
E('p', {}, [ _('A dedicated key pair was generated on this router for host "%s". The private half never leaves this router. You already have access to the remote host, so run this command there to authorize the connection:').replace('%s', name) ]),
cmdI,
msg,
E('div', { 'style': 'margin-top:10px; display:flex; gap:8px; justify-content:flex-end;' }, [ copyBtn, doneBtn ])
]));
cmdI.focus();
cmdI.select();
}
function hostEditor(section, host, onSaved) {
var isNew = !section;
var f = section || '';
@@ -72,13 +116,11 @@ function hostEditor(section, host, onSaved) {
modeSel.value = (host && host.mode) || 'ssh';
var portI = E('input', { 'type': 'number', 'id': 'zt_r_port', 'value': (host && host.port) || '22', 'min': '1', 'max': '65535' });
var userI = E('input', { 'type': 'text', 'id': 'zt_r_user', 'value': (host && host.user) || 'root' });
var cportI = E('input', { 'type': 'number', 'id': 'zt_r_cport', 'value': (host && host.controller_port) || '27893', 'min': '1', 'max': '65535' });
var keyI = E('textarea', {
'id': 'zt_r_key', 'rows': '6', 'style': 'width:100%; font-family:monospace; font-size:12px;',
'placeholder': isNew
? _('Required: paste the PRIVATE key of a keypair whose public half is in the remote ~/.ssh/authorized_keys')
: _('Leave blank to keep the stored key')
});
var cportI = E('input', { 'type': 'number', 'id': 'zt_r_cport', 'value': (host && host.controller_port) || '9993', 'min': '1', 'max': '65535' });
var pubI = (host && host.pubkey)
? E('input', { 'type': 'text', 'readonly': 'readonly', 'value': host.pubkey,
'style': 'width:100%; box-sizing:border-box; font-family:monospace; font-size:12px;' })
: null;
var apiI = E('input', {
'type': 'password', 'style': 'width:100%; font-family:monospace;',
'placeholder': isNew
@@ -95,15 +137,20 @@ function hostEditor(section, host, onSaved) {
saveBtn.disabled = true;
var sectName = f || ('zt' + Date.now().toString(36));
var m = modeSel.value;
/* private_key is always '' now: a dedicated key pair is generated on
* the router when the first SSH-mode save finds none stored. The
* field survives in the RPC contract for API callers only. */
L.resolveDefault(rpcHostSet(
sectName, nameI.value, hostI.value, portI.value,
userI.value, cportI.value,
(m === 'ssh') ? keyI.value : '',
userI.value, (m === 'direct') ? cportI.value : '',
'',
m, (m === 'direct') ? apiI.value : ''), {})
.then(function(res) {
if (res && res.code === 0) {
hostEditor.close();
onSaved();
if (m === 'ssh' && res.key_generated && res.pubkey)
pubkeyModal(res.pubkey, nameI.value || sectName);
} else {
msg.textContent = errText(res, _('Could not save the host'));
saveBtn.disabled = false;
@@ -117,7 +164,22 @@ function hostEditor(section, host, onSaved) {
var modeRow = E('div', { 'class': 'cbi-value' }, [ E('label', {}, [_('Access mode')]), E('div', {}, [ modeSel ]) ]);
var portRow = E('div', { 'class': 'cbi-value' }, [ E('label', {}, [_('SSH port')]), E('div', {}, [ portI ]) ]);
var userRow = E('div', { 'class': 'cbi-value' }, [ E('label', {}, [_('SSH user')]), E('div', {}, [ userI ]) ]);
var keyRow = E('div', { 'class': 'cbi-value' }, [ E('label', {}, [_('SSH private key')]), E('div', {}, [ keyI ]) ]);
var keyRow = E('div', { 'class': 'cbi-value' }, [ E('label', {}, [_('SSH public key')]), E('div', {}, pubI ? [
pubI,
E('div', { 'class': 'cbi-value-description' }, [
_("The public half of this router's dedicated key pair. Add it to the remote host's ~/.ssh/authorized_keys to authorize this router.")
])
] : [
E('div', { 'class': 'cbi-value-description' }, [
_('No private key needed: when you save, this router generates a dedicated key pair and shows you the public key and the exact command to run on the remote host.')
])
]) ]);
var cportRow = E('div', { 'class': 'cbi-value' }, [ E('label', {}, [_('Controller port')]), E('div', {}, [
cportI,
E('div', { 'class': 'cbi-value-description' }, [
_('Direct mode only. Over SSH the port is discovered from the remote daemon and remembered, so it never has to be typed.')
])
]) ]);
var apiRow = E('div', { 'class': 'cbi-value' }, [ E('label', {}, [_('API key')]), E('div', {}, [ apiI ]) ]);
hostEditor.body = E('div', { 'class': 'cbi-section' }, [
@@ -127,7 +189,7 @@ function hostEditor(section, host, onSaved) {
E('div', { 'class': 'cbi-value' }, [ E('label', {}, [_('Public IP or host')]), E('div', {}, [ hostI ]) ]),
portRow,
userRow,
E('div', { 'class': 'cbi-value' }, [ E('label', {}, [_('Controller port')]), E('div', {}, [ cportI, E('div', { 'class': 'cbi-value-description' }, [_('The port the controller API listens on, e.g. 27893.')]) ]) ]),
cportRow,
keyRow,
apiRow,
msg,
@@ -137,6 +199,7 @@ function hostEditor(section, host, onSaved) {
var m = modeSel.value;
portRow.style.display = (m === 'direct') ? 'none' : '';
userRow.style.display = (m === 'direct') ? 'none' : '';
cportRow.style.display = (m === 'direct') ? '' : 'none';
keyRow.style.display = (m === 'direct') ? 'none' : '';
apiRow.style.display = (m === 'direct') ? '' : 'none';
hostI.placeholder = (m === 'direct') ? '192.168.192.247' : '203.0.113.10';
@@ -1355,19 +1418,26 @@ return view.extend({
L.resolveDefault(rpcHostDel(h.section), {}).then(function() { reload(); });
});
listBox.appendChild(E('div', { 'class': 'cbi-section' }, [
/* The name borrows the page-title scale: argon styles h2 at
* 1.25rem, which is exactly the "ZeroTier - Remote Controller"
* heading. Only the h2's card chrome (padding/white
* background/shadow) is neutralised -- typography stays the
* theme's, so a theme resize moves both together. */
listBox.appendChild(E('div', { 'class': 'cbi-section', 'style': 'margin-bottom:1rem;' }, [
E('div', { 'style': 'display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px;' }, [
E('div', {}, [
E('a', { 'href': '#', 'style': 'color:inherit; cursor:pointer; text-decoration:none;', 'title': _('Manage'), 'click': function(ev) { ev.preventDefault(); toggleDetail(); } }, [
E('strong', {}, [ h.name ])
E('h2', { 'style': 'margin:0; padding:0; background:none; box-shadow:none;' }, [
E('a', { 'href': '#', 'style': 'color:inherit; cursor:pointer; text-decoration:none;', 'title': _('Manage'), 'click': function(ev) { ev.preventDefault(); toggleDetail(); } }, [ h.name ])
]),
E('div', { 'style': 'font-family:monospace; font-size:12px; color:#666;' }, [
(h.mode === 'direct' || h.direct)
? E('span', { 'style': 'color:green; font-weight:bold; margin-right:6px;' }, [ '[' + _('Direct') + ']' ])
: E('span', { 'style': 'color:#888; margin-right:6px;' }, [ '[SSH]' ]),
(h.mode === 'direct')
? (h.host + ':' + h.controller_port + _(' (ZeroTier direct)'))
: (h.user + '@' + h.host + ':' + h.port + _(' (controller port ') + h.controller_port + ')')
(h.mode === 'direct')
? (h.host + ':' + h.controller_port + _(' (ZeroTier direct)'))
: (h.user + '@' + h.host + ':' + h.port + (h.controller_port
? _(' (controller port ') + h.controller_port + ')'
: _(' (controller port auto-discovered)')))
]),
h.key_present || h.mode === 'direct'
? (h.mode === 'direct' ? E('div', { 'style': 'font-size:12px; color:green;' }, [_('API token stored')]) : E('div', { 'style': 'font-size:12px; color:green;' }, [_('SSH key installed')]))
@@ -2,4 +2,4 @@
* Keys are sfh(trimws(msgid)) in hex, exactly what cbi.js _() looks up.
* This firmware ships no window.TR bridge of its own. */
window.TR = window.TR || {};
Object.assign(window.TR, {"7ba57fa5":"(控制器端口 ","005fb0ba":" —— 成员将不会从此地址池获得地址。","928f10ca":"节点地址必须为 10 位十六进制数字","4f25ffed":"地址池需要一个覆盖相同子网的路由,否则成员将获取不到地址。","623124cd":"ZeroTier 控制器或 Moon 根节点必须运行在具有固定公网 IP 的服务器上,可经 SSH 访问,且该用户具有免密 sudo 权限。控制器 API 不支持 TLS 且拒绝所有非本机回环的调用方,因此本页面从不直接访问它:而是从本路由器向远程回环接口建立 SSH 隧道,经隧道发送 API 调用。控制器令牌按需经 SSH 读取,从不存储在本机。","fe5585ed":"接入方式","038a5486":"操作","e733b525":"主动桥接","300fe5c8":"添加","3698d6ed":"添加远程主机","5598797e":"加入 Moon","d252deef":"添加地址池","e5b147b7":"添加远程主机","ea8fae46":"添加路由","71677946":"地址","a4a2e2e7":"高级设置","6ee64833":"允许默认路由","d24721ab":"允许 DNS","51aaeef3":"允许全局","e575f233":"允许管理","d6d5a047":"允许 Zerotier 的拨入客户端访问路由器 LAN 资源(需要在 Zerotier 管理页面设定到 LAN 网段的路由表)","4e247ba0":"API 密钥","1e4876a9":"API 令牌已存储","0b830d67":"正在分配地址…","2e4746c8":"授权","18bbde5a":"授权失败","ececebcc":"已授权","e3f664db":"自动允许客户端 NAT","2014c910":"自动 NAT: 已禁用","228abc80":"自动 NAT: 已启用","f2ffec55":"正在备份...","bdabf869":"备份失败","5eca6072":"立即备份","379bb677":"备份请求失败","f9fb6815":"基本设置","6b3b39f7":"广播","fcf89e62":"构建中…","ae5deb3a":"取消","08f23f6b":"无法连接到 ZeroTier 服务","4bd163d1":"无法读取 moon(ZeroTier 服务是否在运行?)","26d000c2":"关闭","c06f3259":"正在收集数据...","8462484d":"正在获取地址...","cd2f6c29":"运行配置目录","8f48fe99":"配置","6e72b101":"已配置的主机","f1d119dc":"控制器端口","5d359cf2":"控制器可经隧道访问","41e439d8":"控制器状态存在","00a35b8a":"运行配置目录保护","bd7030be":"使用拷贝而不是链接,来保护原目录内容(一般用于预先配置的控制器模式)","88720483":"无法构建方案","a1a69329":"无法获取成员列表","c53244dc":"无法连接控制器","2c150360":"无法读取此网络的任何成员","c1dfeb1a":"以下网络无法读取,因此未显示:","f2248b2d":"无法刷新成员列表;表格保持不变","cbb3b741":"无法保存主机","56370eb4":"创建并签署 Moon","e3a2eb47":"创建失败","eb9bb863":"创建网络","6c94b9b0":"点击跳转到 Zerotier 官网管理平台,新建或者管理网络,并允许客户端接入访问你私人网路(新接入的节点默认不允许访问)","9a74d36d":"删除","b3b872fe":"删除失败","bbfce216":"网络详情","51d36b3d":"删除网络 ","f89b7324":"退出失败","88643bc2":"退出请求失败","be021833":"设备","a0c974c7":"诊断","6469d71b":"诊断失败","7a0e4df2":"直连","00c6b479":"将此文件分发给成员,然后在每个节点上使用「接口信息 → Moon → 添加 Moon」。签署密钥从未离开远程主机。","799c81a7":"下载 .moon","000e380f":"编辑","2f2c0439":"编辑远程主机","814db5a8":"留空 = 直连","78c2105b":"启用","17074f70":"端点","037e2ea1":"错误","be7c6b14":"加载 moon 列表出错","ef0771c3":"加载网络信息失败","eef2dd18":"加载节点信息失败","a285946d":"读取 moon 列表出错","b4b44266":"服务重载失败,请手动执行: /etc/init.d/luci-zerotier reload","d0e77240":"按名称、地址或 IP 筛选","9865b44d":"起始 IP","48150b7e":"网关","a62c536c":"常规设置","764447db":"全局设置","8686381d":"隐藏","24553b14":"远程管理的工作原理","42933f28":"接口信息","d08681fe":"IP 地址","da686b34":"IP 地址","ad69d4ef":"IP 分配","fbb92dcf":"IP 地址池","23603ee1":"IP 分配","cccbf9e4":"IP 必须为 IPv4 地址","7e11b65e":"标签","f2929f6f":"结束 IP","c6ce32d3":"延迟","22038f1d":"退出","4e72d9f1":"留空则保留已存储的密钥","8da4a1f4":"留空则保留已存储的令牌","2b1fb9a6":"正在退出 moon...","dee10879":"已退出 Moon","9c0e750d":"链路","10e6bd0b":"加载中...","c2b71057":"本地配置文件","b259d7d8":"管理","949429e8":"托管 IP 必须为 IPv4 地址,以逗号分隔","a31cd521":"托管路由","58695b7a":"成员详情","cdd88b7c":"成员 ID","cdc6d88f":"成员名称","16916c1d":"成员","9c03b236":"个成员","a0ddf226":"Moon","b7ca31c5":"Moon 已创建:","c2e07f85":"Moon 创建失败","bbf90d15":"Moon ID","1af43404":"Moon ID 必须是 10 位十六进制字符(例如 123456abcd)","119eb928":"Moon 种子节点","793bcbfa":"Moon 种子必须是该 moon 某个根节点的 10 位地址","2e8cf7ed":"本节点并未加入该 moon","9a9a1e82":"主机上的 moon.json","7ab90fdf":"Moon","ff21d2cf":"MTU","a59f3806":"多播限制","8Line truncated
Object.assign(window.TR, {"7ba57fa5":"(控制器端口 ","005fb0ba":" —— 成员将不会从此地址池获得地址。","928f10ca":"节点地址必须为 10 位十六进制数字","4f25ffed":"地址池需要一个覆盖相同子网的路由,否则成员将获取不到地址。","623124cd":"ZeroTier 控制器或 Moon 根节点必须运行在具有固定公网 IP 的服务器上,可经 SSH 访问,且该用户具有免密 sudo 权限。控制器 API 不支持 TLS 且拒绝所有非本机回环的调用方,因此本页面从不直接访问它:而是从本路由器向远程回环接口建立 SSH 隧道,经隧道发送 API 调用。控制器令牌按需经 SSH 读取,从不存储在本机。","fe5585ed":"接入方式","038a5486":"操作","e733b525":"主动桥接","300fe5c8":"添加","3698d6ed":"添加远程主机","5598797e":"加入 Moon","d252deef":"添加地址池","e5b147b7":"添加远程主机","ea8fae46":"添加路由","71677946":"地址","a4a2e2e7":"高级设置","6ee64833":"允许默认路由","d24721ab":"允许 DNS","51aaeef3":"允许全局","e575f233":"允许管理","d6d5a047":"允许 Zerotier 的拨入客户端访问路由器 LAN 资源(需要在 Zerotier 管理页面设定到 LAN 网段的路由表)","4e247ba0":"API 密钥","1e4876a9":"API 令牌已存储","0b830d67":"正在分配地址…","2e4746c8":"授权","18bbde5a":"授权失败","ececebcc":"已授权","e3f664db":"自动允许客户端 NAT","2014c910":"自动 NAT: 已禁用","228abc80":"自动 NAT: 已启用","f2ffec55":"正在备份...","bdabf869":"备份失败","5eca6072":"立即备份","379bb677":"备份请求失败","f9fb6815":"基本设置","6b3b39f7":"广播","fcf89e62":"构建中…","ae5deb3a":"取消","08f23f6b":"无法连接到 ZeroTier 服务","4bd163d1":"无法读取 moon(ZeroTier 服务是否在运行?)","26d000c2":"关闭","c06f3259":"正在收集数据...","8462484d":"正在获取地址...","cd2f6c29":"运行配置目录","8f48fe99":"配置","6e72b101":"已配置的主机","f1d119dc":"控制器端口","5d359cf2":"控制器可经隧道访问","41e439d8":"控制器状态存在","00a35b8a":"运行配置目录保护","bd7030be":"使用拷贝而不是链接,来保护原目录内容(一般用于预先配置的控制器模式)","88720483":"无法构建方案","a1a69329":"无法获取成员列表","c53244dc":"无法连接控制器","2c150360":"无法读取此网络的任何成员","c1dfeb1a":"以下网络无法读取,因此未显示:","f2248b2d":"无法刷新成员列表;表格保持不变","cbb3b741":"无法保存主机","56370eb4":"创建并签署 Moon","e3a2eb47":"创建失败","eb9bb863":"创建网络","6c94b9b0":"点击跳转到 Zerotier 官网管理平台,新建或者管理网络,并允许客户端接入访问你私人网路(新接入的节点默认不允许访问)","9a74d36d":"删除","b3b872fe":"删除失败","bbfce216":"网络详情","51d36b3d":"删除网络 ","f89b7324":"退出失败","88643bc2":"退出请求失败","be021833":"设备","a0c974c7":"诊断","6469d71b":"诊断失败","7a0e4df2":"直连","00c6b479":"将此文件分发给成员,然后在每个节点上使用「接口信息 → Moon → 添加 Moon」。签署密钥从未离开远程主机。","799c81a7":"下载 .moon","000e380f":"编辑","2f2c0439":"编辑远程主机","814db5a8":"留空 = 直连","78c2105b":"启用","17074f70":"端点","037e2ea1":"错误","be7c6b14":"加载 moon 列表出错","ef0771c3":"加载网络信息失败","eef2dd18":"加载节点信息失败","a285946d":"读取 moon 列表出错","b4b44266":"服务重载失败,请手动执行: /etc/init.d/luci-zerotier reload","d0e77240":"按名称、地址或 IP 筛选","9865b44d":"起始 IP","48150b7e":"网关","a62c536c":"常规设置","764447db":"全局设置","8686381d":"隐藏","24553b14":"远程管理的工作原理","42933f28":"接口信息","d08681fe":"IP 地址","da686b34":"IP 地址","ad69d4ef":"IP 分配","fbb92dcf":"IP 地址池","23603ee1":"IP 分配","cccbf9e4":"IP 必须为 IPv4 地址","7e11b65e":"标签","f2929f6f":"结束 IP","c6ce32d3":"延迟","22038f1d":"退出","8da4a1f4":"留空则保留已存储的令牌","2b1fb9a6":"正在退出 moon...","dee10879":"已退出 Moon","9c0e750d":"链路","10e6bd0b":"加载中...","c2b71057":"本地配置文件","b259d7d8":"管理","949429e8":"托管 IP 必须为 IPv4 地址,以逗号分隔","a31cd521":"托管路由","58695b7a":"成员详情","cdd88b7c":"成员 ID","cdc6d88f":"成员名称","16916c1d":"成员","9c03b236":"个成员","a0ddf226":"Moon","b7ca31c5":"Moon 已创建:","c2e07f85":"Moon 创建失败","bbf90d15":"Moon ID","1af43404":"Moon ID 必须是 10 位十六进制字符(例如 123456abcd)","119eb928":"Moon 种子节点","793bcbfa":"Moon 种子必须是该 moon 某个根节点的 10 位地址","2e8cf7ed":"本节点并未加入该 moon","9a9a1e82":"主机上的 moon.json","7ab90fdf":"Moon","ff21d2cf":"MTU","a59f3806":"多播限制","8d8bb35f":"我的控制器","d785d68e":"名称",Line truncated
Binary file not shown.
+30 -12
View File
@@ -319,9 +319,6 @@ msgstr "延迟"
msgid "Leave"
msgstr "退出"
msgid "Leave blank to keep the stored key"
msgstr "留空则保留已存储的密钥"
msgid "Leave blank to keep the stored token"
msgstr "留空则保留已存储的令牌"
@@ -583,9 +580,6 @@ msgstr "重命名"
msgid "Rename failed"
msgstr "重命名失败"
msgid "Required: paste the PRIVATE key of a keypair whose public half is in the remote ~/.ssh/authorized_keys"
msgstr "必填:粘贴密钥对的私钥,其公钥部分须已在远程主机的 ~/.ssh/authorized_keys 中"
msgid "Required: the controller API token"
msgstr "必填:控制器 API 令牌"
@@ -664,9 +658,6 @@ msgstr "SSH 密钥已安装"
msgid "SSH port"
msgstr "SSH 端口"
msgid "SSH private key"
msgstr "SSH 私钥"
msgid "SSH tunnel"
msgstr "SSH 隧道"
@@ -691,9 +682,6 @@ msgstr "控制器从这些地址段中分配地址,并按成员逐一报告。
msgid "The following will run on the remote host:"
msgstr "以下命令将在远程主机上执行:"
msgid "The port the controller API listens on, e.g. 27893."
msgstr "控制器 API 监听的端口,例如 27893。"
msgid "The port the controller API listens on, e.g. 27893. Only reachable through the SSH tunnel."
msgstr "控制器 API 监听的端口,例如 27893。仅可经 SSH 隧道访问。"
@@ -748,3 +736,33 @@ msgstr "Zerotier 是一个开源,跨平台,而且适合内网穿透互联的
msgid "ZeroTier version"
msgstr "ZeroTier 版本"
msgid "SSH public key"
msgstr "SSH 公钥"
msgid "The public half of this router's dedicated key pair. Add it to the remote host's ~/.ssh/authorized_keys to authorize this router."
msgstr "本路由器专用密钥对的公钥半边。把它添加到远程主机的 ~/.ssh/authorized_keys 即可授权本路由器。"
msgid "No private key needed: when you save, this router generates a dedicated key pair and shows you the public key and the exact command to run on the remote host."
msgstr "无需私钥:保存时本路由器会生成专用密钥对,并显示公钥与需要在远程主机上执行的确切命令。"
msgid "Direct mode only. Over SSH the port is discovered from the remote daemon and remembered, so it never has to be typed."
msgstr "仅直连模式需要。SSH 模式下端口会从远程守护进程自动发现并记住,无需填写。"
msgid "Copy command"
msgstr "复制命令"
msgid "Copied. Run it on the remote host."
msgstr "已复制。请在远程主机上执行。"
msgid "Done"
msgstr "完成"
msgid "Authorize this router on the remote host"
msgstr "在远程主机上授权本路由器"
msgid "A dedicated key pair was generated on this router for host "%s". The private half never leaves this router. You already have access to the remote host, so run this command there to authorize the connection:"
msgstr "本路由器已为主机“%s”生成专用密钥对。私钥半边永远不会离开本路由器。你本来就拥有远程主机的访问权,在那里执行以下命令即可授权连接:"
msgid " (controller port auto-discovered)"
msgstr "(控制器端口将自动发现)"
@@ -139,7 +139,9 @@ r_load() {
r_name=$(rget "$r_sect" name "$r_sect")
r_host=$(rget "$r_sect" host)
r_cport=$(rget "$r_sect" controller_port 9993)
# Empty means "not configured": SSH mode then discovers the real port from
# the remote daemon (see the block after r_dest) and commits it here.
r_cport=$(rget "$r_sect" controller_port "")
r_mode=$(rget "$r_sect" mode ssh)
r_tokfile=$(rget "$r_sect" token_path "/etc/zerotier/remote/$r_sect.token")
r_mark="/etc/zerotier/remote/$r_sect.direct"
@@ -150,9 +152,11 @@ r_load() {
*) r_die "unknown access mode '$r_mode'" ;;
esac
r_valid_host "$r_host" || r_die "invalid or empty host"
r_valid_port "$r_cport" || r_die "invalid controller_port"
if [ "$r_mode" = "direct" ]; then
# ZT direct has no SSH to discover anything with, so the port is the
# one thing the operator must still supply.
r_valid_port "$r_cport" || r_die "invalid controller_port"
# ZT direct: no ssh at all -- the controller is reached over the
# ZeroTier network with its API token, kept with the same 0600
# discipline as the ssh key.
@@ -183,11 +187,25 @@ r_load() {
r_lport=$(( 20000 + ((0x$_u + $$) % 19000) ))
r_valid_port "$r_lport" || r_die "could not derive a local forwarding port"
# Without an explicit key, ssh would fall back to an agent and then to
# password prompts, which would hang the rpcd call instead of failing it.
[ -f "$r_key" ] || r_die "ssh key not found at $r_key"
r_dest="$r_user@$r_host"
# Controller port discovery, SSH mode with nothing stored: the daemon
# writes its real bound port to zerotier-one.port and the controller API
# shares that port. Costs ONE extra SSH round trip the first time only --
# the finding is committed to UCI, and every later call reads it like a
# typed value. A controller_port already in UCI always wins and is never
# probed; a failed discovery falls back to the daemon default for this
# call and stays unset, so a later call can retry.
if [ "$r_mode" = "ssh" ] && ! r_valid_port "$r_cport"; then
_dp=$(rssh "sudo -n cat /var/lib/zerotier-one/zerotier-one.port" 2>/dev/null | tr -dc '0-9')
if r_valid_port "$_dp"; then
r_cport="$_dp"
uci set "$ZT_UCI_CONFIG.$r_sect.controller_port=$_dp"
uci commit "$ZT_UCI_CONFIG" 2>/dev/null
else
r_cport=9993
fi
fi
}
# --- ssh -------------------------------------------------------------------
@@ -715,11 +715,25 @@ case "$1" in
json_add_string "host" "$_h"
json_add_string "port" "$(uci -q get "zerotier.$_s.port" || echo 22)"
json_add_string "user" "$(uci -q get "zerotier.$_s.user" || echo root)"
json_add_string "controller_port" "$(uci -q get "zerotier.$_s.controller_port" || echo 9993)"
# No fake default: an unset controller_port reports empty and the
# view says it will be auto-discovered (it is, on first use).
json_add_string "controller_port" "$(uci -q get "zerotier.$_s.controller_port")"
json_add_string "mode" "$(uci -q get "zerotier.$_s.mode" || echo ssh)"
json_add_string "key_path" "$_kp"
if [ -f "$_kp" ]; then
json_add_boolean "key_present" 1
# Public half only: a public key is not secret, and the
# editor shows it so the operator can (re)authorize the
# router on the remote host.
_pb=""
if [ -f "$_kp.pub" ]; then
_pb=$(cat "$_kp.pub" 2>/dev/null)
else
# grep is load-bearing: dropbear's ssh-keygen prints a
# "Public key portion is:" header and a Fingerprint line too.
_pb=$(ssh-keygen -y -f "$_kp" 2>/dev/null | grep -m1 "^ssh-")
fi
[ -n "$_pb" ] && json_add_string "pubkey" "$_pb"
else
json_add_boolean "key_present" 0
fi
@@ -793,13 +807,21 @@ case "$1" in
*) _err="invalid ssh user" ;;
esac
[ -z "$_err" ] || { json_add_int "code" 1; json_add_string "error" "$_err"; json_dump; return; }
for _p in "$_rport" "$_cport"; do
case "$_p" in
''|*[!0-9]*) _err="ports must be numeric" ;;
*) [ "$_p" -ge 1 ] 2>/dev/null && [ "$_p" -le 65535 ] 2>/dev/null || _err="port out of range" ;;
case "$_rport" in
''|*[!0-9]*) _err="ports must be numeric" ;;
*) [ "$_rport" -ge 1 ] 2>/dev/null && [ "$_rport" -le 65535 ] 2>/dev/null || _err="port out of range" ;;
esac
[ -z "$_err" ] || { json_add_int "code" 1; json_add_string "error" "$_err"; json_dump; return; }
# SSH port is required; the controller port is OPTIONAL here --
# empty means the helper discovers it from the remote daemon
# and remembers it. Validate only when supplied.
if [ -n "$_cport" ]; then
case "$_cport" in
*[!0-9]*) _err="controller port must be numeric" ;;
*) [ "$_cport" -ge 1 ] 2>/dev/null && [ "$_cport" -le 65535 ] 2>/dev/null || _err="port out of range" ;;
esac
[ -z "$_err" ] || { json_add_int "code" 1; json_add_string "error" "$_err"; json_dump; return; }
done
fi
else
case "$_cport" in
''|*[!0-9]*) _err="controller port must be numeric" ;;
@@ -819,44 +841,80 @@ case "$1" in
fi
fi
_rport=${_rport:-22}
_ruser=${_ruser:-root}
_cport=${_cport:-9993}
_hname=${_hname:-$_sect}
_kp="/etc/zerotier/remote/$_sect.key"
_rport=${_rport:-22}
_ruser=${_ruser:-root}
_hname=${_hname:-$_sect}
_kp="/etc/zerotier/remote/$_sect.key"
mkdir -p /etc/zerotier/remote || { json_add_int "code" 1; json_add_string "error" "cannot create /etc/zerotier/remote"; json_dump; return; }
chmod 700 /etc/zerotier/remote 2>/dev/null
# An existing key is preserved when the field is left blank, so
# editing a host's name does not silently destroy its key.
if [ -n "$_pkey" ]; then
case "$_pkey" in
*"PRIVATE KEY"*) ;;
*) json_add_int "code" 1; json_add_string "error" "that does not look like a private key"; json_dump; return ;;
esac
(umask 077; printf '%s\n' "$_pkey" > "$_kp") || { json_add_int "code" 1; json_add_string "error" "could not write the key file"; json_dump; return; }
chmod 600 "$_kp" 2>/dev/null
# An existing key is preserved when the field is left blank, so
# editing a host's name does not silently destroy its key.
if [ -n "$_pkey" ]; then
case "$_pkey" in
*"PRIVATE KEY"*) ;;
*) json_add_int "code" 1; json_add_string "error" "that does not look like a private key"; json_dump; return ;;
esac
(umask 077; printf '%s\n' "$_pkey" > "$_kp") || { json_add_int "code" 1; json_add_string "error" "could not write the key file"; json_dump; return; }
chmod 600 "$_kp" 2>/dev/null
fi
# No key pasted and none stored: generate a dedicated keypair on
# the router. The private half is written straight to its 0600
# file and never crosses the browser; the public half travels
# back in the response so the UI can show the operator the exact
# command to authorize it on the remote host (which they can run
# there because they already have their own access).
_keygen=0
if [ "$_mode" = "ssh" ] && [ -z "$_pkey" ] && [ ! -f "$_kp" ]; then
if (umask 077 && ssh-keygen -t ed25519 -N '' -C "luci-zerotier-$_sect" -f "$_kp" -q >/dev/null 2>&1); then
_keygen=1
else
rm -f "$_kp" "$_kp.pub" 2>/dev/null
json_add_int "code" 1
json_add_string "error" "could not generate a key pair (ssh-keygen failed)"
json_dump
return
fi
fi
uci -q delete "zerotier.$_sect" 2>/dev/null
uci set "zerotier.$_sect=remote"
uci set "zerotier.$_sect.name=$_hname"
uci set "zerotier.$_sect.host=$_rhost"
uci set "zerotier.$_sect.port=$_rport"
uci set "zerotier.$_sect.user=$_ruser"
uci set "zerotier.$_sect.controller_port=$_cport"
uci set "zerotier.$_sect.key_path=$_kp"
uci set "zerotier.$_sect.port=$_rport"
uci set "zerotier.$_sect.user=$_ruser"
# Direct mode needs the port on file (default 9993); SSH mode with
# an empty value deletes any stale setting so the helper's
# discover-and-remember path runs again.
if [ "$_mode" = "direct" ] || [ -n "$_cport" ]; then
uci set "zerotier.$_sect.controller_port=$( [ "$_mode" = "direct" ] && echo "${_cport:-9993}" || echo "$_cport" )"
else
uci -q delete "zerotier.$_sect.controller_port"
fi
uci set "zerotier.$_sect.key_path=$_kp"
uci set "zerotier.$_sect.mode=$_mode"
# A config change invalidates any cached transport decision: the old
# direct marker or probe-failure note belongs to the old settings.
rm -f "/etc/zerotier/remote/$_sect.direct" "/etc/zerotier/remote/$_sect.nodirect" 2>/dev/null
uci commit zerotier || { json_add_int "code" 1; json_add_string "error" "uci commit failed"; json_dump; return; }
json_add_int "code" 0
json_add_string "key_path" "$_kp"
json_dump
;;
json_add_int "code" 0
json_add_string "key_path" "$_kp"
# The public half rides along whenever it is known, so the editor
# can display it for an existing key and the save flow can show
# the authorize-on-remote command for a freshly generated one.
_pub=""
if [ -f "$_kp.pub" ]; then
_pub=$(cat "$_kp.pub" 2>/dev/null)
elif [ -f "$_kp" ]; then
_pub=$(ssh-keygen -y -f "$_kp" 2>/dev/null | grep -m1 "^ssh-")
fi
[ -n "$_pub" ] && json_add_string "pubkey" "$_pub"
[ "$_keygen" = "1" ] && json_add_boolean "key_generated" 1
json_dump
;;
remote_host_del)
read -r input
json_load "$input" 2>/dev/null
@@ -871,7 +929,11 @@ case "$1" in
[ -n "$_kp" ] || _kp="/etc/zerotier/remote/$_sect.key"
# Only unlink inside our own managed directory.
case "$_kp" in
/etc/zerotier/remote/*.key) [ -f "$_kp" ] && rm -f "$_kp" ;;
/etc/zerotier/remote/*.key)
[ -f "$_kp" ] && rm -f "$_kp"
# *.key does not match <key>.pub; drop it too or it leaks.
rm -f "$_kp.pub" 2>/dev/null
;;
esac
rm -f "/etc/zerotier/remote/$_sect.token" "/etc/zerotier/remote/$_sect.direct" "/etc/zerotier/remote/$_sect.nodirect" 2>/dev/null
uci -q delete "zerotier.$_sect"
@@ -126,19 +126,14 @@ return network.registerProtocol('t2s', {
o.default = '10.0.0.0/8';
o.depends({'ip_manual': ''});
o = s.taboption('general', form.Value, 'ipaddr', _('IPv4 Address'));
o.datatype = 'ip4addr("nomask")';
o = s.taboption('general', form.DynamicList, 'ipaddr', _('IP Address'), _('Format: <code>address/mask</code>, IPv4 or IPv6, e.g. <code>192.168.255.2/30</code> or <code>fd00::2/64</code>'));
o.datatype = 'cidr';
o.placeholder = '192.168.255.2/30';
o.depends({'ip_manual': '1', 'proxy': /http|socks4|socks5|relay|ss/ });
o.rmempty = true;
o = s.taboption('general', form.Value, 'netmask', _('IPv4 Netmask'));
o.value('255.255.255.0', '255.255.255.0');
o.value('255.255.0.0', '255.255.0.0');
o.depends({'ip_manual': '1', 'proxy': /http|socks4|socks5|relay|ss/ });
o.rmempty = true;
o = s.taboption('general', form.Value, 'gateway', _('IPv4 Gateway'));
o.datatype = 'ip4addr("nomask")';
o = s.taboption('general', form.DynamicList, 'gateway', _('Gateway'), _('IPv4 and/or IPv6 gateway'));
o.datatype = 'ipaddr("nomask")';
o.depends({'ip_manual': '1', 'proxy': /http|socks4|socks5|relay|ss/ });
o.rmempty = true;
+30 -2
View File
@@ -1,2 +1,30 @@
Screenshots
.DS_Store
# Local preview environment and development-only tooling
/.preview/
/dev.sh
/tools/preview/
/tools/media/
# Local task notes
/task_plan.md
/findings.md
/progress.md
# Dependencies and interpreter caches
node_modules/
__pycache__/
*.py[cod]
/.cache/
# OpenWrt build and release outputs
/sdk/
/artifacts/
/release-assets/
/argon-config-i18n-packages.txt
*.ipk
*.apk
# OS and editor temporary files
.DS_Store
*~
*.swp
*.swo
+20 -3
View File
@@ -7,15 +7,32 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Argon Theme
LUCI_DEPENDS:=+USE_APK:wget-any +!USE_APK:wget +jsonfilter
PKG_VERSION:=2.4.8
PKG_RELEASE:=29
LUCI_DEPENDS:=+luci-base +USE_APK:wget-any +!USE_APK:wget +jsonfilter +ucode-mod-math
# Keep existing translation package names and the argon-config message domain.
LUCI_BASENAME:=argon-config
PKG_VERSION:=3.0.0
PKG_RELEASE:=30
PKG_LICENSE:=Apache-2.0 MIT
# LuCI derives translations independently of the theme version. Keep release
# builds deterministic even when the package directory has no Git history.
ifneq ($(CONFIG_USE_APK),)
PKG_PO_VERSION:=$(PKG_VERSION)-r$(PKG_RELEASE)
else
# The v3.0.0 IPKs shipped as "unknown", which sorts above numeric versions.
# Keep this prefix for future IPK translations so a normal install upgrades it.
PKG_PO_VERSION:=v$(PKG_VERSION)
endif
CONFIG_LUCI_CSSTIDY:=
# Ship template source across stable and snapshot runtimes. Precompiled
# bytecode would require the SDK's ucode format (see issue #715).
LUCI_MINIFY_UT:=0
define Package/luci-theme-argon/conffiles
/etc/config/argon
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
+60 -122
View File
@@ -1,13 +1,3 @@
<!-- markdownlint-configure-file {
"MD013": {
"code_blocks": false,
"tables": false,
"line_length":200
},
"MD033": false,
"MD041": false
} -->
[license]: /LICENSE
[license-badge]: https://img.shields.io/github/license/jerrykuku/luci-theme-argon?style=flat-square&a=1
[prs]: https://github.com/jerrykuku/luci-theme-argon/pulls
@@ -20,18 +10,11 @@
[download-badge]: https://img.shields.io/github/downloads/jerrykuku/luci-theme-argon/total?style=flat-square
[contact]: https://t.me/jerryk6
[contact-badge]: https://img.shields.io/badge/Contact-telegram-blue?style=flat-square
[en-us-link]: /README.md
[zh-cn-link]: /README_ZH.md
[en-us-release-log]: /RELEASE.md
[zh-cn-release-log]: /RELEASE_ZH.md
[config-link]: https://github.com/jerrykuku/luci-app-argon-config/releases
[official]: https://github.com/openwrt/openwrt
[immortalwrt]: https://github.com/immortalwrt/immortalwrt
<div align="center">
<img src="https://raw.githubusercontent.com/jerrykuku/staff/master/argon_title4.svg">
# A brand new OpenWrt LuCI theme
# A brand new OpenWrt LuCI theme · v3
Argon is **a clean and tidy OpenWrt LuCI theme** that allows<br/>
users to customize their login interface with images or videos.
@@ -44,122 +27,79 @@ It also supports automatic and manual switching between light and dark modes.
[![download][download-badge]][download]
[![contact][contact-badge]][contact]
**English** |
[简体中文][zh-cn-link]
**English** · [简体中文](README_ZH.md)
[Key Features](#key-features) •
[Compatibility](#compatibility) •
[Version History](#version-history) •
[Getting started](#getting-started) •
[Screenshots](#screenshots) •
[Contributors](#contributors) •
[Credits](#credits)
[![Argon 3.0 desktop demo: two login layouts, light and dark modes, and built-in settings](Screenshots/argon-3-demo.gif)](Screenshots/argon-3-demo.mp4)
[Watch the desktop demo · MP4](Screenshots/argon-3-demo.mp4)
<img src="https://raw.githubusercontent.com/jerrykuku/staff/master/argon2.gif">
</div>
## Key Features
## What's new in 3.0
- Clean and modern Argon-style interface design.
- Fully adapted for both desktop and mobile displays.
- Supports automatic or manual switching between light and dark modes.
- Supports custom theme colors, along with adjustable blur and transparency effects.
- The login page supports local images, videos, and online wallpapers as backgrounds.
- Works with [luci-app-argon-config][config-link] for a more complete theme configuration experience.
- **Built-in Argon Config.** Manage the theme without a separate plugin.
- **Broad third-party app support.** Refined layouts, forms and tables for most third-party LuCI apps, with desktop/mobile and light/dark adaptations.
- **Two login layouts.** Choose the classic sidebar or centered card.
- **Light and dark.** Switch from the header or follow your system, with customizable accent colors.
- **Your own look.** Adjust login glass effects, set image or video backgrounds, and upload a browser icon and login logo.
- **Desktop and mobile.** Responsive layouts across screen sizes.
Choose **Classic sidebar** (default) or **Centered card** under **System → Argon Config → Login page style**. Both layouts use the same wallpaper, color and authentication settings. Update both the theme and configuration plugin to use the selector; missing or unsupported `login_style` values fall back to `classic`.
## Install
Unsplash wallpapers require an Unsplash API access key. The current `luci-app-argon-config` UI does not expose this setting; after choosing Unsplash, set it through UCI:
**In 3.0, the theme and configuration features are one package: `luci-theme-argon`. Fresh installations need only this package.** Download the format matching your firmware's native package manager from [Releases](https://github.com/jerrykuku/luci-theme-argon/releases). If the old standalone configuration plugin is installed, follow the migration steps below first.
| Package manager | Package | Install |
| --- | --- | --- |
| `opkg` | `.ipk` | `opkg install ./luci-theme-argon_*.ipk` |
| APK v3 (`apk-tools 3.x`) | `.apk` | `apk add --allow-untrusted ./luci-theme-argon-*.apk` |
Run `opkg update` or `apk update` first. APK v2 cannot read these APK packages; keep your firmware's native package manager.
For translations, install the matching `luci-i18n-argon-config-<language>` package from the same release and in the same format.
Select **Argon** under **System → System → Language and Style → Design**.
<details>
<summary>Already have the old luci-app-argon-config? Migrate to 3.0</summary>
This applies only when the old standalone configuration plugin is installed. Otherwise, skip this section.
The settings page, colors, backgrounds and branding are all included in the theme. The release's `luci-app-argon-config` is an **empty migration package using the old package name**. It preserves existing settings and hands its files over to the theme.
1. Download both `luci-app-argon-config` and `luci-theme-argon` from the **same 3.0 release in the theme repository**, in the same format.
2. Put both files in one directory on the router and open a terminal there. **Do not uninstall the old plugin first.** Choose the command pair for your firmware; run the second command only after the first succeeds.
```sh
uci set 'argon.@global[0].use_api_key=YOUR_UNSPLASH_ACCESS_KEY'
uci commit argon
# opkg
opkg install ./luci-app-argon-config_*.ipk
opkg install ./luci-theme-argon_*.ipk
# APK v3
apk add --allow-untrusted ./luci-app-argon-config-*.apk
apk add --allow-untrusted ./luci-theme-argon-*.apk
```
Without a key, the login page uses a local background. Bing and Wallhaven wallpaper options are unaffected.
The first command replaces the old plugin with the empty migration package. The second installs the integrated theme and restores your settings. Continue configuring it under **System → Argon Config**.
## Custom browser icon and login logo
</details>
Under **System → Argon Config → Custom branding**, upload a browser icon and login logo separately, or use one image for both. Each can be restored to its default. PNG images must be 16–1024 pixels per side and at most 1 MiB. Browser icons must be square; login logos retain their aspect ratio in both login layouts.
## Customize
Update both the theme and configuration plugin. Uploads and resets apply immediately without saving other settings; reload open pages to update their icons. The custom browser icon also supplies the touch icon and web manifest. Existing home-screen shortcuts may need to be added again.
Files are stored in `/www/luci-static/argon/branding/` without replacing packaged assets. The configuration plugin includes this directory in `keep.d` backups for upgrades that preserve configuration. Upgrades without configuration preservation reset the icons.
## Compatibility
Only the `master` branch is maintained now.
Support is focused on modern LuCI environments based on [Official OpenWrt][official] and [ImmortalWrt][immortalwrt].
## Version History
The latest version is v2.4.8 [Click here][en-us-release-log] to view the full version history record.
## Getting started
### Build from source
```bash
cd openwrt/package
git clone https://github.com/jerrykuku/luci-theme-argon.git
make menuconfig #choose LUCI->Theme->Luci-theme-argon
make -j1 V=s
```
### Choose the package format
Choose the format used by the firmware's native package manager. The presence of an `apk` command alone does not identify it:
| Firmware package manager | Release assets | Install command |
| --- | --- | --- |
| `opkg` (including OpenWrt 23.05, 24.10, and GL.iNet firmware based on these versions) | `.ipk` | `opkg install` |
| OpenWrt / ImmortalWrt using APK v3 natively (`apk-tools 3.x`) | `.apk` | `apk add --allow-untrusted` |
The APK assets use **APK v3 format**, which `apk-tools 2.x` (including `2.14.0`) cannot read. `--allow-untrusted` only bypasses signature trust checks; it does not add format support. `apk` cannot install IPK files either.
APK builds target the OpenWrt **25.12.5 stable SDK** and keep theme templates as source for runtime compatibility. The original v2.4.8 APK was built with a snapshot SDK and requires `ucode>=2026.02.27`, which OpenWrt 25.12.5 does not provide ([#715](https://github.com/jerrykuku/luci-theme-argon/issues/715)). That asset needs to be rebuilt with this fix; updating this repository does not replace already published packages. Do not bypass dependency checks to install incompatible bytecode.
For the GL.iNet / OpenWrt 23.05 environment in [#705](https://github.com/jerrykuku/luci-theme-argon/issues/705), download IPK files and install them with the firmware's existing `opkg`. There is no need to replace the system package manager.
Download `luci-theme-argon`, `luci-app-argon-config`, and any required `luci-i18n-argon-config-<language>` packages from [Releases][release]. Select the same format for all packages and use the actual asset filenames; do not construct version suffixes or rename file extensions.
Put the downloaded files in one empty directory, keeping only one version of each package, then run the appropriate commands from that directory.
### Install release packages (`ipk`)
```bash
opkg update
opkg install ./luci-theme-argon_*.ipk ./luci-app-argon-config_*.ipk
```
If you also downloaded language packages in the same format, install them with:
```bash
opkg install ./luci-i18n-argon-config-*.ipk
```
### Install release packages (`apk`)
Use this only on firmware that natively uses `apk-tools 3.x`. Check the version with `apk --version`.
```bash
apk update
apk add --allow-untrusted ./luci-theme-argon-*.apk ./luci-app-argon-config-*.apk
```
If you also downloaded language packages in the same format, install them with:
```bash
apk add --allow-untrusted ./luci-i18n-argon-config-*.apk
```
If you encounter `IO ERROR`, first use `ls -l ./*.apk` to check that the files exist, their names are correct, and the downloads are complete, then check `apk --version`. Do not force installation to bypass format or dependency errors.
Open **System → Argon Config** for login layouts, colors, glass effects, wallpapers and branding. The header's appearance button switches between **Light mode**, **Dark mode** and **Follow system**; the choice also applies to the login page.
Local image and video backgrounds are supported, along with Bing, Unsplash and Wallhaven wallpapers. Unsplash requires an API access key. Browser icons and login logos accept PNG files, 16–1024 pixels per side, up to 1 MiB; browser icons must be square.
## Screenshots
![desktop](/Screenshots/screenshot_pc.jpg)
![mobile](/Screenshots/screenshot_phone.jpg)
Light and dark views of the overview and both login layouts, on desktop and mobile.
![Light mode: desktop overview, classic and centered login](Screenshots/preview-light.png)
![Mobile light mode: classic login, centered login and overview](Screenshots/mobile-light.png)
![Dark mode: desktop overview, classic and centered login](Screenshots/preview-dark.png)
![Mobile dark mode: classic login, centered login and overview](Screenshots/mobile-dark.png)
## Contributors
@@ -169,12 +109,10 @@ If you encounter `IO ERROR`, first use `ls -l ./*.apk` to check that the files e
Made with [contrib.rocks](https://contrib.rocks).
## Related Projects
## Development
- [luci-app-argon-config](https://github.com/jerrykuku/luci-app-argon-config): Configuration plugin for the Argon theme
- [openwrt-package](https://github.com/jerrykuku/openwrt-package): My OpenWrt package collection
- [CasaOS](https://github.com/IceWhaleTech/CasaOS): A simple, elegant open-source personal cloud system and my current primary project
See the [development guide](docs/DEVELOPMENT.md) for source builds, LESS, templates, icons and checks.
## Credits
[Releases](https://github.com/jerrykuku/luci-theme-argon/releases) · [Version history](RELEASE.md) · [Contributors](https://github.com/jerrykuku/luci-theme-argon/graphs/contributors) · [Report an issue](https://github.com/jerrykuku/luci-theme-argon/issues)
[luci-theme-material](https://github.com/LuttyYang/luci-theme-material/)
Licensed under [Apache-2.0](LICENSE). Thanks to [luci-theme-material](https://github.com/LuttyYang/luci-theme-material/).
+57 -119
View File
@@ -1,13 +1,3 @@
<!-- markdownlint-configure-file {
"MD013": {
"code_blocks": false,
"tables": false,
"line_length":200
},
"MD033": false,
"MD041": false
} -->
[license]: /LICENSE
[license-badge]: https://img.shields.io/github/license/jerrykuku/luci-theme-argon?style=flat-square&a=1
[prs]: https://github.com/jerrykuku/luci-theme-argon/pulls
@@ -20,18 +10,11 @@
[download-badge]: https://img.shields.io/github/downloads/jerrykuku/luci-theme-argon/total?style=flat-square
[contact]: https://t.me/jerryk6
[contact-badge]: https://img.shields.io/badge/Contact-telegram-blue?style=flat-square
[en-us-link]: /README.md
[zh-cn-link]: /README_ZH.md
[en-us-release-log]: /RELEASE.md
[zh-cn-release-log]: /RELEASE_ZH.md
[config-link]: https://github.com/jerrykuku/luci-app-argon-config/releases
[official]: https://github.com/openwrt/openwrt
[immortalwrt]: https://github.com/immortalwrt/immortalwrt
<div align="center">
<img src="https://raw.githubusercontent.com/jerrykuku/staff/master/argon_title4.svg">
# 一个全新的 OpenWrt 主题
# 一个全新的 OpenWrt 主题 · v3
Argon 是**一款干净整洁的 OpenWrt LuCI 主题**,
允许用户使用图片或视频自定义其登录界面。
@@ -44,122 +27,79 @@ Argon 是**一款干净整洁的 OpenWrt LuCI 主题**,
[![download][download-badge]][download]
[![contact][contact-badge]][contact]
[English][en-us-link] |
**简体中文**
[English](README.md) · **简体中文**
[特色](#特色) •
[兼容性](#兼容性) •
[版本历史](#版本历史) •
[快速开始](#快速开始) •
[屏幕截图](#屏幕截图) •
[贡献者](#贡献者) •
[鸣谢](#鸣谢)
[![Argon 3.0 桌面演示:两套登录界面、亮暗配色与内置设置](Screenshots/argon-3-demo.gif)](Screenshots/argon-3-demo.mp4)
[观看桌面演示 · MP4](Screenshots/argon-3-demo.mp4)
<img src="https://raw.githubusercontent.com/jerrykuku/staff/master/argon2.gif">
</div>
## 特色
## 3.0 新特性
- 简洁清爽的 Argon 风格界面设计。
- 完整适配桌面端与移动端显示。
- 支持浅色 / 深色模式自动或手动切换。
- 支持自定义主题主色,以及毛玻璃的模糊与透明度。
- 登录页支持本地图片、视频和在线壁纸背景。
- 可搭配 [luci-app-argon-config][config-link] 实现更完整的主题设置体验。
- **内置 Argon Config**:主题设置随主题安装,无需独立插件。
- **广泛适配第三方应用**:针对大多数第三方 LuCI 应用优化布局、表单和表格,兼顾桌面、手机与亮暗模式。
- **两套登录界面**:经典侧栏与居中卡片,自由选择。
- **亮暗配色**:顶部快捷切换或跟随系统,支持自定义主色。
- **个性化外观**:调节毛玻璃效果,设置图片或视频背景,上传网站图标和登录 Logo。
- **桌面与手机适配**:适应不同屏幕尺寸的响应式布局。
在 **系统 → Argon 主题设置 → 登录页样式** 中,可选择默认的**经典侧栏**或**居中卡片**。两种布局共用壁纸、配色和认证设置。请同时更新主题和配置插件以使用此选项;未配置或不支持的 `login_style` 值会回退到 `classic`。
## 安装
Unsplash 在线壁纸需要 Unsplash API Access Key。当前 `luci-app-argon-config` 界面尚未提供此设置;选择 Unsplash 后,可通过 UCI 配置:
**3.0 已将主题和配置功能合并到 `luci-theme-argon`,全新安装只需这一个包。** 前往 [Releases](https://github.com/jerrykuku/luci-theme-argon/releases) 下载与固件原生包管理器匹配的格式。已有旧版独立配置插件的用户,请先按下方迁移步骤操作。
| 包管理器 | 包格式 | 安装命令 |
| --- | --- | --- |
| `opkg` | `.ipk` | `opkg install ./luci-theme-argon_*.ipk` |
| APK v3(`apk-tools 3.x`) | `.apk` | `apk add --allow-untrusted ./luci-theme-argon-*.apk` |
安装前执行 `opkg update` 或 `apk update`。APK v2 无法读取本项目的 APK 包,请沿用固件原生包管理器。
需要翻译时,安装同次发行、相同格式的 `luci-i18n-argon-config-<语言>` 语言包。
在 **系统 → 系统 → 语言和界面 → 主题** 中选择 **Argon**。
<details>
<summary>已安装旧版 luci-app-argon-config?如何迁移到 3.0</summary>
仅适用于已安装旧版独立配置插件的用户;没有安装过该插件可跳过。
3.0 的设置页面、配色、背景和 Logo 管理都在主题包内。发行附件中的 `luci-app-argon-config` 是**沿用旧包名的空迁移包**,用于保留旧配置并将文件交给主题,不是另一个配置插件。
1. 从**主题仓库同一次 3.0 发行**中下载 `luci-app-argon-config` 迁移包和 `luci-theme-argon` 主题包,格式一致。
2. 将两个文件放到路由器同一目录,在该目录打开终端。**无需先卸载旧插件**,按固件选择下面一组命令;第一条成功后,再执行第二条。
```sh
uci set 'argon.@global[0].use_api_key=你的_UNSPLASH_ACCESS_KEY'
uci commit argon
# opkg
opkg install ./luci-app-argon-config_*.ipk
opkg install ./luci-theme-argon_*.ipk
# APK v3
apk add --allow-untrusted ./luci-app-argon-config-*.apk
apk add --allow-untrusted ./luci-theme-argon-*.apk
```
未配置密钥时,登录页会使用本地背景。Bing 和 Wallhaven 壁纸选项不受影响。
第一步将旧插件升级为空迁移包,第二步安装包含配置功能的新主题,并恢复已有设置。之后继续在 **系统 → Argon 主题设置** 中配置即可。
## 自定义网站图标与登录 Logo
</details>
在 **系统 → Argon 主题设置 → 自定义图标** 中,可以分别上传网站图标和登录页 Logo,也可以将同一张图片同时用于两处;每项都可恢复默认。支持 PNG 格式,每边 16–1024 像素,最大 1 MiB。网站图标需为正方形;登录 Logo 按原比例缩放,兼容经典侧栏和居中卡片。
## 自定义
请同时更新主题和配置插件。上传或恢复后立即生效,无需保存其他设置;已打开的页面需要刷新才能更新图标。自定义网站图标也用于手机桌面图标和 manifest,已添加到桌面的快捷方式可能需要重新添加。
打开 **系统 → Argon 主题设置**,调整登录布局、配色、毛玻璃、壁纸和图标。页面右上角可切换**亮色模式、暗色模式、跟随系统**,也会应用于登录页。
自定义文件保存在 `/www/luci-static/argon/branding/`,不会覆盖主题内置图标。配置插件通过 `keep.d` 将此目录纳入保留配置升级的备份;不保留配置时不会保留自定义图标。
背景支持本地图片、视频,以及 Bing、Unsplash、Wallhaven 在线壁纸;Unsplash 需填写 API Access Key。网站图标和登录 Logo 支持 PNG,每边 16–1024 像素、最大 1 MiB,网站图标需为正方形。
## 兼容性
## 效果展示
目前仅维护 `master` 分支。
当前主要面向 [官方 OpenWrt][official] 和 [ImmortalWrt][immortalwrt] 的较新版本 LuCI 环境。
亮暗两色,展示桌面与手机端的首页和两套登录界面。
## 版本历史
![浅色:桌面首页、经典侧栏与居中登录](Screenshots/preview-light.png)
当前最新的版本为 v2.4.8 [点击这里][zh-cn-release-log]查看完整的版本历史日志.
![手机浅色:经典侧栏登录、居中登录和首页](Screenshots/mobile-light.png)
## 快速开始
![深色:桌面首页、经典侧栏与居中登录](Screenshots/preview-dark.png)
### 从源码编译
```bash
cd openwrt/package
git clone https://github.com/jerrykuku/luci-theme-argon.git
make menuconfig #choose LUCI->Theme->Luci-theme-argon
make -j1 V=s
```
### 选择安装包
请按固件原生使用的包管理器选择格式,不能仅根据系统中是否有 `apk` 命令判断:
| 固件的包管理器 | Release 附件 | 安装命令 |
| --- | --- | --- |
| `opkg`(如 OpenWrt 23.05、24.10,以及基于这些版本的 GL.iNet 固件) | `.ipk` | `opkg install` |
| 原生使用 APK v3 的 OpenWrt / ImmortalWrt(`apk-tools 3.x`) | `.apk` | `apk add --allow-untrusted` |
本项目的 APK 使用 **APK v3 格式**,`apk-tools 2.x`(包括 `2.14.0`)无法读取。`--allow-untrusted` 只跳过签名信任检查,不能解决格式不兼容;`apk` 也不能安装 IPK。
APK 构建使用 OpenWrt **25.12.5 稳定版 SDK**,主题模板保留源码以兼容目标运行时。原始 v2.4.8 APK 使用 snapshot SDK 构建,要求 `ucode>=2026.02.27`,而 OpenWrt 25.12.5 不提供该版本([#715](https://github.com/jerrykuku/luci-theme-argon/issues/715))。该附件需要包含本次修复后重新构建;更新仓库代码不会替换已经发布的安装包。请勿绕过依赖检查安装不兼容的字节码。
对于 [#705](https://github.com/jerrykuku/luci-theme-argon/issues/705) 中的 GL.iNet / OpenWrt 23.05 环境,应下载 IPK 并使用固件原有的 `opkg` 安装,无需替换系统包管理器。
从 [Release][release] 下载主题包 `luci-theme-argon`、配置插件包 `luci-app-argon-config`,以及需要的 `luci-i18n-argon-config-<语言>` 语言包。所有包必须选择相同格式,以附件的实际文件名为准,不要自行拼接版本号或修改扩展名。
将文件放在同一个空目录中,每个包只保留一个版本,然后在该目录执行下面对应的命令。
### 安装 release 包 (`ipk`)
```bash
opkg update
opkg install ./luci-theme-argon_*.ipk ./luci-app-argon-config_*.ipk
```
如已下载相同格式的语言包,再执行:
```bash
opkg install ./luci-i18n-argon-config-*.ipk
```
### 安装 release 包 (`apk`)
仅适用于固件原生使用 `apk-tools 3.x` 的环境,可用 `apk --version` 查看版本。
```bash
apk update
apk add --allow-untrusted ./luci-theme-argon-*.apk ./luci-app-argon-config-*.apk
```
如已下载相同格式的语言包,再执行:
```bash
apk add --allow-untrusted ./luci-i18n-argon-config-*.apk
```
遇到 `IO ERROR` 时,先用 `ls -l ./*.apk` 核对文件确实存在、名称正确且下载完整,再检查 `apk --version`;不要通过强制安装绕过包格式或依赖问题。
## 屏幕截图
![desktop](/Screenshots/screenshot_pc.jpg)
![mobile](/Screenshots/screenshot_phone.jpg)
![手机深色:经典侧栏登录、居中登录和首页](Screenshots/mobile-dark.png)
## 贡献者
@@ -169,12 +109,10 @@ apk add --allow-untrusted ./luci-i18n-argon-config-*.apk
Made with [contrib.rocks](https://contrib.rocks).
## 相关项目
## 开发
- [luci-app-argon-config](https://github.com/jerrykuku/luci-app-argon-config): Argon 主题设置插件
- [openwrt-package](https://github.com/jerrykuku/openwrt-package): 我的 OpenWrt 软件包集合
- [CasaOS](https://github.com/IceWhaleTech/CasaOS): 一个简单、易用且优雅的开源个人云系统,也是我目前主要投入的项目
源码构建、LESS、模板、图标和检查说明见[开发文档](docs/DEVELOPMENT_ZH.md)。
## 鸣谢
[下载](https://github.com/jerrykuku/luci-theme-argon/releases) · [版本历史](RELEASE_ZH.md) · [贡献者](https://github.com/jerrykuku/luci-theme-argon/graphs/contributors) · [反馈问题](https://github.com/jerrykuku/luci-theme-argon/issues)
[luci-theme-material](https://github.com/LuttyYang/luci-theme-material/)
采用 [Apache-2.0](LICENSE) 许可证。感谢 [luci-theme-material](https://github.com/LuttyYang/luci-theme-material/)。
+62
View File
@@ -1,5 +1,67 @@
# Update log for master branch
## v3.0.0 [ 2026.10.16 ]
### Argon 3.0
- Integrate Argon Config, wallpaper uploads and custom branding into the theme package, with a compatibility package for existing installations.
- Add classic-sidebar and centered-card login layouts, persistent light/dark/system appearance switching, and automatic dark accent colors.
- Improve desktop and mobile layouts, shared controls, tables, tabs, dialogs and most third-party LuCI applications.
- Replace icon fonts with SVG menu/control icons and complete Argon Config translations.
- Refresh the English/Chinese README, desktop demo, screenshots and development guides.
- Fix translation package versions and upgrade ordering; verify every language package before publication.
- Reduce package size with a WebP login background and optimized WOFF2 fonts, retaining all font characters and rendering instructions.
- 将 Argon Config、背景上传和自定义图标集成到主题,提供旧版配置插件迁移包。
- 提供经典侧栏、居中卡片两套登录布局,持久化亮色/暗色/跟随系统切换,以及自动暗色配色。
- 优化桌面与手机布局、通用控件、表格、标签页、弹窗和大多数第三方 LuCI 应用的适配。
- 使用 SVG 菜单/控件图标替代图标字体,补全 Argon Config 翻译。
- 更新中英文 README、桌面演示动画、截图和开发说明。
- 修复语言包版本及升级顺序,发布前逐包核验全部语言包版本。
- 使用 WebP 登录背景并精简 WOFF2 字体,保留全部字符和渲染指令,减小安装包体积。
This v3.0.0 rebuild uses package revision **2**: IPK `3.0.0-2`, APK `3.0.0-r2`. Translation versions are `v3.0.0-2` (IPK) and `3.0.0-r2` (APK), allowing normal upgrades from the original `unknown` / `0` language packages.
本次 v3.0.0 重发采用安装包修订号 **2**:IPK 为 `3.0.0-2`,APK 为 `3.0.0-r2`。语言包分别为 `v3.0.0-2`(IPK)和 `3.0.0-r2`(APK),可从原 `unknown`/`0` 语言包正常升级。
### Installation / 安装说明
| Firmware / 固件 | Package / 安装包 |
| --- | --- |
| Native `opkg`, including OpenWrt 23.05 / 24.10 and GL.iNet firmware based on them / 原生使用 `opkg` 的固件 | `.ipk` → `opkg install` |
| Native APK v3 with `apk-tools 3.x` / 原生使用 APK v3 的固件 | `.apk` → `apk add --allow-untrusted` |
APK assets use **APK v3** and cannot be read by `apk-tools 2.x`, including `2.14.0`. Having an `apk` command installed on an opkg-based firmware does not make it APK v3 compatible. `--allow-untrusted` does not change format compatibility, and `apk` cannot install IPK files.
APK 附件采用 **APK v3 格式**,`apk-tools 2.x`(包括 `2.14.0`)无法读取。在原生使用 opkg 的固件上额外安装 `apk`,并不会让固件兼容 APK v3;`--allow-untrusted` 不能解决格式不兼容,`apk` 也不能安装 IPK。
APK builds use the OpenWrt **25.12.5 stable SDK**. Theme templates are shipped as source to avoid snapshot-specific ucode bytecode requirements. If an older APK reports `ucode>=2026.02.27`, use a package rebuilt with the [#715 fix](https://github.com/jerrykuku/luci-theme-argon/issues/715); bypassing dependency checks does not make that bytecode compatible.
APK 使用 OpenWrt **25.12.5 稳定版 SDK** 构建,主题模板保留源码,避免 snapshot 的 ucode 字节码版本限制。若旧 APK 提示需要 `ucode>=2026.02.27`,请使用包含 [#715 修复](https://github.com/jerrykuku/luci-theme-argon/issues/715) 的重新构建包;绕过依赖检查不能解决字节码不兼容。
Download the theme and required language packages in the **same format**. Use the exact asset filenames below. For GL.iNet / OpenWrt 23.05, use IPK files with the firmware's existing `opkg`.
主题和需要的语言包应选择**相同格式**,以本页附件实际文件名为准。GL.iNet / OpenWrt 23.05 请下载 IPK 并使用固件原有的 `opkg` 安装。
### Argon 3.0 settings integration / 设置集成
Settings are included in `luci-theme-argon`. Fresh installations need only the theme and desired `luci-i18n-argon-config-*` translations.
设置已内置主题,新安装无需配置插件。已有旧插件时,先单独安装本次发行的 `luci-app-argon-config` 空兼容包,再安装主题;该顺序会保留旧配置并完成文件归属交接。
For an existing configuration plugin, install this release's empty `luci-app-argon-config` compatibility package first, then install the theme in a separate command. Do not install both in one transaction or force overwrites.
### What's Changed
* fix(cascade): add bottom spacing for argon config form by @TuringTrail in https://github.com/jerrykuku/luci-theme-argon/pull/709
* feat(sysauth): render Authentication Login plugins on the login page by @FuSan21 in https://github.com/jerrykuku/luci-theme-argon/pull/710
* fix(sysauth): avoid blue outline on focused login inputs by @TuringTrail in https://github.com/jerrykuku/luci-theme-argon/pull/712
### New Contributors
* @TuringTrail made their first contribution in https://github.com/jerrykuku/luci-theme-argon/pull/709
**Full Changelog**: https://github.com/jerrykuku/luci-theme-argon/compare/v2.4.7...v3.0.0
## v2.4.8 [ 2026.10.08 ]
### v2.4.8 r2 rebuild / 修订版重发
+5
View File
@@ -1,5 +1,10 @@
# Master 分支的更新日志
## v3.0.0 [ 2026.10.16 ]
- GitHub Release: https://github.com/jerrykuku/luci-theme-argon/releases/tag/v3.0.0
- 详细更新内容请以 Release 页面为准。
## v2.4.8 [ 2026.10.08 ]
- GitHub Release: https://github.com/jerrykuku/luci-theme-argon/releases/tag/v2.4.8
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

@@ -0,0 +1,30 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-argon-config
PKG_VERSION:=3.0.0
PKG_RELEASE:=20261016
PKG_LICENSE:=Apache-2.0
LUCI_TITLE:=Argon configuration compatibility package
LUCI_PKGARCH:=all
# Upgrade this empty package before installing the integrated theme. Keeping
# the dependency unversioned lets an existing theme satisfy this first step.
LUCI_DEPENDS:=+luci-theme-argon
# Save even an unchanged legacy conffile before the empty package releases it.
# The integrated theme consumes this private backup on its first initialization.
define Package/luci-app-argon-config/preinst
#!/bin/sh
root="$${IPKG_INSTROOT}"
if [ ! -f "$$root/usr/share/argon/config-integrated" ] && \
[ -f "$$root/etc/config/argon" ]; then
umask 077
mkdir -p "$$root/etc/argon-config-migration" || exit 1
cp -p "$$root/etc/config/argon" "$$root/etc/argon-config-migration/argon" || exit 1
fi
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - make this nested recipe visible to OpenWrt package scans.
+100
View File
@@ -0,0 +1,100 @@
# Developing Argon 3.0
[简体中文](DEVELOPMENT_ZH.md) · [README](../README.md)
## Build with OpenWrt
From an OpenWrt build tree:
```sh
cd package
git clone https://github.com/jerrykuku/luci-theme-argon.git
cd ..
make menuconfig
# LuCI → Themes → luci-theme-argon
make package/luci-theme-argon/compile V=s
```
Theme settings are included. Select the nested `compat/luci-app-argon-config` package under LuCI applications only when building the migration package. Existing translation names remain `luci-i18n-argon-config-*`.
Release CI builds IPK with the OpenWrt 23.05.5 SDK and APK v3 with the 25.12.5 SDK. Ucode templates ship as source to avoid tying packages to one runtime's bytecode format.
Translations use `PKG_PO_VERSION` to follow the theme release, including `-r<revision>` for APK. IPK translations retain a `v` prefix (for example, `v3.0.1`) so ordinary installation upgrades the broken v3.0.0 `unknown` version. Keep that prefix in future IPK releases. CI reads each translation's internal package metadata and blocks artifact upload and publication if the version is wrong.
## Source map
| Location | Responsibility |
| --- | --- |
| `less/cascade.less` | Style entry point and import order |
| `less/theme.less` | Light/dark palette and semantic color tokens |
| `less/spacing.less`, `less/shape.less` | Spacing and corner tokens |
| `less/components/` | Shared controls, tables, tabs, dialogs and appearance menu |
| `less/pages/`, `less/page-fix.less` | LuCI/plugin adaptations and their import index |
| `less/responsive.less` | Viewport adaptations |
| `htdocs/luci-static/argon/` | Compiled CSS, images, fonts and SVG icons |
| `htdocs/luci-static/resources/` | Theme settings and LuCI JavaScript modules |
| `ucode/template/themes/argon/` | Login and application templates |
| `root/` | UCI defaults, RPC services, ACLs and integration files |
| `po/` | Argon Config translations |
| `tests/` | Behavior and integration checks |
## Styles
Edit LESS sources, then regenerate the committed stylesheet with LESS 4.1.3:
```sh
npm install --global less@4.1.3
lessc less/cascade.less htdocs/luci-static/argon/css/cascade.css
```
Light and dark modes share this one stylesheet. Each `@theme-palette` row in `theme.less` defines `role: light, dark;`; one value means both modes share it. Use semantic tokens for text, surfaces, borders and states. Legacy variable aliases exist for LuCI compatibility.
Imports run from tokens and base controls through login/application layout, shared components, page adaptations, and responsive rules. Add common behavior to its component module; keep plugin selectors in `pages/`. The `page-fix.less` file is an import index.
Maintain page/card insets and section, field, control and action gaps through spacing tokens. Small screens reduce outer and card insets to `1rem`. Keep long labels wrapping and let form controls shrink to their containers. Escape native CSS `min()` expressions when LESS would evaluate them prematurely.
Corner tokens cover labels, controls, panels, cards and dialogs. Keep the inside edges of joined controls square. Avoid clipping entire cards: dropdowns and tooltips can extend beyond them. Tables handle their own final-row corners and narrow-screen layout.
Preserve native LuCI structures and interactions. Tab bars join their following content containers; section content owns its vertical spacing. Modal actions wrap or stack as space requires, and long content scrolls through the overlay. Dropdown positioning and scrolling remain controlled by LuCI. Restrict hover effects to hover-capable devices and respect reduced motion.
## Templates
LuCI's `header`, `header_login`, `out_header_login`, `sysauth` and footer entry points remain available.
| Template | Responsibility |
| --- | --- |
| `head.ut` | Validated settings, metadata, styles and conditional application resources |
| `sysauth.ut` | Login layout and shared template assembly |
| `login_background.ut` | Wallpaper selection and image/video output |
| `login_form.ut` | Authentication fields, messages and plugin content |
| `page_scripts.ut` | Scripts shared by the footer entry points |
Pass local values through the second argument of `include`. Login pages do not load application LuCI modules. Only `classic` and `centered` are accepted as `login_style`; unknown values fall back to `classic`.
## Configuration and branding
Settings stay in `/etc/config/argon`; the settings route remains `/admin/system/argon-config`. Fresh installs use local backgrounds, classic login, 10px blur and 0.5 panel opacity. Existing options survive upgrades.
New installations derive dark colors from the primary seed. Existing explicit dark colors stay custom. Dark login glass settings can inherit the light values or keep independent overrides. Switching back to automatic/inherited settings retains custom values for later use.
The header appearance menu saves only its mode through `luci.argon_appearance`. The preference is device-wide and requires Argon write access. Keep this independent from pending form settings; failed saves reconcile with the stored mode.
Uploaded branding lives in `/www/luci-static/argon/branding/`; backgrounds live in `/www/luci-static/argon/background/`. Upload/reset operations apply immediately. Branding does not replace packaged images, and both directories are included in configuration-preserving upgrade backups. The browser icon also supplies the touch icon and manifest; existing home-screen shortcuts may need to be added again.
## Menu icons
Top-level menu nodes resolve `htdocs/luci-static/argon/icon/<node-name>.svg`, independently of translated titles. Filenames are case-sensitive and accept letters, digits, underscores and hyphens. Missing icons use `default.svg`.
Use monochrome SVGs with a `viewBox` and transparent background. CSS masks supply theme and interaction colors. Shared chevrons use the same 24×24 grid and 2px round strokes. See the [icon index](../htdocs/luci-static/argon/icon/README.md) for available assets. This mechanism is separate from uploaded browser icons and login logos.
## Checks
Use Node.js 22+, Python 3, LESS 4.1.3 and a Ucode build matching the supported OpenWrt runtime:
```sh
UCODE_BIN=/path/to/ucode sh .github/scripts/check_theme.sh
```
The script checks JavaScript, shell and Ucode syntax, verifies compiled CSS against LESS, and runs the Node/Python tests. CI's Ucode build is defined in [check.yml](../.github/workflows/check.yml).
For UI changes, check the actual affected LuCI pages in light/dark modes and desktop/mobile widths, including keyboard focus, long text and reduced motion where relevant. Network and hardware behavior still needs a suitable OpenWrt environment.
+100
View File
@@ -0,0 +1,100 @@
# Argon 3.0 开发说明
[English](DEVELOPMENT.md) · [返回 README](../README_ZH.md)
## 源码构建
在 OpenWrt 构建目录中执行:
```sh
cd package
git clone https://github.com/jerrykuku/luci-theme-argon.git
cd ..
make menuconfig
# LuCI → Themes → luci-theme-argon
make package/luci-theme-argon/compile V=s
```
主题设置已内置。仅在需要构建旧插件迁移包时,在 LuCI 应用中额外选择嵌套的 `compat/luci-app-argon-config` 包。语言包继续使用 `luci-i18n-argon-config-*` 名称。
发行 CI 使用 OpenWrt 23.05.5 SDK 构建 IPK、25.12.5 SDK 构建 APK v3。Ucode 模板保留源码,避免绑定某个运行时的字节码格式。
语言包通过 `PKG_PO_VERSION` 跟随主题发行版本,APK 包含 `-r<修订号>`。IPK 语言包固定使用 `v` 前缀(如 `v3.0.1`),使已安装的 v3.0.0 `unknown` 语言包可以直接正常升级;后续版本须保留该前缀。发布前会逐个读取语言包内部元数据,版本不符时阻止上传和发布。
## 目录
| 位置 | 用途 |
| --- | --- |
| `less/cascade.less` | 样式入口及导入顺序 |
| `less/theme.less` | 亮暗配色和语义颜色 token |
| `less/spacing.less`、`less/shape.less` | 间距及圆角 |
| `less/components/` | 通用控件、表格、标签页、弹窗、外观菜单 |
| `less/pages/`、`less/page-fix.less` | LuCI / 插件适配及导入索引 |
| `less/responsive.less` | 视口适配 |
| `htdocs/luci-static/argon/` | 编译后的 CSS、图片、字体和 SVG |
| `htdocs/luci-static/resources/` | 设置页与 LuCI JavaScript 模块 |
| `ucode/template/themes/argon/` | 登录页和后台模板 |
| `root/` | UCI 默认配置、RPC、ACL 及系统集成文件 |
| `po/` | Argon Config 翻译 |
| `tests/` | 行为与集成检查 |
## 样式
修改 LESS 源码后,使用 LESS 4.1.3 重新生成并一同提交 CSS:
```sh
npm install --global less@4.1.3
lessc less/cascade.less htdocs/luci-static/argon/css/cascade.css
```
亮暗色共用一份样式。`theme.less` 中 `@theme-palette` 每行以 `角色: 亮色, 暗色;` 定义颜色;单值表示两种模式共用。文字、背景、边框和状态使用各自语义 token;旧变量别名仅用于 LuCI 兼容。
导入顺序为 token、基础控件、登录与后台布局、共享组件、页面适配、响应式规则。通用行为放入对应组件模块,插件选择器放在 `pages/`;`page-fix.less` 只维护导入索引。
页面及卡片内缩、section、字段、控件与操作间距使用统一 token;小屏外边距及卡片内缩为 `1rem`。长文案允许换行,控件可收缩到容器宽度。LESS 会提前求值的原生 CSS `min()` 表达式需转义。
圆角 token 覆盖标签、控件、面板、卡片和弹窗;组合控件的内部接缝保持直角。不要裁切整个卡片,避免截断下拉菜单与提示框;表格自行处理末行圆角和窄屏布局。
保留 LuCI 原生结构和交互:标签栏与相邻内容容器衔接,section 内容负责纵向间距;弹窗操作随空间换行或堆叠,长正文通过覆盖层滚动。下拉位置和滚动继续由 LuCI 控制。悬浮效果仅用于支持 hover 的设备,并尊重减少动画偏好。
## 模板
保留 LuCI 的 `header`、`header_login`、`out_header_login`、`sysauth` 和页脚入口。
| 模板 | 用途 |
| --- | --- |
| `head.ut` | 配置校验、元信息、样式及后台资源加载 |
| `sysauth.ut` | 登录布局和模板组装 |
| `login_background.ut` | 壁纸选择及图片 / 视频输出 |
| `login_form.ut` | 认证字段、提示和插件内容 |
| `page_scripts.ut` | 页脚共用脚本 |
通过 `include` 的第二个参数传递局部数据。登录页不加载后台 LuCI 模块。`login_style` 仅接受 `classic`、`centered`,其他值回退为 `classic`。
## 配置与品牌资源
配置保存在 `/etc/config/argon`,设置地址仍为 `/admin/system/argon-config`。首次安装采用本地背景、经典登录、10px 模糊和 0.5 透明度;升级保留已有选项。
新安装根据主色自动生成暗色配色,已有独立暗色主色保留为自定义。暗色登录面板可跟随亮色或单独配置;切回自动 / 跟随时仍保留自定义值。
顶部外观菜单通过 `luci.argon_appearance` 单独保存模式,对整个设备生效,需要 Argon 写权限。保持与未保存表单设置独立;保存失败时核对服务端已保存值。
自定义图标位于 `/www/luci-static/argon/branding/`,背景位于 `/www/luci-static/argon/background/`。上传 / 恢复立即生效,不覆盖内置文件;两个目录均纳入保留配置升级的备份。网站图标也用于 touch icon 和 manifest,已有手机桌面快捷方式可能需要重新添加。
## 菜单图标
一级菜单按节点名查找 `htdocs/luci-static/argon/icon/<节点名>.svg`,与翻译后的标题无关。文件名区分大小写,支持字母、数字、下划线和连字符;缺失时使用 `default.svg`。
使用透明背景、带 `viewBox` 的单色 SVG,CSS mask 负责主题与交互颜色。共享箭头使用 24×24 网格、2px 圆角描边。资源见 [SVG 图标索引](../htdocs/luci-static/argon/icon/README.md),与用户上传的网站图标 / 登录 Logo 独立。
## 检查
准备 Node.js 22+、Python 3、LESS 4.1.3,以及与目标 OpenWrt 运行时匹配的 Ucode:
```sh
UCODE_BIN=/path/to/ucode sh .github/scripts/check_theme.sh
```
脚本检查 JavaScript、Shell、Ucode 语法,核对 LESS 与 CSS 产物,并运行 Node / Python 测试。CI 的 Ucode 构建步骤见 [check.yml](../.github/workflows/check.yml)。
UI 改动应检查实际受影响页面的亮暗色、桌面与窄屏,以及相关的键盘焦点、长文本和减少动画场景。网络和硬件行为仍需相应 OpenWrt 环境验证。
File diff suppressed because it is too large. Load diff
@@ -1,3 +0,0 @@
:root{--table-border:0}body{background:#1e1e1e;color:#ccc}.spinning::before{filter:invert(1)}.login-page .login-container{background-color:rgba(0,0,0,var(--blur-opacity-dark));-webkit-backdrop-filter:blur(var(--blur-radius-dark));backdrop-filter:blur(var(--blur-radius-dark))}.login-page .login-container .login-form{background-color:transparent;-webkit-backdrop-filter:none;backdrop-filter:none}.login-page .login-container .login-form .brand{color:#adb5bd}.login-page .login-container .login-form .form-login .input-group::before{color:#adb5bd}.login-page .login-container .login-form .form-login .input-group input{background-color:transparent!important;color:#adb5bd;border-bottom:#adb5bd 1px solid!important;border-radius:0!important;border-top:none!important;border-left:none!important;border-right:none!important;box-shadow:none}.login-page .login-container .login-form .form-login .cbi-button-apply{background-color:#483d8b!important;background-color:var(--dark-primary)!important}.login-page .login-container .login-form .form-login .cbi-button-apply:focus,.login-page .login-container .login-form .form-login .cbi-button-apply:hover{opacity:.9}header::after{background-color:#1e1e1e!important}.main .main-left{background-color:#333!important;box-shadow:0 0 .5rem 0 rgba(0,0,0,.15)}.main .main-left .sidenav-header .brand{color:#ccc}.main .main-left .nav .slide .slide-menu .active a{color:#ccc}.main .main-left .nav .slide .slide-menu .active a::after{background-color:#ccc!important}.main .main-left .nav .slide .slide-menu .active::after{background-color:var(--dark-primary)!important}.main .main-left .nav .slide .slide-menu li a{color:#ccc}.main .main-left .nav .slide .slide-menu li a:hover{background:0 0!important}.main .main-left .nav .slide .slide-menu li::after{background-color:var(--dark-primary)!important}.main .main-left .nav .slide .menu.active{background-color:#483d8b!important;background-color:var(--dark-primary)!important;color:#fff!important}.main .main-left .nav .slide .menu.active a::after{background-color:#fff!important}.main .main-left .nav li a{color:#ccc!important}.main .main-left .nav li a:hover{background-color:#483d8b!important;background-color:var(--dark-primary)!important;color:#fff!important}.main .main-left::-webkit-scrollbar-thumb{background-color:#252526!important}.main .main-left::-webkit-scrollbar-track{background-color:#333}.main .main-right{background-color:#1e1e1e}h2{color:#ccc;background:#333}h3{color:#ccc;border-bottom:0;background:#333}h4{color:#8c6900}abbr{color:var(--dark-primary)!important}a:active,a:link,a:visited{color:#a5b2ff}input:-webkit-autofill{background-color:#3c3c3c!important}#channel_graph{background-color:transparent!important}.cbi-button-apply,.cbi-button-edit,.cbi-value-field .cbi-input-apply{color:#fff!important;background-color:#483d8b!important;background-color:var(--dark-primary)!important;border-color:#483d8b!important;border-color:var(--dark-primary)!important}.cbi-section em{color:#ccc}header.bg-primary{background-color:#1e1e1e!important}.cbi-map-descr{color:#ccc}.cbi-section{background:0 0;box-shadow:0 0 .5rem 0 rgba(0,0,0,.35)}.panel-title{color:#ccc;background-color:#333}div>.table>.tr:nth-of-type(2n),div>table>tbody>tr:nth-of-type(2n){background-color:#252526}table>tbody>tr>td,table>tfoot>tr>td,table>thead>tr>td{color:#ccc}fieldset>table>tbody>tr:nth-of-type(2n){background-color:#252526}table>tbody>tr>td,table>tfoot>tr>td,table>thead>tr>td{border-top:1px solid #252526}#conns>div>div,#membuff>div>div,#memfree>div>div,#memtotal>div>div,#swapfree>div>div,#swaptotal>div>div{background-color:var(--dark-primary)!important}#conns>div>div>div>small,#membuff>div>div>div>small,#memfree>div>div>div>small,#memtotal>div>div>div>small,#swapfree>div>div>div>small,#swaptotal>div>div>div>small{color:#ccc!important}.node-system-packages>.main .cbi-section-node:first-child .cbi-value-last{line-height:1.8em}.node-system-packages>.main .cbi-section-node:first-child .cbi-value-last div[style="margin:3px 0; width:300px; height:10px; border:1px solid #000000; background-color:#80C080"]{border:1px solid #999!important;background-color:transparent!important}.node-system-packages>.main .cbi-section-node:first-child .cbi-value-last div[style="margin:3px 0; width:300px; height:10px; border:1px solid #000000; background-color:#80C080"] div{background-color:#ba8b00!important}table>tbody>tr>th,table>tfoot>tr>th,table>thead>tr>th{background-color:#252526;border-bottom:#000 1px solid!important}.tr.cbi-section-table-titles.named::before,.tr[data-title]::before{background-color:#252526}#cbi-wireless>#wifi_assoclist_table>.tr:nth-child(2),.cbi-section-table-row::before,.tr>.td,.tr>.th,tr>td,tr>th{border-top:0}.cbi-rowstyle-2{background-color:#1e1e1e}.cbi-rowstyle-1{background-color:#252526}.cbi-rowstyle-2 .cbi-button-down,.cbi-rowstyle-2 .cbi-button-up,body:not(.Interfaces) .cbi-rowstyle-2:first-child{background-color:#666!important}.cbi-section>h3:first-child,.panel-title,h3{color:#ccc}h4{backLine truncated
.modal>pre,.modal>textarea{color: #ccc}
input[placeholder]::placeholder {color: #888888;opacity: 1}
@@ -0,0 +1,50 @@
# Argon SVG icons / SVG 图标
`copy.svg` 和 `download.svg` 是文件管理操作使用的补充图标,采用24×24画布、2px描边,并通过CSS mask跟随控件颜色;不属于原字体导出清单。
原 `argon.woff2` 中全部 **28 个可见图标**已按原始矢量轮廓导出,字体文件已从主题移除。文件名使用实际服务或功能名称,例如 `status.svg`、`services.svg`、`docker.svg`;Unicode 编码仅保留在对照表中。SVG 使用透明背景和 `viewBox`,可用于 CSS mask 或内联 SVG。
All **28 visible icons** from the former `argon.woff2` font are exported with their original vector outlines; the font files have been removed from the theme. Filenames use service or function names, such as `status.svg`, `services.svg`, and `docker.svg`; Unicode code points are kept in the mapping. SVGs have transparent backgrounds and view boxes, suitable for CSS masks or inline SVG.
`chevron-down.svg` 和 `chevron-right.svg` 在导出后统一重绘为 24×24、2px 描边、圆端点和圆连接。两者互为旋转,保留原字体编码作为功能对照。CSS mask 跟随控件颜色;作为原生 select 或排序按钮的背景图时,使用 SVG 的中性默认颜色。其余 26 个图标保留字体轮廓。对照表中的 `sourceViewBox` 与 `designOverride` 标记这两处设计替换。
The two chevrons are redesigned on a shared 24×24 grid with a 2px stroke, round caps and joins, preserving their original code point mappings. CSS masks inherit the control color; native select and sorting backgrounds use the SVG's neutral default color. The other 26 icons keep their font outlines. Mapping entries record `sourceViewBox` and `designOverride` for these exceptions.
字体共有 33 个字形;`.notdef`、`uni0000`、`nonmarkingreturn`、`uni0001`、`space` 没有可见轮廓,无需生成空 SVG。[font-icons.json](font-icons.json) 记录完整的编码、字形名、文件名、画布尺寸和源字体 SHA-256。画布保留字体坐标与留白,并扩展至包含越界轮廓,避免裁切。
The font contains 33 glyphs. The five invisible glyphs listed above have no SVG output. [font-icons.json](font-icons.json) records code points, glyph names, filenames, canvas sizes, and the source font SHA-256. View boxes preserve font spacing and expand where an outline exceeds the original em square.
一级菜单按节点名称直接匹配 SVG,例如状态 → `status.svg`、系统 → `system.svg`、服务 → `services.svg`、网络 → `network.svg`、Docker → `docker.svg`。缺失时使用 `default.svg`(原字体的软件包图标)。登录输入框使用 `user.svg`、`password.svg`,移动菜单按钮使用 `menu.svg`。主题不再依赖图标字体;对照表中的源文件名与 SHA-256 仅作为导出来源记录,`sourceIncluded: false` 表示源字体不随主题分发。
Top-level menus match their node names directly, for example `status.svg`, `system.svg`, `services.svg`, `network.svg`, and `docker.svg`. Missing icons use `default.svg`, the original package glyph. Login fields use `user.svg` and `password.svg`; the mobile menu toggle uses `menu.svg`. The theme no longer depends on an icon font. The source filename and SHA-256 remain as export provenance; `sourceIncluded: false` records that the original font is not distributed.
| 预览 / Preview | 编码 / Code point | SVG | 图形 / Shape |
| --- | --- | --- | --- |
| <img src="chevron-down.svg" width="32" height="32" alt="Chevron down"> | `U+E20B` | [chevron-down.svg](chevron-down.svg) | 下箭头 / Chevron down |
| <img src="menu.svg" width="32" height="32" alt="Menu"> | `U+E20E` | [menu.svg](menu.svg) | 菜单 / Menu |
| <img src="heart.svg" width="32" height="32" alt="Heart"> | `U+E291` | [heart.svg](heart.svg) | 心形 / Heart |
| <img src="ring.svg" width="32" height="32" alt="Ring"> | `U+E603` | [ring.svg](ring.svg) | 环形 / Ring |
| <img src="delete.svg" width="32" height="32" alt="Trash"> | `U+E900` | [delete.svg](delete.svg) | 删除 / Trash |
| <img src="edit.svg" width="32" height="32" alt="Edit"> | `U+E901` | [edit.svg](edit.svg) | 编辑 / Edit |
| <img src="check.svg" width="32" height="32" alt="Check"> | `U+E902` | [check.svg](check.svg) | 勾选 / Check |
| <img src="loading.svg" width="32" height="32" alt="Loading dots"> | `U+E903` | [loading.svg](loading.svg) | 加载圆点 / Loading dots |
| <img src="transfer.svg" width="32" height="32" alt="Transfer arrows"> | `U+E904` | [transfer.svg](transfer.svg) | 上传下载 / Transfer arrows |
| <img src="close.svg" width="32" height="32" alt="Close"> | `U+E905` | [close.svg](close.svg) | 关闭 / Close |
| <img src="status.svg" width="32" height="32" alt="Dashboard"> | `U+E906` | [status.svg](status.svg) | 状态 / Dashboard |
| <img src="logout.svg" width="32" height="32" alt="Log out"> | `U+E907` | [logout.svg](logout.svg) | 退出 / Log out |
| <img src="network.svg" width="32" height="32" alt="Network"> | `U+E908` | [network.svg](network.svg) | 网络 / Network |
| <img src="services.svg" width="32" height="32" alt="Services"> | `U+E909` | [services.svg](services.svg) | 服务 / Services |
| <img src="system.svg" width="32" height="32" alt="System"> | `U+E90A` | [system.svg](system.svg) | 系统 / System |
| <img src="vpn.svg" width="32" height="32" alt="VPN"> | `U+E90B` | [vpn.svg](vpn.svg) | VPN |
| <img src="nas.svg" width="32" height="32" alt="Database"> | `U+E90C` | [nas.svg](nas.svg) | 存储服务 / NAS |
| <img src="bandwidth_monitor.svg" width="32" height="32" alt="Bandwidth monitor"> | `U+E90D` | [bandwidth_monitor.svg](bandwidth_monitor.svg) | 带宽监控 / Bandwidth monitor |
| <img src="hello-world.svg" width="32" height="32" alt="Hello world"> | `U+E90E` | [hello-world.svg](hello-world.svg) | Hello world |
| <img src="chevron-right.svg" width="32" height="32" alt="Chevron right"> | `U+E90F` | [chevron-right.svg](chevron-right.svg) | 右箭头 / Chevron right |
| <img src="password.svg" width="32" height="32" alt="Unlocked padlock"> | `U+E910` | [password.svg](password.svg) | 密码 / Unlocked padlock |
| <img src="docker.svg" width="32" height="32" alt="Docker"> | `U+E911` | [docker.svg](docker.svg) | Docker |
| <img src="control.svg" width="32" height="32" alt="Controls"> | `U+E912` | [control.svg](control.svg) | 控制 / Controls |
| <img src="statistics.svg" width="32" height="32" alt="Statistics"> | `U+E913` | [statistics.svg](statistics.svg) | 统计 / Statistics |
| <img src="asterisk.svg" width="32" height="32" alt="Asterisk"> | `U+E914` | [asterisk.svg](asterisk.svg) | 星号 / Asterisk |
| <img src="default.svg" width="32" height="32" alt="Package"> | `U+E915` | [default.svg](default.svg) | 默认图标(软件包) / Default (package) |
| <img src="user.svg" width="32" height="32" alt="User"> | `U+E971` | [user.svg](user.svg) | 用户 / User |
| <img src="help.svg" width="32" height="32" alt="Help"> | `U+F059` | [help.svg](help.svg) | 帮助 / Help |
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M956 668 672 512 956 356Q974 346 979.5 326.5Q985 307 975 290L936 222Q926 205 906.5 200Q887 195 869 205L592 373L599 49Q599 29 585 14.5Q571 0 551 0H473Q453 0 439 14.5Q425 29 425 49L432 373L155 205Q137 195 117.5 200Q98 205 88 222L49 290Q39 307 44.5 326.5Q50 346 68 356L352 512L68 668Q50 678 44.5 697.5Q39 717 49 734L88 802Q98 819 117.5 824Q137 829 155 819L432 651L425 975Q425 995 439 1009.5Q453 1024 473 1024H551Q571 1024 585 1009.5Q599 995 599 975L592 651L869 819Q887 829 906.5 824Q926 819 936 802L975 734Q985 717 979.5 697.5Q974 678 956 668Z"/></svg>

After

Width:  |  Height:  |  Size: 643 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 0 1025 1024" fill="currentColor"><path d="M934 43H88Q51 43 25 69.5Q-1 96 -1 132V756Q-1 792 25 818.5Q51 845 88 845H466V912H291Q274 912 260.5 922.5Q247 933 244 949Q240 971 254 986Q268 1001 288 1001H731Q748 1001 761.5 990Q775 979 778 963Q781 941 767.5 926.5Q754 912 733 912H555V845H934Q971 845 997 818.5Q1023 792 1023 756V132Q1023 96 997 69.5Q971 43 934 43ZM800 489H653L602 640Q599 650 588.5 651.5Q578 653 571 644L411 422L391 473Q389 480 383 483Q377 486 371 486H201Q185 486 171.5 475.5Q158 465 155 449Q151 427 165 412Q179 397 199 397H324L375 268Q378 258 389 256.5Q400 255 406 264L562 477L584 411Q586 404 592 399.5Q598 395 604 395H796Q813 395 826 405.5Q839 416 843 433Q848 458 835 473.5Q822 489 800 489Z"/></svg>

After

Width:  |  Height:  |  Size: 761 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M0 533 384 917 1024 277 853 107 384 576 171 363Z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" color="#8898aa" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>

After

Width:  |  Height:  |  Size: 205 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" color="#8898aa" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>

After

Width:  |  Height:  |  Size: 205 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M0 73 73 0 1024 951 951 1024 0 73ZM73 1024 0 951 951 0 1024 73 73 1024Z"/></svg>

After

Width:  |  Height:  |  Size: 174 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M1024 983V41Q1024 24 1012 12Q1000 0 983 0H41Q24 0 12 12Q0 24 0 41V983Q0 1000 12 1012Q24 1024 41 1024H983Q1000 1024 1012 1012Q1024 1000 1024 983ZM293 374H195Q190 374 187 371Q184 368 184 364V302Q184 298 187 295Q190 292 195 292H293Q306 252 340.5 226Q375 200 420 200Q464 200 499 226Q534 252 547 292H829Q834 292 837 295Q840 298 840 302V364Q840 368 837 371Q834 374 829 374H547Q534 414 499 440Q464 466 420 466Q375 466 340.5 440Q306 414 293 374ZM477 732Q477 732 477 732Q477 732 477 732H195Q190 732 187 729Q184 726 184 722V660Q184 656 187 653Q190 650 195 650H477Q477 650 477 650.5Q477 651 477 651Q490 610 525 584Q560 558 604 558Q649 558 683.5 584Q718 610 731 651Q731 651 731 650.5Q731 650 731 650H829Q834 650 837 653Q840 656 840 660V722Q840 726 837 729Q834 732 829 732H731Q731 732 731 732Q731 732 731 732Q718 772 683.5 798Q649 824 604 824Q560 824 525 798Q490 772 477 732ZM604 645Q585 645 571.5 658.5Q558 672 558 691Q558 710 571.5 723.5Q585 737 604 737Q623 737 636.5 723.5Q650 710 650 691Q650 672 636.5 658.5Q623 645 604 645ZM442 373 445 371Q445 371 445 371Q445 371 446 371L447 370Q447 370 447 370Q447 370 447 370Q449 369 450 368Q451 367 452 365Q453 365 453 364.5Q453 364 454 364Q455 363 455.5 362Q456 361 456 360L458 359Q458 359 458 358.5Q458 358 458 358Q459 357 459.5 355.5Q460 354 461 353Q461 353 461 352.5Q461 352 461 352Q462 352 462 351.5Q462 351 462 351Q462 351 462 350.5Q462 350 462 350Q463 349 463 348.5Q463 348 463 348Q463 347 463.5 346.5Q464 346 464 346V345V343Q464 343 464.5 342.5Q465 342 465 342Q465 341 465 340.5Q465 340 465 339Q465 339 465 339Q465 339 465 339Q465 338 465 337.5Q465 337 466 337Q466 336 466 336Q466 336 466 336Q466 335 466 334Q466 333 466 333Q466 332 466 331Q466 330 466 330Q466 330 466 329.5Q466 329 466 329Q466 328 465.5 327.5Q465 327 465 327Q465 326 465 326Q465 326 465 326Q465 325 465 324.5Q465 324 465 323Q465 323 464.5 322.5Q464 322 464 322V321V319Q464 319 463.5 318.5Q463 318 463 318Q463 317 463 316.5Q463 316 462 316Q462 315 462 315Q462 315 462 314Q462 314 462 313.5Q462 313 461 313Q461 313 461 312.5Q461 312 461 312Q460 311 459.5 309.5Q459 308 458 307Q458 307 458 307Q458 307 458 307L456 305Q456 304 455 303Q454 302 454 302Q453 301 453 301Q453 301 452 300Q451 299 450 298Q449 297 448 296H447Q447 296 447 296Q447 296 447 296L446 294Q445 294 445 294Q445 294 445 294L442 292Q442 292 442 292Q442 292 442 292Q437 289 431.5 287.5Q426 286 420 286Q414 286 408.5 287.5Q403 289 398 292Q398 292 398 292Q398 292 397 292L394 294Q394 294 394 294Q394 294 394 294L393 296Q392 296 392 296Q392 296 392 296Q391 297 390 298Q389 299 388 300H387Q387 301 386.5 301Q386 301 386 302Q385 302 384.5 303Q384 304 383 305L382 307Q382 307 382 307Q382 307 382 307Q381 308 380 309.5Q379 311 379 312Q379 312 379 312.5Q379 313 378 313Q378 313 378 313.5Q378 314 378 314Q378 315 377.5 315Q377 315 377 316Q377 316 377 316.5Q377 317 377 318Q376 318 376 318.5Q376 319 376 319V321L375 322Q375 322 375 322.5Q375 323 375 323Q375 324 375 324.5Q375 325 375 326Q375 326 374.5 326Q374 326 374 327Q374 327 374 327.5Q374 328 374 329Q374 329 374 329Q374 329 374 330Q374 330 374 331Q374 332 374 333Q374 333 374 334Q374 335 374 336Q374 336 374 336Q374 336 374 337Q374 337 374 337.5Q374 338 374 339Q374 339 374 339Q374 339 375 339Q375 340 375 340.5Q375 341 375 342Q375 342 375 342.5Q375 343 375 343L376 345V346Q376 346 376 346.5Q376 347 377 348Q377 348 377 348.5Q377 349 377 350Q377 350 377.5 350.5Q378 351 378 351Q378 351 378 351.5Q378 352 378 352Q378 352 378.5 352.5Q379 353 379 353Q380 354 380.5 355.5Q381 357 382 358Q382 358 382 358.5Q382 359 382 359L383 360Q384 361 384.5 362Q385 363 386 364Q386 364 386.5 364.5Q387 365 387 365H388Q389 366 390 367.5Q391 369 392 370Q392 370 392 370Q392 370 393 370L394 371Q394 371 394 371Q394 371 394 371L397 373Q398 373 398 373Q398 373 398 373Q403 376 408.5 377.5Q414 379 420 379Q426 379 431.5 377.5Q437 376 442 373Q442 373 442 373Q442 373 442 373Z"/></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="13" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/></svg>

After

Width:  |  Height:  |  Size: 281 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1025" fill="currentColor"><path d="M418 1016Q439 1029 454 1021Q469 1013 469 988V589Q469 563 454 539.5Q439 516 418 503L88 298Q66 289 51 297Q36 305 36 327V725Q36 751 51 774.5Q66 798 88 811L418 1016ZM139 172Q117 185 117 201Q117 217 139 229L464 435Q485 447 512.5 448Q540 449 561 440L886 275Q907 262 907 246.5Q907 231 886 218L543 13Q526 0 497 0Q468 0 446 13L139 172ZM561 988Q561 1013 576 1021Q591 1029 612 1016L937 851Q958 838 973 814.5Q988 791 988 765V366Q988 340 973 332.5Q958 325 937 337L612 503Q590 515 575.5 539Q561 563 561 588Z"/></svg>

After

Width:  |  Height:  |  Size: 596 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M125 252 157 893Q157 947 196.5 985.5Q236 1024 291 1024H734Q789 1024 828.5 985.5Q868 947 868 893L899 252H125ZM936 207Q936 186 936 181.5Q936 177 936 176Q936 123 908.5 95Q881 67 839 67H683Q683 39 660 19.5Q637 0 610 0H416Q388 0 364.5 19.5Q341 39 341 67H185Q139 67 113.5 98Q88 129 88 176Q88 177 88 181.5Q88 186 88 207H936ZM646 420Q646 405 657 394Q668 383 683 383Q698 383 709 394Q720 405 720 420V844Q720 859 709 870Q698 881 683 881Q668 881 657 870Q646 859 646 844ZM475 420Q475 405 486 394Q497 383 512 383Q527 383 538 394Q549 405 549 420V844Q549 859 538 870Q527 881 512 881Q497 881 486 870Q475 859 475 844ZM304 420Q304 405 315 394Q326 383 341 383Q356 383 367 394Q378 405 378 420V844Q378 859 367 870Q356 881 341 881Q326 881 315 870Q304 859 304 844Z"/></svg>

After

Width:  |  Height:  |  Size: 843 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1110 1024" fill="currentColor"><path d="M537 64 624 62 638 76 639 165 625 178 538 180 524 167 523 77ZM270 204 357 202 371 215 372 305 358 318 271 320 258 306 257 217ZM404 202 492 201 505 215 506 303 493 317 405 318 392 305 391 215ZM538 200 625 199 639 213V301L627 315L540 316L526 303L525 214ZM874 285Q918 299 976 393Q1060 391 1110 446V452Q1042 556 951 557Q752 880 432 884L358 885Q123 888 47 616L40 547L39 494L52 479L796 470Q830 470 836 449L835 374Q848 285 874 285ZM138 342 225 340 239 355V443L226 456L139 458L125 445L124 356ZM272 340 358 339 373 353 374 441 360 455 273 456 260 443 259 354ZM405 338 493 337 506 351 507 440 494 453 407 454 394 441 393 352ZM540 336 627 335 640 349 641 438 628 451 541 452 527 439 526 351ZM674 335 760 333 775 347 776 436 762 450 675 451 662 437 661 349Z"/></svg>

After

Width:  |  Height:  |  Size: 848 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12m-5-5 5 5 5-5M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/></svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M272 876Q268 877 263 877Q258 877 253 877Q233 877 213.5 869.5Q194 862 179 847Q161 828 153 803.5Q145 779 150 753Q196 502 219.5 444Q243 386 260 370L545 84L479 18H63Q37 18 18.5 36.5Q0 55 0 81V959Q0 986 18.5 1004.5Q37 1023 63 1023H698Q724 1023 742.5 1004.5Q761 986 761 959V661L656 766Q640 782 582 806Q524 830 272 876ZM1005 176 849 20Q830 1 803.5 1Q777 1 758 20L334 444Q325 453 311 506Q297 558 284 617.5Q271 677 262 724Q253 772 253 772Q253 772 301 763Q349 754 408 741Q467 728 519 715Q572 701 581 691L1005 268Q1024 249 1024 222Q1024 195 1005 176ZM442 662 394 678Q393 679 392 679Q391 679 390 679Q390 679 389.5 679Q389 679 389 679Q388 679 387 678.5Q386 678 385 678Q384 678 384 678Q384 678 383 677Q382 677 381 676.5Q380 676 379 675L351 646Q348 644 347 639.5Q346 635 347 631L363 584Q364 581 365 579Q366 577 367 575Q367 575 367.5 574.5Q368 574 368 573Q369 572 370 570Q371 568 373 567Q373 567 373 567Q373 567 373 567Q374 565 375.5 564Q377 563 378 563Q378 562 378.5 562Q379 562 379 562Q381 561 382 560.5Q383 560 385 560Q385 560 385.5 560.5Q386 561 387 561Q387 561 387.5 561Q388 561 388 561Q389 561 390 562Q391 563 392 563L462 634Q469 641 462 649.5Q455 658 442 662ZM888 233 845 276Q839 282 830.5 282Q822 282 815 276L749 210Q743 204 743 195Q743 186 749 180L792 137Q798 131 807 131Q816 131 822 137L888 203Q894 210 894 218.5Q894 227 888 233Z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,451 @@
{
"source": "argon.woff2",
"sourceIncluded": false,
"sourceSha256": "a2b317e0113b0e5eb3ffe4ec7559d784564f02e8624ba29bbc44d7afd74585a4",
"unitsPerEm": 1024,
"ascent": 960,
"descent": -64,
"glyphCount": 33,
"iconCount": 28,
"icons": [
{
"name": "chevron-down",
"glyph": "uniE20B",
"codepoints": [
"U+E20B"
],
"file": "chevron-down.svg",
"viewBox": [
0,
0,
24,
24
],
"sourceViewBox": [
0,
0,
1024,
1024
],
"designOverride": "Shared 24px outline chevron, 2px stroke with round caps and joins"
},
{
"name": "menu",
"glyph": "uniE20E",
"codepoints": [
"U+E20E"
],
"file": "menu.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "heart",
"glyph": "uniE291",
"codepoints": [
"U+E291"
],
"file": "heart.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "ring",
"glyph": "uniE603",
"codepoints": [
"U+E603"
],
"file": "ring.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "delete",
"glyph": "uniE900",
"codepoints": [
"U+E900"
],
"file": "delete.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "edit",
"glyph": "uniE901",
"codepoints": [
"U+E901"
],
"file": "edit.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "check",
"glyph": "uniE902",
"codepoints": [
"U+E902"
],
"file": "check.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "loading",
"glyph": "uniE903",
"codepoints": [
"U+E903"
],
"file": "loading.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "transfer",
"glyph": "uniE904",
"codepoints": [
"U+E904"
],
"file": "transfer.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "close",
"glyph": "uniE905",
"codepoints": [
"U+E905"
],
"file": "close.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "status",
"glyph": "uniE906",
"codepoints": [
"U+E906"
],
"file": "status.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "logout",
"glyph": "uniE907",
"codepoints": [
"U+E907"
],
"file": "logout.svg",
"viewBox": [
0,
0,
1030,
1024
]
},
{
"name": "network",
"glyph": "uniE908",
"codepoints": [
"U+E908"
],
"file": "network.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "services",
"glyph": "uniE909",
"codepoints": [
"U+E909"
],
"file": "services.svg",
"viewBox": [
0,
0,
1024,
1029
]
},
{
"name": "system",
"glyph": "uniE90A",
"codepoints": [
"U+E90A"
],
"file": "system.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "vpn",
"glyph": "uniE90B",
"codepoints": [
"U+E90B"
],
"file": "vpn.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "nas",
"glyph": "uniE90C",
"codepoints": [
"U+E90C"
],
"file": "nas.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "bandwidth_monitor",
"glyph": "uniE90D",
"codepoints": [
"U+E90D"
],
"file": "bandwidth_monitor.svg",
"viewBox": [
-1,
0,
1025,
1024
]
},
{
"name": "hello-world",
"glyph": "uniE90E",
"codepoints": [
"U+E90E"
],
"file": "hello-world.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "chevron-right",
"glyph": "uniE90F",
"codepoints": [
"U+E90F"
],
"file": "chevron-right.svg",
"viewBox": [
0,
0,
24,
24
],
"sourceViewBox": [
0,
0,
1024,
1024
],
"designOverride": "Shared 24px outline chevron, 2px stroke with round caps and joins"
},
{
"name": "password",
"glyph": "uniE910",
"codepoints": [
"U+E910"
],
"file": "password.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "docker",
"glyph": "uniE911",
"codepoints": [
"U+E911"
],
"file": "docker.svg",
"viewBox": [
0,
0,
1110,
1024
]
},
{
"name": "control",
"glyph": "uniE912",
"codepoints": [
"U+E912"
],
"file": "control.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "statistics",
"glyph": "uniE913",
"codepoints": [
"U+E913"
],
"file": "statistics.svg",
"viewBox": [
0,
0,
1025,
1024
]
},
{
"name": "asterisk",
"glyph": "uniE914",
"codepoints": [
"U+E914"
],
"file": "asterisk.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "default",
"glyph": "uniE915",
"codepoints": [
"U+E915"
],
"file": "default.svg",
"viewBox": [
0,
0,
1024,
1025
]
},
{
"name": "user",
"glyph": "uniE971",
"codepoints": [
"U+E971"
],
"file": "user.svg",
"viewBox": [
0,
0,
1024,
1024
]
},
{
"name": "help",
"glyph": "uniF059",
"codepoints": [
"U+F059"
],
"file": "help.svg",
"viewBox": [
0,
0,
1024,
1024
]
}
],
"skippedGlyphs": [
{
"glyph": ".notdef",
"codepoints": [],
"reason": "no visible outline"
},
{
"glyph": "uni0000",
"codepoints": [
"U+0000"
],
"reason": "no visible outline"
},
{
"glyph": "nonmarkingreturn",
"codepoints": [],
"reason": "no visible outline"
},
{
"glyph": "uni0001",
"codepoints": [
"U+0001"
],
"reason": "no visible outline"
},
{
"glyph": "space",
"codepoints": [
"U+0020"
],
"reason": "no visible outline"
}
]
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 931 450 875Q344 779 296 733Q248 687 189 619Q130 551 108 496Q86 441 86 383Q86 285 153 217Q220 149 320 149Q436 149 512 239Q588 149 704 149Q804 149 871 217Q938 285 938 383Q938 461 886 545Q834 629 773 691Q712 753 574 877Z"/></svg>

After

Width:  |  Height:  |  Size: 325 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M490 0Q501 0 512 0Q523 0 534 0Q537 1 540 1.5Q543 2 546 3Q564 5 581 6.5Q598 8 615 12Q694 28 761.5 65.5Q829 103 884 162Q913 193 936 226Q959 260 976 297Q993 334 1004 373Q1015 412 1019 455Q1020 464 1021.5 472.5Q1023 481 1024 490Q1024 501 1024 512Q1024 523 1024 534Q1023 537 1022.5 540Q1022 543 1021 546Q1019 564 1017.5 581Q1016 598 1012 615Q996 694 958.5 761.5Q921 829 862 884Q831 913 798 936Q764 959 727 976Q690 993 651 1004Q611 1015 569 1019Q560 1020 551.5 1021.5Q543 1023 534 1024Q523 1024 512 1024Q501 1024 490 1024Q487 1023 484 1022.5Q481 1022 478 1021Q460 1019 443 1017.5Q426 1016 409 1012Q330 996 262.5 958.5Q195 921 140 862Q111 831 88 798Q65 764 48 727Q31 690 20 651Q9 611 5 569Q4 560 2.5 551.5Q1 543 0 534Q0 523 0 512Q0 501 0 490Q1 487 1.5 484Q2 481 3 478Q5 460 6.5 443Q8 426 12 409Q28 330 65.5 262.5Q103 195 162 140Q193 111 226 88Q260 65 297 48Q334 31 373 20Q412 9 455 5Q464 4 472.5 2.5Q481 1 490 0ZM338 639Q343 656 347 671Q351 686 356 700Q358 709 364 714Q370 719 378 719Q385 719 391.5 714Q398 709 401 701Q402 697 403.5 692.5Q405 688 406 684Q415 657 423.5 630Q432 603 440 576Q444 564 437 555Q430 546 418 548Q411 550 406.5 554Q402 558 400 566Q395 583 390 600.5Q385 618 379 637Q374 620 369.5 604.5Q365 589 361 574Q358 565 352.5 559.5Q347 554 339 554Q331 554 324.5 559Q318 564 315 574Q314 579 312.5 584Q311 589 309 594Q306 606 303 617.5Q300 629 296 642Q290 624 285 608Q280 592 275 576Q272 566 265 562.5Q258 559 249 561Q241 564 236.5 572Q232 580 235 589Q237 596 239 603Q241 610 244 616Q251 639 258.5 661.5Q266 684 273 706Q276 715 281.5 719.5Q287 724 294 725Q302 725 308.5 720Q315 715 318 707Q319 704 319.5 702Q320 700 321 697Q325 683 329 669Q333 655 338 639ZM881 564Q881 543 881 522Q881 501 881 479Q881 470 875.5 464Q870 458 861 458Q852 458 845.5 464Q839 470 839 479Q839 487 839 495.5Q839 504 839 512Q839 517 837.5 519Q836 521 830 520Q797 515 773.5 534.5Q750 554 751 586Q751 598 751 609.5Q751 621 751 632Q751 651 763 666Q775 681 794 685Q813 689 831.5 685Q850 681 867 673Q874 670 877.5 664Q881 658 881 650Q881 629 881 607.5Q881 586 881 564ZM185 374Q185 366 185 357Q185 348 185 339Q185 329 179.5 323.5Q174 318 165 317Q156 317 149.5 323.5Q143 330 143 338Q143 385 143 431Q143 477 143 523Q143 530 146 534.5Q149 539 155 541Q166 545 175.5 538.5Q185 532 185 521Q185 499 185 476.5Q185 454 185 432Q184 424 187.5 420Q191 416 197 414Q213 408 222.5 414Q232 420 233 437Q233 439 233 440.5Q233 442 233 444Q233 462 233 479.5Q233 497 233 515Q233 525 238 530.5Q243 536 252 536Q261 536 267 530Q273 524 273 515Q273 492 273 469.5Q273 447 273 424Q272 406 261 391.5Q250 377 233 374Q222 372 210 372.5Q198 373 185 374ZM330 467Q336 466 341 465.5Q346 465 351 465Q363 464 374.5 463Q386 462 397 461Q405 460 410 455Q415 450 416 443Q417 433 417 423Q417 413 415 404Q410 382 394 371.5Q378 361 352 361Q325 362 306 381Q287 400 287 427Q287 439 287 450.5Q287 462 287 474Q287 493 299.5 508Q312 523 330 527Q349 531 366 527Q383 523 399 515Q407 511 409.5 503Q412 495 409 487Q405 480 397.5 478Q390 476 381 480Q375 482 369 484Q363 486 357 488Q345 491 338 486Q331 481 330 467ZM688 421Q688 421 687.5 421Q687 421 687 421Q687 415 687 409Q687 403 687 397Q686 382 679.5 370Q673 358 660 350Q630 332 595.5 346Q561 360 554 393Q551 407 551 422Q551 437 551 452Q551 467 558 479Q565 491 577 499Q606 518 641 504.5Q676 491 685 458Q687 449 687 439.5Q687 430 688 421ZM580 620Q579 620 578.5 620Q578 620 578 620Q578 612 578.5 604Q579 596 578 588Q573 565 556 553Q539 541 514 541Q484 542 465 561Q446 580 445 610Q445 621 445 632Q445 643 445 654Q445 664 449 673.5Q453 683 460 691Q474 705 491 708Q508 711 527 706Q546 702 558.5 690Q571 678 576 658Q579 649 579 639.5Q579 630 580 620ZM693 581Q693 604 693 626.5Q693 649 693 672Q693 682 698 687.5Q703 693 712 693Q723 693 729 687Q735 681 735 671Q735 626 735 580.5Q735 535 735 489Q735 481 731 476Q727 471 720 469Q710 467 701.5 473.5Q693 480 693 491Q693 513 693 535.5Q693 558 693 581ZM433 410Q433 432 433 454.5Q433 477 433 500Q433 509 438 514.5Q443 520 451 521Q460 521 466.5 515.5Q473 510 473 501Q473 454 473 407.5Q473 361 473 314Q473 307 469.5 303Q466 299 460 297Q450 294 441.5 300Q433 306 433 317Q433 340 433 363.5Q433 387 433 410ZM495 404Q495 427 495 449.5Q495 472 495 495Q495 505 500 510.5Q505 516 514 517Q522 517 528.5 510.5Q535 504 535 495Q535 449 535 403Q535 357 535 311Q535 307 533 303.5Q531 300 529 297Q525 292 519.5 291.5Q514 291 508 293Q502 296 498.5 300.5Q495 305 495 312Q495 335 495 358Q495 381 495 404ZM595 630Q595 643 595 656Q595 669 596 682Q596 686 598.5 691Q601 696 604 697Q608 699 613.5 699Q619 699 624 698Q629 696 632 691Q635 686 635 680Q635 659 635 638.5Q635 618 635 597Q635 578 638 575Q641 572 661 573Q670 573 676.5 568Q683 563 684 554Q685 546 680.5 540Q676 534 667 532Q661 531 655 531Q649 531 643 532Q622 533 608.5 548Q595 563 595 583Q595 595 595 606.5Q595 618 595 630ZM839 604Q839 596 839 588Q839 580 839 572Q839 570 838.5 567.5Q838 565 837 564Q830 559 822.5 558.5Q815 558 808 560Q802 562 797.5 566.5Q793 57Line truncated

After

Width:  |  Height:  |  Size: 6.5 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 795V686Q512 678 507 673Q502 667 494 667H384Q376 667 371 673Q366 678 366 686V795Q366 803 371 809Q376 814 384 814H494Q502 814 507 809Q512 803 512 795ZM658 411Q658 361 627 318Q595 275 547 252Q500 229 450 229Q311 229 238 350Q230 364 243 374L318 431Q322 435 329 435Q338 435 343 428Q374 389 393 375Q412 362 442 362Q469 362 491 377Q512 391 512 410Q512 432 501 445Q489 458 462 471Q426 487 396 520Q366 554 366 592V613Q366 621 371 626Q376 631 384 631H494Q502 631 507 626Q512 621 512 613Q512 602 524 584Q537 567 555 556Q574 546 583 540Q593 534 610 520Q626 506 635 492Q644 479 651 458Q658 437 658 411ZM878 521Q878 641 819 741Q760 842 659 901Q558 960 439 960Q319 960 219 901Q118 842 59 741Q0 641 0 521Q0 402 59 301Q118 200 219 141Q319 82 439 82Q558 82 659 141Q760 200 819 301Q878 402 878 521Z"/></svg>

After

Width:  |  Height:  |  Size: 887 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M871 761Q871 764 872.5 766Q874 768 877 768Q879 768 881 766Q883 764 883 761Q883 761 883 761Q883 761 883 761Q883 759 881 757Q879 755 877 755Q877 755 877 755Q877 755 877 755Q877 755 877 755Q877 755 877 755Q874 755 872.5 757Q871 759 871 761Q871 761 871 761Q871 761 871 761ZM733 904Q733 909 736.5 912.5Q740 916 745 916Q750 916 754 912.5Q758 909 758 904Q758 898 754 894.5Q750 891 745 891Q740 891 736.5 894.5Q733 898 733 904ZM547 977Q547 985 552.5 990.5Q558 996 565 996Q573 996 578.5 990.5Q584 985 584 977Q584 969 578.5 963.5Q573 958 565 958Q558 958 552.5 963.5Q547 969 547 977ZM347 967Q347 977 354 984.5Q361 992 371 992Q382 992 389 984.5Q396 977 396 967Q396 957 389 949.5Q382 942 371 942Q361 942 354 949.5Q347 957 347 967ZM169 876Q169 889 178 898Q187 907 200 907Q213 907 222 898Q231 889 231 876Q231 863 222 854Q213 845 200 845Q187 845 178 854Q169 863 169 876ZM46 721Q46 737 57 747.5Q68 758 83 758Q99 758 110 747.5Q121 737 121 721Q121 706 110 695Q99 684 83 684Q68 684 57 695Q46 706 46 721ZM0 531Q0 549 12.5 561.5Q25 574 44 574Q62 574 74.5 561.5Q87 549 87 531Q87 513 74.5 500Q62 487 44 487Q25 487 12.5 500Q0 513 0 531ZM38 342Q38 363 52.5 377.5Q67 392 88 392Q108 392 123 377.5Q138 363 138 342Q138 321 123 306.5Q108 292 88 292Q67 292 52.5 306.5Q38 321 38 342ZM152 190Q152 213 168.5 229.5Q185 246 208 246Q231 246 247.5 229.5Q264 213 264 190Q264 166 247.5 150Q231 134 208 134Q185 134 168.5 150Q152 166 152 190ZM319 102Q319 128 337.5 146.5Q356 165 381 165Q407 165 425.5 146.5Q444 128 444 102Q444 77 425.5 58.5Q407 40 381 40Q356 40 337.5 58.5Q319 77 319 102ZM507 97Q507 125 527 145Q547 165 575 165Q604 165 624 145Q644 125 644 97Q644 69 624 48.5Q604 28 575 28Q547 28 527 48.5Q507 69 507 97ZM679 174Q679 205 701 227Q723 249 754 249Q784 249 806 227Q828 205 828 174Q828 143 806 121.5Q784 100 754 100Q723 100 701 121.5Q679 143 679 174ZM801 320Q801 353 825 377Q849 401 882 401Q916 401 939.5 377Q963 353 963 320Q963 286 939.5 262.5Q916 239 882 239Q849 239 825 262.5Q801 286 801 320ZM850 508Q850 544 875.5 569.5Q901 595 937 595Q973 595 998.5 569.5Q1024 544 1024 508Q1024 472 998.5 446.5Q973 421 937 421Q901 421 875.5 446.5Q850 472 850 508Z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1030 1024" fill="currentColor"><path d="M765 777V613H411V411H765V247L1030 512L765 777ZM563 6Q604 6 634 36Q664 66 664 107V310H563V107H107V917H563V714H664V917Q664 958 634 988Q604 1018 563 1018H107Q66 1018 36 988Q6 958 6 917V107Q6 66 36 36Q66 6 107 6H563Z"/></svg>

After

Width:  |  Height:  |  Size: 315 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M128 277H896V363H128ZM128 575V491H896V575ZM128 789V703H896V789Z"/></svg>

After

Width:  |  Height:  |  Size: 166 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8m-4-4v4"/></svg>

After

Width:  |  Height:  |  Size: 240 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.9 13A9 9 0 0 1 11 3.1 9 9 0 1 0 20.9 13Z"/></svg>

After

Width:  |  Height:  |  Size: 221 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 439Q580 439 643 433Q706 427 765 414Q824 402 870.5 384Q917 366 951 342V439Q951 459 936 477Q921 495 892 512Q863 529 823 542.5Q783 556 732 565Q681 575 626 580Q571 585 512 585Q453 585 398 580Q343 575 292 565Q241 556 201 542.5Q161 529 132 512Q103 495 88 477Q73 459 73 439V342Q107 366 153.5 384Q200 402 259 414Q318 427 381 433Q444 439 512 439ZM512 878Q580 878 643 871.5Q706 865 765 853Q824 841 870.5 823Q917 805 951 781V878Q951 897 936 915.5Q921 934 892 951Q863 968 823 981Q783 994 732 1004Q681 1014 626 1019Q571 1024 512 1024Q453 1024 398 1019Q343 1014 292 1004Q241 994 201 981Q161 968 132 951Q103 934 88 915.5Q73 897 73 878V781Q107 805 153.5 823Q200 841 259 853Q318 865 381 871.5Q444 878 512 878ZM512 658Q580 658 643 652Q706 646 765 634Q824 621 870.5 603Q917 585 951 561V658Q951 678 936 696.5Q921 715 892 731Q863 748 823 761.5Q783 775 732 785Q681 795 626 800Q571 805 512 805Q453 805 398 800Q343 795 292 785Q241 775 201 761.5Q161 748 132 731Q103 715 88 696.5Q73 678 73 658V561Q107 585 153.5 603Q200 621 259 634Q318 646 381 652Q444 658 512 658ZM512 0Q571 0 626 5Q681 10 732 20Q783 30 823 43Q863 56 892 73Q921 90 936 108.5Q951 127 951 146V219Q951 239 936 257.5Q921 276 892 293Q863 309 823 322.5Q783 336 732 346Q681 356 626 361Q571 366 512 366Q453 366 398 361Q343 356 292 346Q241 336 201 322.5Q161 309 132 293Q103 276 88 257.5Q73 239 73 219V146Q73 127 88 108.5Q103 90 132 73Q161 56 201 43Q241 30 292 20Q343 10 398 5Q453 0 512 0Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M384 768H640V1024H384ZM384 0H640V256H384ZM768 768H1024V1024H768ZM0 768H256V1024H0ZM480 320H544V480H480ZM480 512H544V704H480ZM928 704H864V544H160V704H96V480H928Z"/></svg>

After

Width:  |  Height:  |  Size: 263 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M827 453H315V360Q315 282 367.5 222Q420 162 498 158Q571 153 626.5 193.5Q682 234 701 299Q703 305 708 310Q713 315 721 315H801Q810 315 816.5 307.5Q823 300 821 291Q809 235 780 188Q750 140 707 106Q664 72 610 54Q556 36 496 39Q433 42 379 69Q325 96 285 139Q245 182 222 238Q198 294 197 356V453Q164 453 141 476Q118 499 118 532V906Q118 938 141 961.5Q164 985 197 985H827Q860 985 883 961.5Q906 938 906 906V532Q906 499 883 476Q860 453 827 453ZM603 841Q606 850 599.5 858Q593 866 583 866H439Q429 866 422.5 858.5Q416 851 419 841L455 723Q433 708 421.5 683Q410 658 416 628Q421 600 442 579.5Q463 559 492 553Q540 544 575 573Q610 602 610 646Q610 670 599 690.5Q588 711 569 723L603 841Z"/></svg>

After

Width:  |  Height:  |  Size: 764 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 0Q407 0 315 39Q223 78 154 146Q85 214 44 305Q3 396 0 500Q2 409 36 330Q69 250 125.5 191Q182 132 257 98Q331 64 416 64Q502 64 578 99Q654 134 710.5 195Q767 256 799 338Q832 419 832 512Q832 552 860 580Q888 608 928 608Q968 608 996 580Q1024 552 1024 512Q1024 406 984 313Q944 219 874.5 149.5Q805 80 711 40Q618 0 512 0ZM512 1024Q617 1024 709 985Q801 946 870 878Q939 810 980 719Q1021 628 1024 524Q1022 615 988 694Q955 774 898.5 833Q842 892 767 926Q693 960 608 960Q522 960 446 925Q370 890 313.5 829Q257 768 225 686Q192 605 192 512Q192 472 164 444Q136 416 96 416Q56 416 28 444Q0 472 0 512Q0 618 40 711Q80 805 149.5 874.5Q219 944 313 984Q406 1024 512 1024Z"/></svg>

After

Width:  |  Height:  |  Size: 749 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1029" fill="currentColor"><path d="M549 752Q551 745 551 736.5Q551 728 551 720Q551 711 551 703.5Q551 696 549 687L613 641Q619 638 620.5 632Q622 626 618 620L556 514Q552 509 547.5 507.5Q543 506 537 507L466 542Q454 532 440 524.5Q426 517 411 510L404 431Q404 426 399.5 421.5Q395 417 390 417H268Q262 417 258 421.5Q254 426 254 431L244 512Q229 517 216 525.5Q203 534 189 544L118 512Q112 510 106.5 512Q101 514 99 519L37 625Q33 630 34.5 636Q36 642 42 646L106 692Q104 699 104 707.5Q104 716 104 724Q104 733 104 740.5Q104 748 106 756L42 803Q36 806 34.5 812Q33 818 37 823L99 929Q103 935 107.5 936.5Q112 938 118 936L189 904Q201 914 215 921.5Q229 929 244 936L251 1015Q251 1020 255.5 1024.5Q260 1029 265 1029H387Q393 1029 397 1024.5Q401 1020 401 1015L408 936Q424 931 437 922.5Q450 914 464 904L535 936Q540 938 546 936.5Q552 935 554 929L616 823Q619 818 617.5 812Q616 806 611 803L549 752ZM327 835Q279 835 245.5 801.5Q212 768 212 720Q212 671 245.5 637.5Q279 604 327 604Q376 604 409.5 637.5Q443 671 443 720Q443 768 409.5 801.5Q376 835 327 835ZM925 311Q927 303 927 295.5Q927 288 927 281Q927 274 927 267Q927 260 925 251L982 210Q988 206 988.5 201.5Q989 197 987 191L929 92Q926 87 921 85.5Q916 84 911 88L844 118Q832 109 819.5 102Q807 95 793 88L786 16Q788 11 785 8Q782 5 777 5H664Q659 5 654.5 8Q650 11 650 16L643 88Q630 93 617.5 100Q605 107 593 118L526 88Q521 86 516 87.5Q511 89 507 92L450 191Q446 197 447.5 201.5Q449 206 454 210L512 251V281Q512 288 512.5 295.5Q513 303 514 311L457 353Q451 356 450.5 361Q450 366 452 371L510 470Q513 476 518 477.5Q523 479 528 475L595 445Q607 454 619.5 461Q632 468 646 475L653 547Q653 552 657 555Q661 558 667 558H780Q785 558 789 555Q793 552 793 547L800 475Q814 470 826.5 462.5Q839 455 851 445L918 475Q923 477 928 475.5Q933 474 936 470L994 371Q997 366 996 361Q995 356 989 353L925 311ZM720 397Q671 397 637.5 363.5Q604 330 604 281Q604 233 637.5 199.5Q671 166 720 166Q768 166 801.5 199.5Q835 233 835 281Q835 330 801.5 363.5Q768 397 720 397Z"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1025 1024" fill="currentColor"><path d="M243 933V326Q243 308 230.5 295.5Q218 283 202 283H79Q62 283 50 296Q38 309 38 326V933H243ZM503 933V65Q503 47 491 33.5Q479 20 462 20H340Q323 20 311 33.5Q299 47 299 65V933H503ZM764 933V549Q764 531 752 518Q740 505 723 505H600Q583 505 571 518Q559 531 559 549V933H764ZM1025 933V326Q1025 308 1012.5 295.5Q1000 283 984 283H861Q844 283 832 296Q820 309 820 326V933H1025Z"/></svg>

After

Width:  |  Height:  |  Size: 462 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M568 0H1024V341H568ZM568 1024V456H1024V1024ZM0 1024V683H456V1024ZM0 568V0H456V568Z"/></svg>

After

Width:  |  Height:  |  Size: 185 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M4.93 4.93l1.42 1.42m11.3 11.3 1.42 1.42m0-14.14-1.42 1.42m-11.3 11.3-1.42 1.42"/></svg>

After

Width:  |  Height:  |  Size: 315 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 769Q459 769 412 749Q366 729 331.5 694.5Q297 660 277 614Q257 567 257 514Q257 461 277 414Q297 367 331.5 332.5Q366 298 412 279Q459 259 512 259Q565 259 612 279Q658 298 692.5 332.5Q727 367 747 414Q767 461 767 514Q767 567 747 614Q727 660 692.5 694.5Q658 729 612 749Q565 769 512 769ZM1022 598V430Q998 425 970.5 420Q943 415 913 412Q905 382 894 354.5Q883 327 869 302Q888 278 904.5 255.5Q921 233 935 211L814 91Q793 105 770.5 121Q748 137 723 153Q699 139 671.5 127Q644 115 614 109Q611 79 606 52Q601 25 596 0H428Q423 25 418 52Q413 79 410 109Q380 118 352.5 128.5Q325 139 301 153Q276 139 253.5 123.5Q231 108 210 95L93 211Q107 233 122.5 255.5Q138 278 155 302Q141 327 129 354.5Q117 382 111 412Q81 415 53.5 420Q26 425 2 430V598Q26 603 53.5 608Q81 613 111 616Q119 646 130 673.5Q141 701 155 725Q136 750 120 772Q104 794 93 816L213 937Q235 923 257.5 906.5Q280 890 304 871Q329 885 356.5 897Q384 909 414 915Q416 945 421 972Q426 999 432 1024H599Q605 999 610 972Q615 945 618 915Q648 906 675 895.5Q702 885 727 871Q752 890 774 906.5Q796 923 818 937L938 816Q925 794 908.5 772Q892 750 873 725Q886 701 898.5 673.5Q911 646 916 616Q944 613 971 608Q998 603 1022 598Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Loaded 100 of 254 files, more files were not shown because too many files have changed in this diff. Show more