mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 02:11:19 +08:00
🦄 Sync 2026-07-26 23:49:39
This commit is contained in:
parent
2450ea7cca
commit
bf03fd23d8
@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.7.24
|
||||
PKG_RELEASE:=193
|
||||
PKG_RELEASE:=194
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@ -259,18 +259,48 @@ end
|
||||
|
||||
function get_redir_log()
|
||||
local name = http.formvalue("name")
|
||||
local proto = http.formvalue("proto")
|
||||
local proto = http.formvalue("proto"):upper()
|
||||
local path = "/tmp/etc/passwall/acl/" .. name
|
||||
proto = proto:upper()
|
||||
if proto == "UDP" and (uci:get(appname, "@global[0]", "udp_node") or "nil") == "tcp" and not fs.access(path .. "/" .. proto .. ".log") then
|
||||
proto = "TCP"
|
||||
|
||||
local function alert(msg)
|
||||
http.write(string.format("<script>alert('%s');window.close();</script>", i18n.translate(msg)))
|
||||
end
|
||||
|
||||
if name == "default" then
|
||||
if proto == "UDP" and (uci:get(appname, "@global[0]", "udp_node") or "nil") == "tcp" and not fs.access(path .. "/" .. proto .. ".log") then
|
||||
proto = "TCP"
|
||||
end
|
||||
else
|
||||
local global_tcp = uci:get(appname, "@global[0]", "tcp_node") or "nil"
|
||||
local global_udp = uci:get(appname, "@global[0]", "udp_node") or "nil"
|
||||
local acl_tcp = uci:get(appname, name, "tcp_node") or "nil"
|
||||
local acl_udp = uci:get(appname, name, "udp_node") or "nil"
|
||||
local global_enabled = uci:get(appname, "@global[0]", "enabled") == "1"
|
||||
if proto == "TCP" and acl_tcp == global_tcp and global_enabled then
|
||||
path = "/tmp/etc/passwall/acl/default"
|
||||
if uci:get(appname, "@global[0]", "log_tcp") ~= "1" then
|
||||
alert("The access control node is the same as the global node. Please enable global logging.")
|
||||
return
|
||||
end
|
||||
end
|
||||
if proto == "UDP" and acl_udp == global_udp and global_enabled then
|
||||
path = "/tmp/etc/passwall/acl/default"
|
||||
if uci:get(appname, "@global[0]", "log_udp") ~= "1" then
|
||||
alert("The access control node is the same as the global node. Please enable global logging.")
|
||||
return
|
||||
end
|
||||
end
|
||||
if proto == "UDP" and acl_udp == "tcp" and not fs.access(path .. "/" .. proto .. ".log") then
|
||||
proto = "TCP"
|
||||
end
|
||||
end
|
||||
|
||||
if fs.access(path .. "/" .. proto .. ".log") then
|
||||
local content = luci.sys.exec("tail -n 19999 ".. path .. "/" .. proto .. ".log")
|
||||
content = content:gsub("\n", "<br />")
|
||||
http.write(content)
|
||||
else
|
||||
http.write(string.format("<script>alert('%s');window.close();</script>", i18n.translate("Not enabled log")))
|
||||
alert("Not enabled log")
|
||||
end
|
||||
end
|
||||
|
||||
@ -289,6 +319,18 @@ end
|
||||
function get_chinadns_log()
|
||||
local flag = http.formvalue("flag")
|
||||
local path = "/tmp/etc/passwall/acl/" .. flag .. "/chinadns_ng.log"
|
||||
if flag ~= "default" then
|
||||
local global_tcp = uci:get(appname, "@global[0]", "tcp_node") or "nil"
|
||||
local acl_tcp = uci:get(appname, flag, "tcp_node") or "nil"
|
||||
if acl_tcp == global_tcp and uci:get(appname, "@global[0]", "enabled") == "1" then
|
||||
path = "/tmp/etc/passwall/acl/default/chinadns_ng.log"
|
||||
if uci:get(appname, "@global[0]", "log_chinadns_ng") ~= "1" then
|
||||
http.write(string.format("<script>alert('%s');window.close();</script>", i18n.translate("The access control node is the same as the global node. Please enable global logging.")))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if fs.access(path) then
|
||||
local content = luci.sys.exec("tail -n 5000 ".. path)
|
||||
content = content:gsub("\n", "<br />")
|
||||
|
||||
@ -7,7 +7,8 @@ m = Map(appname)
|
||||
m.redirect = api.url("acl")
|
||||
api.set_apply_on_parse(m)
|
||||
|
||||
if not arg[1] or not m:get(arg[1]) then
|
||||
local cfgid = arg[1]
|
||||
if not cfgid or not m:get(cfgid) then
|
||||
luci.http.redirect(m.redirect)
|
||||
end
|
||||
|
||||
@ -69,7 +70,7 @@ local dynamicList_write = function(self, section, value)
|
||||
end
|
||||
|
||||
-- [[ ACLs Settings ]]--
|
||||
s = m:section(NamedSection, arg[1], translate("ACLs"), translate("ACLs"))
|
||||
s = m:section(NamedSection, cfgid, translate("ACLs"), translate("ACLs"))
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
@ -80,9 +81,22 @@ o.rmempty = false
|
||||
|
||||
---- Remarks
|
||||
o = s:option(Value, "remarks", translate("Remarks"))
|
||||
o.default = arg[1]
|
||||
o.default = cfgid
|
||||
o.rmempty = false
|
||||
|
||||
---- Log
|
||||
o = s:option(Flag, "log", translate("Log"))
|
||||
o.default = 0
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(ListValue, "loglevel", "Sing-Box/Xray " .. translate("Log Level"))
|
||||
o.default = "warning"
|
||||
o:value("debug")
|
||||
o:value("info")
|
||||
o:value("warning")
|
||||
o:value("error")
|
||||
o:depends("log", "1")
|
||||
|
||||
o = s:option(Value, "interface", translate("Source Interface"))
|
||||
o:value("", translate("All"))
|
||||
local iface = api.get_network_devices()
|
||||
@ -238,8 +252,10 @@ o.template = appname .. "/cbi/nodes_listvalue"
|
||||
o.group = {"",""}
|
||||
o.remove = function(self, section)
|
||||
local v = s.fields["shunt_udp_node"]:formvalue(section)
|
||||
if not v then
|
||||
if not v or v == "close" then
|
||||
return m:del(section, self.option)
|
||||
else
|
||||
return m:set(section, self.option, "tcp")
|
||||
end
|
||||
end
|
||||
|
||||
@ -373,9 +389,6 @@ o:value("dnsmasq", "Dnsmasq")
|
||||
o:value("chinadns-ng", translate("ChinaDNS-NG (recommended)"))
|
||||
o:depends({ _tcp_node_bool = "1" })
|
||||
|
||||
o = s:option(DummyValue, "view_chinadns_log", " ")
|
||||
o.template = appname .. "/acl/view_chinadns_log"
|
||||
|
||||
o = s:option(Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature."))
|
||||
o.default = "0"
|
||||
o:depends({ _tcp_node_bool = "1" })
|
||||
@ -603,4 +616,9 @@ for k, v in pairs(nodes_table) do
|
||||
end
|
||||
end
|
||||
|
||||
local footer = Template(appname .. "/acl/config_footer")
|
||||
footer.api = api
|
||||
footer.cfgid = cfgid
|
||||
m:append(footer)
|
||||
|
||||
return api.return_map(m)
|
||||
|
||||
@ -124,8 +124,10 @@ o.group = {"",""}
|
||||
o:depends("_node_sel_other", "1")
|
||||
o.remove = function(self, section)
|
||||
local v = s.fields["shunt_udp_node"]:formvalue(section)
|
||||
if not v then
|
||||
if not v or v == "close" then
|
||||
return m:del(section, self.option)
|
||||
else
|
||||
return m:set(section, self.option, "tcp")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
120
luci-app-passwall/luasrc/view/passwall/acl/config_footer.htm
Normal file
120
luci-app-passwall/luasrc/view/passwall/acl/config_footer.htm
Normal file
@ -0,0 +1,120 @@
|
||||
<%
|
||||
local api = self.api
|
||||
local cfgid = self.cfgid
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
function to_edit_node(btn) {
|
||||
if (!btn) return;
|
||||
const idReg = /^cbid\..*\..*node$/;
|
||||
let hidden_select = null;
|
||||
const selects = document.querySelectorAll('select[id^="cbid."][id*="."][id$="node"]');
|
||||
for (const sel of selects) {
|
||||
if ( idReg.test(sel.id) && getComputedStyle(sel).display === "none" && (sel.compareDocumentPosition(btn) & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
||||
hidden_select = sel;
|
||||
}
|
||||
}
|
||||
if (!hidden_select) return;
|
||||
let node_select_value = hidden_select?.options[0]?.value;
|
||||
if (!node_select_value || node_select_value.indexOf("tcp") === 0) {
|
||||
return;
|
||||
}
|
||||
let to_url = '<%=api.url("node_config")%>/' + node_select_value;
|
||||
if (node_select_value.indexOf("Socks_") === 0) {
|
||||
to_url = '<%=api.url("socks_config")%>/' + node_select_value.substring("Socks_".length);
|
||||
}
|
||||
location.href = to_url;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
function go() {
|
||||
const node = document.querySelector(".cbi-section-node");
|
||||
if (!node) return false;
|
||||
let changed = false;
|
||||
const all_node = node.querySelectorAll("[id]");
|
||||
const reg1 = /^cbid\..*\.(tcp_node|udp_node)\.main$/;
|
||||
|
||||
for (const el of all_node) {
|
||||
if (!reg1.test(el.id)) continue;
|
||||
|
||||
if (el.querySelector(".node-actions")) continue;
|
||||
|
||||
const cbid = el.id.replace(/\.main$/, "");
|
||||
const hidden_select = document.getElementById(cbid);
|
||||
const node_select_value = hidden_select?.options[0]?.value;
|
||||
if (!node_select_value || node_select_value.indexOf("tcp") === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let html = '<a href="#" onclick="return to_edit_node(this);"><%:Edit%></a>';
|
||||
|
||||
const m = cbid.match(/\.(tcp|udp)_node$/);
|
||||
if (m && (m[1] === "tcp" || m[1] === "udp")) {
|
||||
html += '<a href="#" onclick="window.open(\'' + '<%=api.url("get_redir_log") .. "?name=" .. cfgid%>&proto=' + m[1] + '\', \'_blank\')"><%:Log%></a>';
|
||||
}
|
||||
|
||||
html = '<div class="node-actions" style="display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; margin-left:4px;">' + html + '</div>'
|
||||
|
||||
el.insertAdjacentHTML("beforeend", html);
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
const target = document.querySelector('form') || document.body;
|
||||
const observer = new MutationObserver(() => go() ? observer.disconnect() : 0);
|
||||
observer.observe(target, { childList: true, subtree: true });
|
||||
const timer = setInterval(() => go() ? (clearInterval(timer), observer.disconnect()) : 0, 300);
|
||||
setTimeout(() => { clearInterval(timer); observer.disconnect(); }, 5000);
|
||||
});
|
||||
|
||||
(function () {
|
||||
const startTime = Date.now();
|
||||
const TIMEOUT = 3000;
|
||||
|
||||
const waitForDnsSelect = () => {
|
||||
const dns_select = document.querySelector("select[id*='dns_shunt']");
|
||||
if (dns_select) {
|
||||
initDnsSelect(dns_select);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - startTime >= TIMEOUT) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(waitForDnsSelect);
|
||||
};
|
||||
waitForDnsSelect();
|
||||
|
||||
const initDnsSelect = (dns_select) => {
|
||||
if (dns_select.value === "chinadns-ng") {
|
||||
addLogLink(dns_select);
|
||||
}
|
||||
if (dns_select._dnsLogBinded) return;
|
||||
dns_select._dnsLogBinded = true;
|
||||
|
||||
dns_select.addEventListener("change", () => {
|
||||
const existingLogLink = dns_select.parentElement.querySelector("a.dns-log-link");
|
||||
if (existingLogLink) {
|
||||
existingLogLink.remove();
|
||||
}
|
||||
if (dns_select.value === "chinadns-ng") {
|
||||
addLogLink(dns_select);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addLogLink = (select) => {
|
||||
if (select.parentElement.querySelector("a.dns-log-link")) {
|
||||
return;
|
||||
}
|
||||
const logLink = document.createElement("a");
|
||||
logLink.innerHTML = "<%:Log%>";
|
||||
logLink.href = "#";
|
||||
logLink.className = "dns-log-link";
|
||||
logLink.style.marginLeft = "10px";
|
||||
logLink.setAttribute("onclick", "window.open('" + '<%=api.url("get_chinadns_log") .. "?flag=" .. cfgid%>' + "', '_blank')");
|
||||
select.insertAdjacentElement("afterend", logLink);
|
||||
};
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
@ -1,55 +0,0 @@
|
||||
<%
|
||||
local api = require "luci.passwall.api"
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function () {
|
||||
const startTime = Date.now();
|
||||
const TIMEOUT = 3000;
|
||||
|
||||
const waitForDnsSelect = () => {
|
||||
const dns_select = document.querySelector("select[id*='dns_shunt']");
|
||||
if (dns_select) {
|
||||
initDnsSelect(dns_select);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - startTime >= TIMEOUT) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(waitForDnsSelect);
|
||||
};
|
||||
waitForDnsSelect();
|
||||
|
||||
const initDnsSelect = (dns_select) => {
|
||||
if (dns_select.value === "chinadns-ng") {
|
||||
addLogLink(dns_select);
|
||||
}
|
||||
if (dns_select._dnsLogBinded) return;
|
||||
dns_select._dnsLogBinded = true;
|
||||
|
||||
dns_select.addEventListener("change", () => {
|
||||
const existingLogLink = dns_select.parentElement.querySelector("a.dns-log-link");
|
||||
if (existingLogLink) {
|
||||
existingLogLink.remove();
|
||||
}
|
||||
if (dns_select.value === "chinadns-ng") {
|
||||
addLogLink(dns_select);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addLogLink = (select) => {
|
||||
if (select.parentElement.querySelector("a.dns-log-link")) {
|
||||
return;
|
||||
}
|
||||
const logLink = document.createElement("a");
|
||||
logLink.innerHTML = "<%:Log%>";
|
||||
logLink.href = "#";
|
||||
logLink.className = "dns-log-link";
|
||||
logLink.style.marginLeft = "10px";
|
||||
logLink.setAttribute("onclick", "window.open('" + '<%=api.url("get_chinadns_log") .. "?flag=" .. section%>' + "', '_blank')");
|
||||
select.insertAdjacentElement("afterend", logLink);
|
||||
};
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
@ -1712,6 +1712,9 @@ msgstr "要执行的 Shell 命令,用 %s 代替日志内容。"
|
||||
msgid "Not enabled log"
|
||||
msgstr "未启用日志"
|
||||
|
||||
msgid "The access control node is the same as the global node. Please enable global logging."
|
||||
msgstr "访问控制节点与全局节点相同,请开启全局日志。"
|
||||
|
||||
msgid "It is recommended to disable logging during regular use to reduce system overhead."
|
||||
msgstr "正常使用时建议关闭日志,以减少系统开销。"
|
||||
|
||||
|
||||
@ -303,13 +303,13 @@ run_dns2socks() {
|
||||
}
|
||||
|
||||
run_chinadns_ng() {
|
||||
local _flag _listen_port _dns_local _dns_trust _no_ipv6_trust _use_direct_list _use_proxy_list _gfwlist _chnlist _default_mode _default_tag _no_logic_log _tcp_node _filter_https
|
||||
local _flag _listen_port _dns_local _dns_trust _no_ipv6_trust _use_direct_list _use_proxy_list _gfwlist _chnlist _default_mode _default_tag _no_logic_log _tcp_node _filter_https _log
|
||||
local _extra_param=""
|
||||
eval_set_val "$@"
|
||||
|
||||
local _CONF_FILE=$TMP_ACL_PATH/$_flag/chinadns_ng.conf
|
||||
local _LOG_FILE="/dev/null"
|
||||
[ "$(config_t_get global log_chinadns_ng "0")" = "1" ] && _LOG_FILE=$TMP_ACL_PATH/$_flag/chinadns_ng.log
|
||||
[ "${_log}" = "1" ] && _LOG_FILE=$TMP_ACL_PATH/$_flag/chinadns_ng.log
|
||||
|
||||
_extra_param="-FLAG ${_flag} -TCP_NODE ${_tcp_node} -LISTEN_PORT ${_listen_port} -DNS_LOCAL ${_dns_local} -DNS_TRUST ${_dns_trust}"
|
||||
_extra_param="${_extra_param} -USE_DIRECT_LIST ${_use_direct_list} -USE_PROXY_LIST ${_use_proxy_list} -USE_BLOCK_LIST ${_use_block_list}"
|
||||
@ -1430,7 +1430,8 @@ start_dns() {
|
||||
_default_tag=$(config_t_get global chinadns_ng_default_tag smart) \
|
||||
_no_logic_log=0 \
|
||||
_tcp_node=${TCP_NODE} \
|
||||
_filter_https=$(config_t_get global force_https_soa 0)
|
||||
_filter_https=$(config_t_get global force_https_soa 0) \
|
||||
_log=$(config_t_get global log_chinadns_ng 0)
|
||||
|
||||
USE_DEFAULT_DNS="chinadns_ng"
|
||||
}
|
||||
@ -1524,6 +1525,9 @@ acl_app() {
|
||||
[ "$(config_n_get $sid enabled)" = "1" ] || continue
|
||||
eval $(uci -q show "${CONFIG}.${item}" | cut -d'.' -sf 3-)
|
||||
|
||||
log=${log:-0}
|
||||
loglevel=${loglevel:-warning}
|
||||
|
||||
if [ -n "${sources}" ]; then
|
||||
for s in $sources; do
|
||||
local s2
|
||||
@ -1684,7 +1688,8 @@ acl_app() {
|
||||
_default_tag=${chinadns_ng_default_tag:-smart} \
|
||||
_no_logic_log=1 \
|
||||
_tcp_node=${tcp_node} \
|
||||
_filter_https=${force_https_soa:-0}
|
||||
_filter_https=${force_https_soa:-0} \
|
||||
_log=${log}
|
||||
|
||||
use_default_dns="chinadns_ng"
|
||||
}
|
||||
@ -1722,6 +1727,8 @@ acl_app() {
|
||||
redir_port=$(get_new_port $(expr $redir_port + 1))
|
||||
set_cache_var "node_${tcp_node}_redir_port" "${redir_port}"
|
||||
tcp_port=$redir_port
|
||||
local log_file="/dev/null"
|
||||
[ "${log}" = "1" ] && log_file="${TMP_ACL_PATH}/${sid}/TCP.log"
|
||||
|
||||
if [ "${type}" = "sing-box" ] || [ "${type}" = "xray" ]; then
|
||||
config_file="acl/${tcp_node}_TCP_${redir_port}.json"
|
||||
@ -1744,12 +1751,12 @@ acl_app() {
|
||||
}
|
||||
config_file="$TMP_PATH/$config_file"
|
||||
[ "${type}" = "sing-box" ] && type="singbox"
|
||||
run_${type} flag=$tcp_node node=$tcp_node tcp_redir_port=$redir_port ${_extra_param} config_file=$config_file
|
||||
run_${type} flag=$tcp_node node=$tcp_node tcp_redir_port=$redir_port ${_extra_param} config_file=$config_file log_file=$log_file loglevel=$loglevel
|
||||
else
|
||||
config_file="acl/${tcp_node}_SOCKS_${socks_port}.json"
|
||||
run_socks flag=$tcp_node node=$tcp_node bind=127.0.0.1 socks_port=$socks_port config_file=$config_file
|
||||
local log_file=$TMP_ACL_PATH/ipt2socks_${tcp_node}_${redir_port}.log
|
||||
log_file="/dev/null"
|
||||
# local log_file=$TMP_ACL_PATH/ipt2socks_${tcp_node}_${redir_port}.log
|
||||
# log_file="/dev/null"
|
||||
run_ipt2socks flag=acl_${tcp_node} tcp_tproxy=${is_tproxy} local_port=$redir_port socks_address=127.0.0.1 socks_port=$socks_port log_file=$log_file
|
||||
fi
|
||||
run_dns ${_dns_port}
|
||||
@ -1801,6 +1808,8 @@ acl_app() {
|
||||
redir_port=$(get_new_port $(expr $redir_port + 1))
|
||||
set_cache_var "node_${udp_node}_redir_port" "${redir_port}"
|
||||
udp_port=$redir_port
|
||||
local log_file="/dev/null"
|
||||
[ "${log}" = "1" ] && log_file="${TMP_ACL_PATH}/${sid}/UDP.log"
|
||||
|
||||
local type
|
||||
if [ "$(config_get_type ${udp_node#Socks_})" = "socks" ]; then
|
||||
@ -1816,12 +1825,12 @@ acl_app() {
|
||||
config_file="acl/${udp_node}_UDP_${redir_port}.json"
|
||||
config_file="$TMP_PATH/$config_file"
|
||||
[ "${type}" = "sing-box" ] && type="singbox"
|
||||
run_${type} flag=$udp_node node=$udp_node udp_redir_port=$redir_port config_file=$config_file
|
||||
run_${type} flag=$udp_node node=$udp_node udp_redir_port=$redir_port config_file=$config_file log_file=$log_file loglevel=$loglevel
|
||||
else
|
||||
config_file="acl/${udp_node}_SOCKS_${socks_port}.json"
|
||||
run_socks flag=$udp_node node=$udp_node bind=127.0.0.1 socks_port=$socks_port config_file=$config_file
|
||||
local log_file=$TMP_ACL_PATH/ipt2socks_${udp_node}_${redir_port}.log
|
||||
log_file="/dev/null"
|
||||
# local log_file=$TMP_ACL_PATH/ipt2socks_${udp_node}_${redir_port}.log
|
||||
# log_file="/dev/null"
|
||||
run_ipt2socks flag=acl_${udp_node} local_port=$redir_port socks_address=127.0.0.1 socks_port=$socks_port log_file=$log_file
|
||||
fi
|
||||
fi
|
||||
@ -1832,7 +1841,7 @@ acl_app() {
|
||||
fi
|
||||
}
|
||||
unset enabled sid remarks sources interface tcp_no_redir_ports udp_no_redir_ports use_global_config tcp_node udp_node use_direct_list use_proxy_list use_block_list use_gfw_list chn_list tcp_proxy_mode udp_proxy_mode filter_proxy_ipv6 dns_mode remote_dns v2ray_dns_mode remote_dns_doh remote_dns_client_ip
|
||||
unset _ip _mac _iprange _ipset _ip_or_mac source_list tcp_port udp_port config_file _extra_param dns_cache_key
|
||||
unset _ip _mac _iprange _ipset _ip_or_mac source_list tcp_port udp_port config_file _extra_param dns_cache_key log loglevel
|
||||
unset _china_ng_listen _chinadns_local_dns _direct_dns_mode chinadns_ng_default_tag dnsmasq_filter_proxy_ipv6 remote_fakedns force_https_soa use_fakedns
|
||||
done
|
||||
unset socks_port redir_port dns_port dnsmasq_port chinadns_port
|
||||
|
||||
@ -57,6 +57,7 @@ const callLuciNetworkDevices = rpc.declare({
|
||||
const callLuciWirelessDevices = rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
method: 'getWirelessDevices',
|
||||
nobatch: true,
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
@ -100,6 +101,9 @@ const callNetworkProtoHandlers = rpc.declare({
|
||||
|
||||
let _init = null;
|
||||
let _state = null;
|
||||
let _wirelessInit = null;
|
||||
let _wirelessRadios = {};
|
||||
let _wirelessHostapd = {};
|
||||
const _protocols = {};
|
||||
const _protospecs = {};
|
||||
|
||||
@ -352,6 +356,55 @@ function maskToPrefix(mask, v6) {
|
||||
return bits;
|
||||
}
|
||||
|
||||
function refreshWirelessState() {
|
||||
if (_wirelessInit != null)
|
||||
return _wirelessInit;
|
||||
|
||||
const wifiDevices = uci.sections('wireless', 'wifi-device');
|
||||
const qcaOnly = wifiDevices.length > 0 && wifiDevices.every(function(device) {
|
||||
return (device.type == 'qcawifi' || device.type == 'qcawificfg80211');
|
||||
});
|
||||
|
||||
if (qcaOnly)
|
||||
return Promise.resolve();
|
||||
|
||||
_wirelessInit = L.resolveDefault(callLuciWirelessDevices(), {}).then(function(radios) {
|
||||
const objects = [];
|
||||
|
||||
_wirelessRadios = L.isObject(radios) ? radios : {};
|
||||
|
||||
for (let radio in _wirelessRadios)
|
||||
if (L.isObject(_wirelessRadios[radio]) && Array.isArray(_wirelessRadios[radio].interfaces))
|
||||
for (let ri of _wirelessRadios[radio].interfaces)
|
||||
if (L.isObject(ri) && ri.ifname)
|
||||
objects.push('hostapd.%s'.format(ri.ifname));
|
||||
|
||||
return (objects.length ? L.resolveDefault(rpc.list.apply(rpc, objects), {}) : Promise.resolve({}));
|
||||
}).then(function(res) {
|
||||
const hostapd = {};
|
||||
|
||||
for (let k in res) {
|
||||
const m = k.match(/^hostapd\.(.+)$/);
|
||||
|
||||
if (m)
|
||||
hostapd[m[1]] = res[k];
|
||||
}
|
||||
|
||||
_wirelessHostapd = hostapd;
|
||||
|
||||
if (_state != null) {
|
||||
_state.radios = _wirelessRadios;
|
||||
_state.hostapd = _wirelessHostapd;
|
||||
}
|
||||
|
||||
_wirelessInit = null;
|
||||
}).catch(function() {
|
||||
_wirelessInit = null;
|
||||
});
|
||||
|
||||
return _wirelessInit;
|
||||
}
|
||||
|
||||
function initNetworkState(refresh) {
|
||||
if (_state == null || refresh) {
|
||||
const hasWifi = L.hasSystemFeature('wifi');
|
||||
@ -361,18 +414,17 @@ function initNetworkState(refresh) {
|
||||
L.resolveDefault(callNetworkInterfaceDump(), []),
|
||||
L.resolveDefault(callLuciBoardJSON(), {}),
|
||||
L.resolveDefault(callLuciNetworkDevices(), {}),
|
||||
L.resolveDefault(callLuciWirelessDevices(), {}),
|
||||
L.resolveDefault(callLuciHostHints(), {}),
|
||||
getProtocolHandlers(),
|
||||
L.resolveDefault(uci.load('network')),
|
||||
hasWifi ? L.resolveDefault(uci.load('wireless')) : L.resolveDefault(),
|
||||
L.resolveDefault(uci.load('luci'))
|
||||
]).then(function([netifd_ifaces, board_json, luci_devs, radios, hosts]) {
|
||||
]).then(function([netifd_ifaces, board_json, luci_devs, hosts]) {
|
||||
|
||||
const s = {
|
||||
isTunnel: {}, isBridge: {}, isSwitch: {}, isWifi: {},
|
||||
ifaces: netifd_ifaces, radios: radios, hosts: hosts,
|
||||
netdevs: {}, bridges: {}, switches: {}, hostapd: {}
|
||||
ifaces: netifd_ifaces, radios: _wirelessRadios, hosts: hosts,
|
||||
netdevs: {}, bridges: {}, switches: {}, hostapd: _wirelessHostapd
|
||||
};
|
||||
|
||||
for (let name in luci_devs) {
|
||||
@ -515,25 +567,9 @@ function initNetworkState(refresh) {
|
||||
}
|
||||
|
||||
_init = null;
|
||||
_state = s;
|
||||
|
||||
const objects = [];
|
||||
|
||||
if (L.isObject(s.radios))
|
||||
for (let radio in s.radios)
|
||||
if (L.isObject(s.radios[radio]) && Array.isArray(s.radios[radio].interfaces))
|
||||
for (let ri of s.radios[radio].interfaces)
|
||||
if (L.isObject(ri) && ri.ifname)
|
||||
objects.push('hostapd.%s'.format(ri.ifname));
|
||||
|
||||
return (objects.length ? L.resolveDefault(rpc.list.apply(rpc, objects), {}) : Promise.resolve({})).then(function(res) {
|
||||
for (let k in res) {
|
||||
const m = k.match(/^hostapd\.(.+)$/);
|
||||
if (m)
|
||||
s.hostapd[m[1]] = res[k];
|
||||
}
|
||||
|
||||
return (_state = s);
|
||||
});
|
||||
return s;
|
||||
});
|
||||
} // end if (refresh || !_init)
|
||||
|
||||
@ -1366,6 +1402,8 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
|
||||
*/
|
||||
getWifiDevice(devname) {
|
||||
return initNetworkState().then(L.bind(function() {
|
||||
refreshWirelessState();
|
||||
|
||||
const existingDevice = uci.get('wireless', devname);
|
||||
|
||||
if (existingDevice == null || existingDevice['.type'] != 'wifi-device')
|
||||
@ -1386,6 +1424,8 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
|
||||
*/
|
||||
getWifiDevices() {
|
||||
return initNetworkState().then(L.bind(function() {
|
||||
refreshWirelessState();
|
||||
|
||||
const uciWifiDevices = uci.sections('wireless', 'wifi-device');
|
||||
const rv = [];
|
||||
|
||||
@ -1429,8 +1469,11 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
|
||||
* be found.
|
||||
*/
|
||||
getWifiNetwork(netname) {
|
||||
return initNetworkState()
|
||||
.then(L.bind(this.lookupWifiNetwork, this, netname));
|
||||
return initNetworkState().then(L.bind(function() {
|
||||
refreshWirelessState();
|
||||
|
||||
return this.lookupWifiNetwork(netname);
|
||||
}, this));
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1444,6 +1487,8 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
|
||||
*/
|
||||
getWifiNetworks() {
|
||||
return initNetworkState().then(L.bind(function() {
|
||||
refreshWirelessState();
|
||||
|
||||
const wifiIfaces = uci.sections('wireless', 'wifi-iface');
|
||||
const rv = [];
|
||||
|
||||
|
||||
@ -42,6 +42,10 @@ let cachedIwinfoResolver = null;
|
||||
let cachedIwinfoResolverPromise = null;
|
||||
let cachedAssocLists = Object.create(null);
|
||||
let pendingAssocLists = Object.create(null);
|
||||
let qcaRuntimeProbeHoldoffUntil = 0;
|
||||
|
||||
const qcaRuntimeProbeHoldoffKey = 'luci-qca-wireless-runtime-holdoff';
|
||||
const qcaRuntimeProbeHoldoff = 30000;
|
||||
|
||||
function pushUnique(list, value) {
|
||||
if (value && list.indexOf(value) < 0)
|
||||
@ -286,6 +290,9 @@ function refreshIwinfoInfoMap() {
|
||||
}
|
||||
|
||||
function startIwinfoInfoRefresh() {
|
||||
if (areQcaRuntimeProbesSuspended())
|
||||
return;
|
||||
|
||||
refreshIwinfoInfoMap().catch(() => {});
|
||||
}
|
||||
|
||||
@ -300,6 +307,29 @@ function isQcaWifiHwtype(hwtype) {
|
||||
return (hwtype == 'qcawifi' || hwtype == 'qcawificfg80211');
|
||||
}
|
||||
|
||||
function suspendQcaRuntimeProbes() {
|
||||
qcaRuntimeProbeHoldoffUntil = Date.now() + qcaRuntimeProbeHoldoff;
|
||||
|
||||
try {
|
||||
window.sessionStorage.setItem(qcaRuntimeProbeHoldoffKey, String(qcaRuntimeProbeHoldoffUntil));
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
|
||||
function areQcaRuntimeProbesSuspended() {
|
||||
let deadline = qcaRuntimeProbeHoldoffUntil;
|
||||
|
||||
try {
|
||||
deadline = Math.max(deadline, +window.sessionStorage.getItem(qcaRuntimeProbeHoldoffKey) || 0);
|
||||
|
||||
if (deadline <= Date.now())
|
||||
window.sessionStorage.removeItem(qcaRuntimeProbeHoldoffKey);
|
||||
}
|
||||
catch (e) {}
|
||||
|
||||
return (deadline > Date.now());
|
||||
}
|
||||
|
||||
function isConfigWifiDeviceDisabled(deviceName) {
|
||||
return (uci.get('wireless', deviceName, 'disabled') == '1');
|
||||
}
|
||||
@ -1146,6 +1176,10 @@ function getAssocListForNetwork(radioNet) {
|
||||
return Promise.resolve([]);
|
||||
|
||||
const hwtype = uci.get('wireless', radioNet.getWifiDeviceName(), 'type');
|
||||
|
||||
if (isQcaWifiHwtype(hwtype) && areQcaRuntimeProbesSuspended())
|
||||
return Promise.resolve([]);
|
||||
|
||||
const candidates = getAssocListCandidates(radioNet);
|
||||
const resolvedIfname = candidates[0];
|
||||
|
||||
@ -1464,9 +1498,13 @@ function radio_restart(id, ev) {
|
||||
|
||||
function network_updown(id, map, ev) {
|
||||
const radio = uci.get('wireless', id, 'device');
|
||||
const hwtype = uci.get('wireless', radio, 'type');
|
||||
const disabled = (uci.get('wireless', id, 'disabled') == '1') ||
|
||||
(uci.get('wireless', radio, 'disabled') == '1');
|
||||
|
||||
if (isQcaWifiHwtype(hwtype))
|
||||
suspendQcaRuntimeProbes();
|
||||
|
||||
if (disabled) {
|
||||
uci.unset('wireless', id, 'disabled');
|
||||
uci.unset('wireless', radio, 'disabled');
|
||||
@ -1548,14 +1586,19 @@ var CBIWifiFrequencyValue = form.Value.extend({
|
||||
const chval = +cfg_channel;
|
||||
const allow_auto = (hwtype == 'mt_dbdc' || isQcaWifiHwtype(hwtype) ||
|
||||
cfg_channel == 'auto' || L.hasSystemFeature('hostapd', 'acs'));
|
||||
const configOnly = isConfigWifiDeviceDisabled(device_section) ||
|
||||
(isQcaWifiHwtype(hwtype) && areQcaRuntimeProbesSuspended());
|
||||
const configDevice = configOnly
|
||||
? network.getWifiDevicesFromConfig().find((device) => device.getName() == device_section)
|
||||
: null;
|
||||
|
||||
return Promise.all([
|
||||
network.getWifiDevice(device_section),
|
||||
this.callFrequencyList(device_section),
|
||||
this.callDeviceInfo(device_section),
|
||||
L.resolveDefault(this.callWirelessStatus(), {}),
|
||||
isQcaWifiHwtype(hwtype) ? L.resolveDefault(fs.exec_direct('/usr/sbin/iw', [ 'dev' ]), '') : '',
|
||||
isQcaWifiHwtype(hwtype) ? L.resolveDefault(fs.exec_direct('/usr/sbin/iw', [ 'phy' ]), '') : ''
|
||||
configOnly ? configDevice : network.getWifiDevice(device_section),
|
||||
configOnly ? [] : this.callFrequencyList(device_section),
|
||||
configOnly ? {} : this.callDeviceInfo(device_section),
|
||||
configOnly ? {} : L.resolveDefault(this.callWirelessStatus(), {}),
|
||||
!configOnly && isQcaWifiHwtype(hwtype) ? L.resolveDefault(fs.exec_direct('/usr/sbin/iw', [ 'dev' ]), '') : '',
|
||||
!configOnly && isQcaWifiHwtype(hwtype) ? L.resolveDefault(fs.exec_direct('/usr/sbin/iw', [ 'phy' ]), '') : ''
|
||||
]).then(L.bind(function(data) {
|
||||
const wifidevs = data[0];
|
||||
const freqlist = data[1];
|
||||
@ -1613,6 +1656,10 @@ var CBIWifiFrequencyValue = form.Value.extend({
|
||||
}
|
||||
|
||||
const configured_band = getConfiguredBand(hwtype, statuscfg.hwmode ?? hwval, devcfg.channel ?? cfg_channel, devcfg.band ?? bandval, statuscfg.htmode ?? htval);
|
||||
|
||||
if (configOnly && cfg_channel && cfg_channel != 'auto' && Array.isArray(this.channels[configured_band]))
|
||||
this.channels[configured_band].push(cfg_channel, String(cfg_channel), { available: true });
|
||||
|
||||
const has_band_channels = (band) => {
|
||||
const channels = this.channels[band];
|
||||
const offset = (channels?.[0] == 'auto') ? 3 : 0;
|
||||
@ -2228,8 +2275,10 @@ var CBIWifiTxPowerValue = form.ListValue.extend({
|
||||
|
||||
load: function(section_id) {
|
||||
const device_section = this.getDeviceSection ? this.getDeviceSection(section_id) : section_id;
|
||||
const hwtype = uci.get('wireless', device_section, 'type');
|
||||
|
||||
if (isConfigWifiDeviceDisabled(device_section)) {
|
||||
if (isConfigWifiDeviceDisabled(device_section) ||
|
||||
(isQcaWifiHwtype(hwtype) && areQcaRuntimeProbesSuspended())) {
|
||||
this.powerval = this.wifiNetwork ? getDisplayTxPower(this.wifiNetwork) : null;
|
||||
this.poweroff = this.wifiNetwork ? this.wifiNetwork.getTXPowerOffset() : null;
|
||||
this.value('', _('driver default'));
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
/* The bar carries no colour transition: one would ease its bg/text over
|
||||
--mega-menu-duration on every data-darkmode flip, lagging the bar behind
|
||||
the untransitioned page in all three nav modes. The mega-menu wipe colour
|
||||
lives on .desktop-menu-sheet below, which owns its transition. */
|
||||
header {
|
||||
@apply bg-bg sticky top-0 z-40 mb-2 transition-colors duration-(--mega-menu-duration,300ms) ease-[cubic-bezier(0.4,0,0.6,1)];
|
||||
@apply bg-bg sticky top-0 z-40 mb-2;
|
||||
|
||||
& .header-content {
|
||||
@apply relative z-10 flex h-14 items-center justify-between px-6 py-3 max-md:px-4 max-md:py-2;
|
||||
@ -266,9 +270,11 @@ header {
|
||||
@apply md:flex;
|
||||
}
|
||||
|
||||
/* D1 底线呼吸:展开态沿用短底线汉堡;折叠后底线伸满(三线齐整 =
|
||||
侧栏收纳完毕)。dasharray 只改可见长度,线帽形状不受影响。 */
|
||||
[data-nav-type="sidebar"].sidebar-collapsed
|
||||
/* D1 底线呼吸(2026-07 反转):展开后底线伸满 —— 三线齐整 = 内容
|
||||
全部亮出;折叠态回落到短底线 = 还有内容收着,缺口招手可展开。
|
||||
与耦合滑动同向:侧栏进场线补齐,退场线让位。dasharray 只改可见
|
||||
长度,线帽形状不受影响。 */
|
||||
[data-nav-type="sidebar"]:not(.sidebar-collapsed)
|
||||
&
|
||||
.navigation-toggle-line-bottom {
|
||||
@apply md:[stroke-dasharray:15_15];
|
||||
@ -284,6 +290,12 @@ header {
|
||||
& .navigation-toggle-line {
|
||||
transform-box: fill-box;
|
||||
@apply origin-center transition-[transform,opacity,stroke-dasharray] duration-150 ease-in-out;
|
||||
|
||||
/* Desktop sidebar: the dasharray breath runs in step with the
|
||||
250ms coupled slide (the mobile X morph keeps its snappy 150). */
|
||||
[data-nav-type="sidebar"] & {
|
||||
@apply md:duration-[250ms];
|
||||
}
|
||||
}
|
||||
|
||||
/* Tabler menu-2 geometry: all three lines are full-length equals.
|
||||
@ -356,8 +368,11 @@ body[data-nav-type="sidebar"] {
|
||||
overflow-x-clip keeps the sliding sidebar out of horizontal overflow. */
|
||||
|
||||
/* Single source of truth for the sidebar column, so collapsing only has to
|
||||
zero this one value instead of restating the grid template. */
|
||||
--sidebar-w: 17rem;
|
||||
zero this one value instead of restating the grid template.
|
||||
--sidebar-w-full stays constant: the inner panel width and the slide
|
||||
keyframes read it, and must not see the zeroed collapsed value. */
|
||||
--sidebar-w-full: 17rem;
|
||||
--sidebar-w: var(--sidebar-w-full);
|
||||
|
||||
@apply md:grid md:grid-cols-[var(--sidebar-w)_minmax(0,1fr)] md:grid-rows-[auto_minmax(0,1fr)] md:overflow-x-clip;
|
||||
|
||||
@ -410,29 +425,60 @@ body[data-nav-type="sidebar"] {
|
||||
}
|
||||
|
||||
& .sidebar-panel {
|
||||
@apply border-hairline sticky top-0 hidden h-dvh w-full overflow-hidden rounded-none border-r transition-[visibility] duration-[250ms] md:block;
|
||||
@apply sticky top-0 hidden h-dvh w-full overflow-hidden rounded-none md:block;
|
||||
}
|
||||
|
||||
/* The grid column snaps closed to avoid a full-page layout animation.
|
||||
Visibility removes collapsed controls from the tab order; on expansion,
|
||||
the inner panel still enters via compositor-only translate + opacity. */
|
||||
/* The grid column still snaps (one reflow, never a per-frame layout
|
||||
animation); visibility removes collapsed controls from the tab order.
|
||||
The hairline lives on the inner panel, not the frame, so it tracks the
|
||||
sliding edge during the run animations below. */
|
||||
&.sidebar-collapsed .sidebar-panel {
|
||||
@apply invisible;
|
||||
}
|
||||
|
||||
& .sidebar-panel-inner {
|
||||
@apply bg-bg flex h-full w-68 flex-col overflow-hidden transition-[translate,opacity] duration-[250ms];
|
||||
@apply bg-bg border-hairline flex h-full w-(--sidebar-w-full) flex-col overflow-hidden border-r;
|
||||
}
|
||||
|
||||
&.sidebar-collapsed .sidebar-panel-inner {
|
||||
@apply -translate-x-full opacity-0;
|
||||
@apply -translate-x-full;
|
||||
}
|
||||
|
||||
/* Coupled slide: the column width snaps in the same frame the
|
||||
sidebar-anim-* class lands, then the sidebar and the whole content
|
||||
column (header + #maincontent) slide the same 17rem with the same
|
||||
curve — compositor-only, so the seam moves as one wall with zero
|
||||
per-frame reflow. menu-aurora.js swaps the class per direction
|
||||
(distinct animation-names restart cleanly on rapid toggles) and
|
||||
clears it after the run. */
|
||||
&.sidebar-anim-open > header,
|
||||
&.sidebar-anim-open > #maincontent,
|
||||
&.sidebar-anim-open .sidebar-panel-inner {
|
||||
@apply md:animate-[sidebar-run-in_250ms_var(--ease-in-out)_both];
|
||||
}
|
||||
|
||||
&.sidebar-anim-close > header,
|
||||
&.sidebar-anim-close > #maincontent {
|
||||
@apply md:animate-[sidebar-run-back_250ms_var(--ease-in-out)_both];
|
||||
}
|
||||
|
||||
/* Closing: the column is already zero, so the panel holds its old spot
|
||||
as a fixed overlay for one run while its content slides off. */
|
||||
&.sidebar-anim-close .sidebar-panel {
|
||||
@apply md:visible md:fixed md:top-0 md:left-0 md:w-(--sidebar-w-full);
|
||||
}
|
||||
|
||||
&.sidebar-anim-close .sidebar-panel-inner {
|
||||
@apply md:animate-[sidebar-run-out_250ms_var(--ease-in-out)_both];
|
||||
}
|
||||
|
||||
/* Sidebar head: logo + hostname. h-14 matches the header row so its
|
||||
bottom hairline continues the header's border in one unbroken line
|
||||
across the viewport. */
|
||||
across the viewport. box-content: the header is auto-height (56px
|
||||
content + border OUTSIDE), so this border must sit outside its 56px
|
||||
too — border-box would paint it one pixel higher than the header's. */
|
||||
& .sidebar-head {
|
||||
@apply border-hairline flex h-14 shrink-0 items-center gap-2.5 border-b px-4;
|
||||
@apply border-hairline box-content flex h-14 shrink-0 items-center gap-2.5 border-b px-4;
|
||||
|
||||
& .sidebar-logo {
|
||||
@apply size-5 shrink-0 rounded-md;
|
||||
@ -475,6 +521,26 @@ body[data-nav-type="sidebar"] {
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared travel distance for the coupled sidebar slide: content column and
|
||||
panel move the same 17rem, so the seam between them never separates. */
|
||||
@keyframes sidebar-run-in {
|
||||
from {
|
||||
translate: calc(-1 * var(--sidebar-w-full)) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sidebar-run-back {
|
||||
from {
|
||||
translate: var(--sidebar-w-full) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sidebar-run-out {
|
||||
to {
|
||||
translate: calc(-1 * var(--sidebar-w-full)) 0;
|
||||
}
|
||||
}
|
||||
|
||||
#maincontent {
|
||||
/* Centred shells (top-bar navigation) clamp here and gutter the remainder.
|
||||
The sidebar shell overrides this — it caps the shell instead, see
|
||||
|
||||
@ -33,7 +33,9 @@
|
||||
}
|
||||
|
||||
.navigation-group-toggle {
|
||||
@apply after:size-3.5 after:shrink-0 after:bg-current after:opacity-55 after:transition-[transform,opacity] after:duration-[250ms] after:content-[''] after:[mask:var(--icon-arrow-right)_center/cover_no-repeat] hover:after:opacity-100;
|
||||
/* size-4.5 ≈ the row's text height — 3.5 read as undersized next to the
|
||||
text-lg category labels, on desktop and in the mobile drawer alike. */
|
||||
@apply after:size-4.5 after:shrink-0 after:bg-current after:opacity-55 after:transition-[transform,opacity] after:duration-[250ms] after:content-[''] after:[mask:var(--icon-arrow-right)_center/cover_no-repeat] hover:after:opacity-100;
|
||||
}
|
||||
|
||||
.nav-category-label {
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
}
|
||||
|
||||
.cmdk-panel {
|
||||
@apply border-hairline bg-surface/85 fixed top-24 left-1/2 z-120 w-[min(36rem,calc(100vw-2.5rem))] -translate-x-1/2 overflow-hidden rounded-3xl border shadow-xl backdrop-blur-xl backdrop-saturate-150 transition-[opacity,translate] duration-150 ease-out starting:-translate-y-2 starting:opacity-0;
|
||||
@apply border-hairline bg-surface/85 fixed top-24 left-1/2 z-120 w-[min(36rem,calc(100vw-2.5rem))] -translate-x-1/2 overflow-hidden rounded-2xl border shadow-xl backdrop-blur-xl backdrop-saturate-150 transition-[opacity,translate] duration-150 ease-out starting:-translate-y-2 starting:opacity-0;
|
||||
|
||||
/* <md: the same panel becomes a full-screen takeover (Algolia DocSearch
|
||||
manner) — opaque page background (no full-screen blur cost on phones),
|
||||
|
||||
@ -30,6 +30,7 @@ return baseclass.extend({
|
||||
const mobileList = overlay.querySelector("#mobile-nav-list");
|
||||
const desktop = window.matchMedia("(min-width: 768px)");
|
||||
const SIDEBAR_COLLAPSED_KEY = "aurora.sidebarCollapsed";
|
||||
let sidebarAnimTimer;
|
||||
|
||||
const isDesktopSidebar = () =>
|
||||
desktop.matches && document.body.dataset.navType === "sidebar";
|
||||
@ -79,7 +80,23 @@ return baseclass.extend({
|
||||
closeMobileNavigation();
|
||||
|
||||
const collapsed = !expanded;
|
||||
document.body.classList.toggle("sidebar-collapsed", collapsed);
|
||||
const body = document.body;
|
||||
|
||||
// Coupled slide (_layout.css): the class must land in the same
|
||||
// frame as the column snap. Open/close carry distinct
|
||||
// animation-names, so alternating toggles restart the run without
|
||||
// a forced reflow; the timer (not animationend — three elements
|
||||
// animate) clears the class once the 250ms run is over.
|
||||
body.classList.remove("sidebar-anim-open", "sidebar-anim-close");
|
||||
body.classList.add(
|
||||
collapsed ? "sidebar-anim-close" : "sidebar-anim-open",
|
||||
);
|
||||
clearTimeout(sidebarAnimTimer);
|
||||
sidebarAnimTimer = setTimeout(() => {
|
||||
body.classList.remove("sidebar-anim-open", "sidebar-anim-close");
|
||||
}, 300);
|
||||
|
||||
body.classList.toggle("sidebar-collapsed", collapsed);
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, collapsed);
|
||||
updateToggleState(expanded);
|
||||
return;
|
||||
@ -488,7 +505,7 @@ return baseclass.extend({
|
||||
this.bindNavigationAccordion(list);
|
||||
|
||||
crumb.forEach((title, i) => {
|
||||
if (i) crumbEl?.appendChild(E("li", { class: "crumb-sep" }, ["›"]));
|
||||
if (i) crumbEl?.appendChild(E("li", { class: "crumb-sep" }, ["/"]));
|
||||
crumbEl?.appendChild(
|
||||
E("li", { class: i === crumb.length - 1 ? "current" : "" }, [title]),
|
||||
);
|
||||
@ -533,12 +550,14 @@ return baseclass.extend({
|
||||
// index — matched and rendered like pages, grouped under _("Design")
|
||||
// (the System → Language and Style label) — but execute header.ut's
|
||||
// global setTheme() instead of navigating, so the panel stays open and
|
||||
// previews the switch live. luci-base has no Light/Dark msgids — they
|
||||
// stay English literals, wrapped in _() so a future catalog entry would
|
||||
// take effect (the same trade the icon-only switcher makes).
|
||||
// previews the switch live. luci-base carries no mode msgids, so the
|
||||
// labels borrow luci-app-aurora-config's already-translated "Light
|
||||
// Mode"/"Dark Mode" — matching its msgids verbatim is what makes the
|
||||
// palette follow the UI language wherever the config app is installed,
|
||||
// and they fall back to English where it isn't.
|
||||
[
|
||||
["light", _("Light")],
|
||||
["dark", _("Dark")],
|
||||
["light", _("Light Mode")],
|
||||
["dark", _("Dark Mode")],
|
||||
["device", _("Automatic")],
|
||||
].forEach(([mode, title]) =>
|
||||
this.paletteIndex.push({
|
||||
@ -549,8 +568,9 @@ return baseclass.extend({
|
||||
}),
|
||||
);
|
||||
|
||||
// Only msgids that already exist in the luci-base catalog are used —
|
||||
// the theme intentionally ships no translations of its own.
|
||||
// Only msgids that already exist in a shipped catalog (luci-base, or the
|
||||
// config app's as above) are used — the theme intentionally ships no
|
||||
// translations of its own.
|
||||
const isMac = /Mac|iP(ad|hone|od)/.test(navigator.platform);
|
||||
this.paletteKey = isMac ? "⌘K" : "Ctrl+K";
|
||||
toggle.setAttribute("aria-keyshortcuts", isMac ? "Meta+K" : "Control+K");
|
||||
@ -1039,8 +1059,9 @@ return baseclass.extend({
|
||||
);
|
||||
}
|
||||
// Both vars live on the header, not the container: the container
|
||||
// inherits the height, and the header's own transition-colors reads
|
||||
// the duration so the bar colour fades in lockstep with the wipe.
|
||||
// inherits both, and the close fallback timer reads the duration
|
||||
// back off the header (the bar itself must stay transition-free so
|
||||
// theme flips repaint it in the same frame as the page).
|
||||
header.style.setProperty("--mega-menu-height", `${canvasHeight}px`);
|
||||
header.style.setProperty("--mega-menu-duration", `${revealDuration}ms`);
|
||||
};
|
||||
|
||||
@ -729,7 +729,7 @@ test("renders sidebar items, logout, and translated crumbs without duplication",
|
||||
assert.equal(crumb.children.length, 3);
|
||||
assert.deepEqual(
|
||||
crumb.children.map((child) => textContent(child)),
|
||||
["translated:Network", "›", "translated:Wireless"],
|
||||
["translated:Network", "/", "translated:Wireless"],
|
||||
);
|
||||
assert.equal(crumb.children[2].getAttribute("class"), "current");
|
||||
assert.equal(list.dataset.accordionBound, "true");
|
||||
|
||||
@ -83,7 +83,7 @@ test("shared navigation styles define active and expanded states", () => {
|
||||
assertIncludesUtilities(sublink, [
|
||||
"font-medium",
|
||||
"hover:bg-hover-faint",
|
||||
"rounded-lg",
|
||||
"rounded-xl",
|
||||
]);
|
||||
assertIncludesUtilities(activeSublink, [
|
||||
"text-brand",
|
||||
@ -212,6 +212,20 @@ test("mega-menu reveal and retract share the page-top origin", () => {
|
||||
assert.doesNotMatch(headerLift ?? "", /bg-mega-menu-bg/);
|
||||
});
|
||||
|
||||
test("theme flips repaint the bar in the same frame as the page", () => {
|
||||
const headerDeclaration = layoutStyles.match(
|
||||
/^header \{\s*@apply ([^;]+);/m,
|
||||
)?.[1];
|
||||
|
||||
assert.ok(headerDeclaration, "Missing header root declaration");
|
||||
assertIncludesUtilities(headerDeclaration, ["bg-bg", "sticky"]);
|
||||
// A colour transition on the bar itself would ease its bg/text over
|
||||
// --mega-menu-duration on every data-darkmode flip, lagging the bar behind
|
||||
// the untransitioned page in all three nav modes. The mega-menu wipe colour
|
||||
// lives on .desktop-menu-sheet, which carries its own transition.
|
||||
assert.doesNotMatch(headerDeclaration, /\btransition/);
|
||||
});
|
||||
|
||||
test("mega-menu category masks use Tailwind arbitrary utilities", () => {
|
||||
const title = getBlock(layoutStyles, "& .desktop-nav-title");
|
||||
const icon = getBlock(title, "&::before");
|
||||
|
||||
@ -167,7 +167,8 @@ test("content dropdowns stay above the closed header and below the open mega-men
|
||||
/\[data-nav-type="mega-menu"\]\s*&:has\([\s\S]*?\.desktop-menu-container[\s\S]*?\.active[\s\S]*?\)\s*\{\s*@apply\s+([^;]+);/,
|
||||
)?.[1] ?? "";
|
||||
const dropdownRule = dropdown.match(/&\.dropdown\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
const messageRule = message.match(/\.alert-message\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
const messageRule =
|
||||
message.match(/\.alert-message\s*\{[\s\S]*?@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
const overlayRule =
|
||||
overlay.match(/& \.desktop-menu-overlay\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
|
||||
|
||||
|
||||
@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_VERSION:=1.1.4
|
||||
PKG_RELEASE:=58
|
||||
PKG_VERSION:=1.1.6
|
||||
PKG_RELEASE:=59
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user