update 2026-08-08 16:33:49

This commit is contained in:
action committed 2026-08-08 16:33:49 +08:00
1 parent d76a25e5cf
commit 74e7dc08d2
22 files changed
+794 -131

No files matched your search

+2 -2
View File
@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=dae PKG_NAME:=dae
PKG_VERSION:=2026.08.06 PKG_VERSION:=2026.08.08
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=c7547891533d195731507977cb36734a5a18aa14 PKG_SOURCE_VERSION:=f52534a912a2f0a1406740384684667c1ece89c0
PKG_SOURCE_URL:=https://github.com/olicesx/dae.git PKG_SOURCE_URL:=https://github.com/olicesx/dae.git
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
+2 -2
View File
@@ -8,12 +8,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=ddns-go PKG_NAME:=ddns-go
PKG_VERSION:=6.17.4 PKG_VERSION:=6.17.5
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=80f3816d95933949ce7306b988b2612d91b95c6992af8e956378628fde19b615 PKG_HASH:=910ba8e64d05fd48043f4c88492f32f459367be022ec08f0aa6e977c5c3419bf
PKG_LICENSE:=MIT PKG_LICENSE:=MIT
PKG_LICENSE_FILES:=LICENSE PKG_LICENSE_FILES:=LICENSE
@@ -0,0 +1,45 @@
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function get_current_url() {
return window.location.origin + window.location.pathname;
}
function getOption(config, section, opt) {
let obj;
const id = `cbid.${config}.${section}.${opt}`;
obj = document.getElementsByName(id)[0] || document.getElementById(id);
if (obj) {
const combobox = document.getElementById('cbi.combobox.' + id);
if (combobox) {
obj.combobox = combobox;
}
const div = document.getElementById(id);
if (div && div.getElementsByTagName("li").length > 0) {
obj = div;
}
return obj;
} else {
return null;
}
}
+106 -18
View File
@@ -79,8 +79,8 @@ function index()
entry({"admin", "services", appname, "connect_status"}, call("connect_status")).leaf = true entry({"admin", "services", appname, "connect_status"}, call("connect_status")).leaf = true
entry({"admin", "services", appname, "ping_node"}, call("ping_node")).leaf = true entry({"admin", "services", appname, "ping_node"}, call("ping_node")).leaf = true
entry({"admin", "services", appname, "urltest_node"}, call("urltest_node")).leaf = true entry({"admin", "services", appname, "urltest_node"}, call("urltest_node")).leaf = true
entry({"admin", "services", appname, "update_config"}, call("update_config")).leaf = true
entry({"admin", "services", appname, "add_node"}, call("add_node")).leaf = true entry({"admin", "services", appname, "add_node"}, call("add_node")).leaf = true
entry({"admin", "services", appname, "update_node"}, call("update_node")).leaf = true
entry({"admin", "services", appname, "set_node"}, call("set_node")).leaf = true entry({"admin", "services", appname, "set_node"}, call("set_node")).leaf = true
entry({"admin", "services", appname, "copy_node"}, call("copy_node")).leaf = true entry({"admin", "services", appname, "copy_node"}, call("copy_node")).leaf = true
entry({"admin", "services", appname, "clear_all_nodes"}, call("clear_all_nodes")).leaf = true entry({"admin", "services", appname, "clear_all_nodes"}, call("clear_all_nodes")).leaf = true
@@ -96,6 +96,10 @@ function index()
entry({"admin", "services", appname, "subscribe_manual"}, call("subscribe_manual")).leaf = true entry({"admin", "services", appname, "subscribe_manual"}, call("subscribe_manual")).leaf = true
entry({"admin", "services", appname, "subscribe_manual_all"}, call("subscribe_manual_all")).leaf = true entry({"admin", "services", appname, "subscribe_manual_all"}, call("subscribe_manual_all")).leaf = true
entry({"admin", "services", appname, "flush_set"}, call("flush_set")).leaf = true entry({"admin", "services", appname, "flush_set"}, call("flush_set")).leaf = true
entry({"admin", "services", appname, "get_shunt_rules"}, call("get_shunt_rules")).leaf = true
entry({"admin", "services", appname, "add_shunt_rule"}, call("add_shunt_rule")).leaf = true
entry({"admin", "services", appname, "delete_select_shunt_rules"}, call("delete_select_shunt_rules")).leaf = true
entry({"admin", "services", appname, "save_shunt_rule_order"}, call("save_shunt_rule_order")).leaf = true
--[[rule_list]] --[[rule_list]]
entry({"admin", "services", appname, "read_rulelist"}, call("read_rulelist")).leaf = true entry({"admin", "services", appname, "read_rulelist"}, call("read_rulelist")).leaf = true
@@ -471,6 +475,23 @@ function urltest_node()
http_write_json(e) http_write_json(e)
end end
function update_config()
local id = http.formvalue("id") -- Node id
local data = http.formvalue("data") -- json new Data
if id and data then
local data_t = jsonParse(data) or {}
if next(data_t) then
for k, v in pairs(data_t) do
uci:set(appname, id, k, v)
end
api.uci_save(uci, appname)
http_write_json_ok()
return
end
end
http_write_json_error()
end
function add_node() function add_node()
local redirect = http.formvalue("redirect") local redirect = http.formvalue("redirect")
@@ -493,23 +514,6 @@ function add_node()
end end
end end
function update_node()
local id = http.formvalue("id") -- Node id
local data = http.formvalue("data") -- json new Data
if id and data then
local data_t = jsonParse(data) or {}
if next(data_t) then
for k, v in pairs(data_t) do
uci:set(appname, id, k, v)
end
api.uci_save(uci, appname)
http_write_json_ok()
return
end
end
http_write_json_error()
end
function set_node() function set_node()
local protocol = http.formvalue("protocol") local protocol = http.formvalue("protocol")
local section = http.formvalue("section") local section = http.formvalue("section")
@@ -1134,3 +1138,87 @@ function fetch_certsha256()
local data = api.fetch_cert_sha256(address, port, sni, timeout, h3) local data = api.fetch_cert_sha256(address, port, sni, timeout, h3)
http_write_json(data ~= "" and { code = 1, data = data } or { code = 0 }) http_write_json(data ~= "" and { code = 1, data = data } or { code = 0 })
end end
function get_shunt_rules()
local id = http.formvalue("id")
local result = {}
if id then
result = uci:get_all(appname, id)
else
local default_items = {}
local other_items = {}
uci:foreach(appname, "shunt_rules", function(t)
if not t.group or t.group == "" then
default_items[#default_items + 1] = t
else
other_items[#other_items + 1] = t
end
end)
for i = 1, #default_items do result[#result + 1] = default_items[i] end
for i = 1, #other_items do result[#result + 1] = other_items[i] end
end
http_write_json(result)
end
function add_shunt_rule()
local add_name = http.formvalue("add_name")
local redirect = http.formvalue("redirect")
local uuid = add_name
if add_name then
local has = uci:get(appname, uuid)
if has then
http_write_json_error({ message = "This ID already exists." })
return
end
else
uuid = api.gen_short_uuid()
end
uci:section(appname, "shunt_rules", uuid)
local group = http.formvalue("group")
if group and group ~= "default" then
uci:set(appname, uuid, "group", group)
end
if redirect == "1" then
api.uci_save(uci, appname)
http.redirect(api.url("shunt_rules", uuid))
else
api.uci_save(uci, appname)
http_write_json_ok({uuid = uuid, redirect_url = api.url("shunt_rules", uuid)})
end
end
function delete_select_shunt_rules()
local ids = http.formvalue("ids")
local redirect = http.formvalue("redirect")
string.gsub(ids, '[^' .. "," .. ']+', function(w)
uci:foreach(appname, "nodes", function(s)
if s["protocol"] and s["protocol"] == "_shunt" then
uci:delete(appname, s[".name"], w)
end
end)
uci:delete(appname, w)
end)
if redirect == "1" then
api.uci_save(uci, appname)
http.redirect(api.url("rule"))
else
api.uci_save(uci, appname, true, true)
end
end
function save_shunt_rule_order()
local ids = http.formvalue("ids") or ""
local new_order = {}
for id in ids:gmatch("([^,]+)") do
new_order[#new_order + 1] = id
end
for idx, name in ipairs(new_order) do
luci.sys.call(string.format("uci -q reorder %s.%s=%d", appname, name, idx - 1))
end
api.sh_uci_commit(appname)
http_write_json({ status = "ok" })
end
@@ -154,6 +154,7 @@ if (has_singbox or has_xray) and #nodes_table > 0 then
if current_node.protocol == "_shunt" then if current_node.protocol == "_shunt" then
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua") local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua")
setfenv(shunt_lua, getfenv(1))(m, s, { setfenv(shunt_lua, getfenv(1))(m, s, {
s_cfgid = s:cfgsections()[1],
node_id = current_node_id, node_id = current_node_id,
node = current_node, node = current_node,
socks_list = socks_list, socks_list = socks_list,
@@ -4,9 +4,17 @@ if not data.node_id or not data.node then
return return
end end
local s_cfgid = data.s_cfgid
local current_node_id = data.node_id local current_node_id = data.node_id
local node_list = data.node_list or api.get_node_list() local node_list = data.node_list or api.get_node_list()
local groups = {}
m.uci:foreach(appname, "shunt_rules", function(s)
if s.group and s.group ~= "" then
groups[s.group] = true
end
end)
local function get_cfgvalue() local function get_cfgvalue()
return function(self, section) return function(self, section)
return m:get(current_node_id, self.option) return m:get(current_node_id, self.option)
@@ -92,15 +100,27 @@ o = add_option(Flag, "fakedns", '<a style="color:#FF8C00">FakeDNS</a>' .. " " ..
translate("Suitable scenarios for let the node servers get the target domain names.") .. "<br>" .. translate("Suitable scenarios for let the node servers get the target domain names.") .. "<br>" ..
translate("Such as: DNS unlocking of streaming media, reducing DNS query latency, etc.")) translate("Such as: DNS unlocking of streaming media, reducing DNS query latency, etc."))
shunt_group = add_option(ListValue, "shunt_group", translate("Shunt Rule Group"))
shunt_group:value("", translate("default"))
for k, v in pairs(groups) do
shunt_group:value(k)
end
local shunt_group_val = m:get(current_node_id, "shunt_group") or ""
shunt_group_val = shunt_group_val:lower()
local shunt_rules = {} local shunt_rules = {}
m.uci:foreach(appname, "shunt_rules", function(e) m.uci:foreach(appname, "shunt_rules", function(e)
e.id = e[".name"] local group = e.group or ""
e.remarks = e.remarks or e[".name"] group = group:lower()
e["_node_option"] = e[".name"] if group == shunt_group_val then
e["_node_default"] = "" e.id = e[".name"]
e["_fakedns_option"] = e[".name"] .. "_fakedns" e.remarks = e.remarks or e[".name"]
e["_proxy_tag_option"] = e[".name"] .. "_proxy_tag" e["_node_option"] = e[".name"]
table.insert(shunt_rules, e) e["_node_default"] = ""
e["_fakedns_option"] = e[".name"] .. "_fakedns"
e["_proxy_tag_option"] = e[".name"] .. "_proxy_tag"
table.insert(shunt_rules, e)
end
end) end)
table.insert(shunt_rules, { table.insert(shunt_rules, {
id = ".default", id = ".default",
@@ -185,6 +205,8 @@ end
local footer = Template(appname .. "/include/shunt_options") local footer = Template(appname .. "/include/shunt_options")
footer.api = api footer.api = api
footer.config = m.config
footer.id = current_node_id footer.id = current_node_id
footer.s_cfgid = s_cfgid or current_node_id
footer.normal_list = api.jsonc.stringify(node_list.normal_list) footer.normal_list = api.jsonc.stringify(node_list.normal_list)
m:append(footer) m:append(footer)
@@ -145,34 +145,8 @@ end
s:append(Template(appname .. "/rule/rule_version")) s:append(Template(appname .. "/rule/rule_version"))
local cfgname = "shunt_rules"
if has_xray or has_singbox then if has_xray or has_singbox then
s = m:section(TypedSection, cfgname, "Sing-Box/Xray " .. translate("Shunt Rule"), "<a style='color: red'>" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "</a>") m:append(Template(appname .. "/rule/shunt_rule_list"))
s.template = "cbi/tblsection"
s.anonymous = false
s.addremove = true
s.sortable = true
s.extedit = api.url("shunt_rules", "%s")
function s.create(e, t)
TypedSection.create(e, t)
luci.http.redirect(e.extedit:format(t))
end
function s.remove(e, t)
m.uci:foreach(appname, "nodes", function(s)
if s["protocol"] and s["protocol"] == "_shunt" then
m:del(s[".name"], t)
end
end)
TypedSection.remove(e, t)
end
o = s:option(DummyValue, "remarks", translate("Remarks"))
end end
local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
return api.return_map(m) return api.return_map(m)
@@ -44,6 +44,19 @@ function clean_text(text)
:gsub("[ \t]*\n[ \t]*", "\n") :gsub("[ \t]*\n[ \t]*", "\n")
end end
local remarks_lookup = {}
local groups = {}
m.uci:foreach(appname, "shunt_rules", function(s)
if s[".name"] ~= arg[1] then
if s.remarks then
remarks_lookup[s.remarks] = s[".name"]
end
if s.group and s.group ~= "" then
groups[s.group] = true
end
end
end)
s = m:section(NamedSection, arg[1], "shunt_rules", "") s = m:section(NamedSection, arg[1], "shunt_rules", "")
s.addremove = false s.addremove = false
s.dynamic = false s.dynamic = false
@@ -56,9 +69,34 @@ remarks.validate = function(self, value, section)
if value == "" then if value == "" then
return nil, translate("Remark cannot be empty.") return nil, translate("Remark cannot be empty.")
end end
if remarks_lookup[value] then
return nil, translate("This remark already exists, please change a new remark.")
end
return value return value
end end
o = s:option(Value, "group", translate("Shunt Rule Group"))
o.default = ""
o:value("", translate("default"))
for k, v in pairs(groups) do
o:value(k)
end
o.write = function(self, section, value)
value = api.trim(value)
local lower = value:lower()
if lower == "" or lower == "default" then
return m:del(section, self.option)
end
for _, v in ipairs(self.keylist or {}) do
if v:lower() == lower then
return m:set(section, self.option, v)
end
end
m:set(section, self.option, value)
end
protocol = s:option(MultiValue, "protocol", translate("Protocol")) protocol = s:option(MultiValue, "protocol", translate("Protocol"))
protocol:value("http") protocol:value("http")
protocol:value("tls") protocol:value("tls")
@@ -1594,11 +1594,15 @@ function gen_config(var)
[".name"] = "GFW_Mode_List", [".name"] = "GFW_Mode_List",
remarks = "GFW_Mode_List", remarks = "GFW_Mode_List",
domain_list = (domain_list ~= "") and domain_list or nil, domain_list = (domain_list ~= "") and domain_list or nil,
ip_list = (ip_list ~= "") and ip_list or nil ip_list = (ip_list ~= "") and ip_list or nil,
group = node["shunt_group"]
}) })
end end
end end
foreach_shunt_rule(function(e) foreach_shunt_rule(function(e)
if node["shunt_group"] ~= e.group then
return
end
local outboundTag = gen_shunt_node(e[".name"]) local outboundTag = gen_shunt_node(e[".name"])
if outboundTag and e.remarks then if outboundTag and e.remarks then
if outboundTag == "default" then if outboundTag == "default" then
@@ -1405,11 +1405,15 @@ function gen_config(var)
[".name"] = "GFW_Mode_List", [".name"] = "GFW_Mode_List",
remarks = "GFW_Mode_List", remarks = "GFW_Mode_List",
domain_list = (domain_list ~= "") and domain_list or nil, domain_list = (domain_list ~= "") and domain_list or nil,
ip_list = (ip_list ~= "") and ip_list or nil ip_list = (ip_list ~= "") and ip_list or nil,
group = node["shunt_group"]
}) })
end end
end end
foreach_shunt_rule(function(e) foreach_shunt_rule(function(e)
if node["shunt_group"] ~= e.group then
return
end
local outbound_tag = gen_shunt_node(e[".name"]) local outbound_tag = gen_shunt_node(e[".name"])
if outbound_tag and e.remarks then if outbound_tag and e.remarks then
if outbound_tag == "default" then if outbound_tag == "default" then
@@ -1,2 +1,24 @@
<!-- map header --> <!-- map header -->
<script src="<%=resource%>/view/<%=self.api.appname%>/cbi.js"></script> <script src="<%=resource%>/view/<%=self.api.appname%>/cbi.js"></script>
<script src="<%=resource%>/view/<%=self.api.appname%>/func.js"></script>
<script type="text/javascript">
//<![CDATA[
function update_config(cfgid, data, redirect_url) {
XHR.get('<%=self.api.url("update_config")%>', {
id: cfgid,
data: JSON.stringify(data)
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
if (redirect_url) {
window.location.href = redirect_url;
}
}
else {
alert("<%:Error%>");
}
});
}
//]]>
</script>
@@ -256,7 +256,7 @@ local appname = api.appname
let new_val = el.target.value let new_val = el.target.value
const new_hasItem = shunt_list.find(element => element.id == new_val); const new_hasItem = shunt_list.find(element => element.id == new_val);
if (new_hasItem) { if (new_hasItem) {
XHR.get('<%=api.url("update_node")%>', { XHR.get('<%=api.url("update_config")%>', {
id: "<%=self.global_cfgid%>", id: "<%=self.global_cfgid%>",
data: JSON.stringify({ data: JSON.stringify({
tcp_node: new_val tcp_node: new_val
@@ -53,6 +53,22 @@
} }
} }
} }
document.addEventListener("DOMContentLoaded", function () {
waitForElement('select[name*="<%=self.config%>"][name*="shunt_group"]', function(el) {
let o_val = el.value;
el.addEventListener("change", () => {
el.blur();
if (o_val != el.value) {
let save = true;
if (save) {
update_config("<%=self.id%>", {
shunt_group: getOption("<%=self.config%>", "<%=self.s_cfgid%>", "shunt_group").value,
}, get_current_url());
}
}
});
});
});
document.addEventListener("DOMContentLoaded", () => setTimeout(() => { document.addEventListener("DOMContentLoaded", () => setTimeout(() => {
refresh_depends(); refresh_depends();
const table_dom = document.getElementById("cbi-passwall-shunt_option_list"); const table_dom = document.getElementById("cbi-passwall-shunt_option_list");
@@ -1,8 +1,8 @@
<%
local api = self.api
-%>
<script type="text/javascript"> <script type="text/javascript">
//<![CDATA[ //<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=self.api.url("node_config")%>/' + node_id;
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) { waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) {
let o_val = el.value; let o_val = el.value;
@@ -11,13 +11,18 @@ local api = self.api
if (o_val != el.value) { if (o_val != el.value) {
let save = true; let save = true;
if (save) { if (save) {
//document.getElementsByClassName('cbi-button-save')[0].click(); /*
update_node({ const saveBtn = document.getElementsByClassName('cbi-button-save');
remarks: getOption("remarks").value, if (saveBtn && saveBtn.length > 0) {
group: getOption("group").value, saveBtn[saveBtn.length - 1].click();
type: getOption("type").value, }
*/
update_config(node_id, {
remarks: getOption("<%=self.config%>", "<%=self.section%>", "remarks").value,
group: getOption("<%=self.config%>", "<%=self.section%>", "group").value,
type: getOption("<%=self.config%>", "<%=self.section%>", "type").value,
//protocol: '' //protocol: ''
}); }, node_config_url);
} }
} }
}); });
@@ -31,12 +36,17 @@ local api = self.api
let name = name_split[name_split.length - 1]; let name = name_split[name_split.length - 1];
let save = true; let save = true;
if (save) { if (save) {
//document.getElementsByClassName('cbi-button-save')[0].click(); /*
update_node({ const saveBtn = document.getElementsByClassName('cbi-button-save');
remarks: getOption("remarks").value, if (saveBtn && saveBtn.length > 0) {
group: getOption("group").value, saveBtn[saveBtn.length - 1].click();
protocol: getOption(name).value }
}); */
update_config(node_id, {
remarks: getOption("<%=self.config%>", "<%=self.section%>", "remarks").value,
group: getOption("<%=self.config%>", "<%=self.section%>", "group").value,
protocol: getOption("<%=self.config%>", "<%=self.section%>", name).value,
}, node_config_url);
} else { } else {
window.location.href = node_config_url + "?select_proto=" + el.value; window.location.href = node_config_url + "?select_proto=" + el.value;
} }
@@ -1,64 +1,11 @@
<%
local api = self.api
-%>
<script type="text/javascript"> <script type="text/javascript">
//<![CDATA[ //<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=api.url("node_config")%>/' + node_id;
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function getOption(opt) {
var obj;
var id = 'cbid.<%=self.config%>.' + node_id + '.' + opt;
obj = document.getElementsByName(id)[0] || document.getElementById(id);
if (obj) {
var combobox = document.getElementById('cbi.combobox.' + id);
if (combobox) {
obj.combobox = combobox;
}
var div = document.getElementById(id);
if (div && div.getElementsByTagName("li").length > 0) {
obj = div;
}
return obj;
} else {
return null;
}
}
function update_node(data) {
XHR.get('<%=api.url("update_node")%>', {
id: node_id,
data: JSON.stringify(data)
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
window.location.href = node_config_url;
}
else {
alert("<%:Error%>");
}
});
}
function fetchCertSha256(el) { function fetchCertSha256(el) {
if (el.getAttribute("data-loading") === "1") return; if (el.getAttribute("data-loading") === "1") return;
el.setAttribute("data-loading", "1"); el.setAttribute("data-loading", "1");
el.style.pointerEvents = "none"; el.style.pointerEvents = "none";
el.style.color = "red"; el.style.color = "red";
XHR.get('<%=api.url("fetch_certsha256")%>', { id: node_id }, function(x, res) { XHR.get('<%=self.api.url("fetch_certsha256")%>', { id: "<%=self.section%>" }, function(x, res) {
el.setAttribute("data-loading", "0"); el.setAttribute("data-loading", "0");
el.style.pointerEvents = ""; el.style.pointerEvents = "";
el.style.color = ""; el.style.color = "";
@@ -0,0 +1,475 @@
<%
local api = require "luci.passwall.api"
local appname = api.appname
local uci = api.uci
-%>
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
<style>
table th, .table .th {
text-align: center;
}
table td, .table .td {
text-align: center;
/* white-space: nowrap; */
word-break: keep-all;
}
.td.cbi-section-actions {
text-align: right !important;
}
.shunt_rule-wrapper {
align-items: center;
display: inline-flex !important;
gap: 4px;
}
.cbi-tabmenu > li {
margin-right: 2px !important;
}
.cbi-tabmenu > li:last-child {
margin-right: 0 !important;
}
.shunt_rule-wrapper .drag-handle {
cursor: grab !important;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 100;
padding: 0 !important;
line-height: inherit;
user-select: none;
align-self: stretch;
background-color: transparent;
}
.shunt_rule-wrapper .drag-handle:hover {
background: transparent;
}
/* enable flex for small screens*/
@media screen and (max-width: 1152px) {
.cbi-section-table-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.cbi-section-table-titles {
display: none !important;
}
.td-actions {
padding-top: 0 !important;
border-top-width: 0 !important;
flex: 1 1 350px;
}
}
/* shrink actionbar even further for mobile devices */
@media screen and (max-width: 500px) {
.shunt_rule-wrapper {
gap: 0;
}
.cbi-button {
margin-left: 0 !important;
margin-right: 1px !important;
}
.td-actions {
padding-left: 5px!important;
padding-right: 5px !important;
}
}
.sortable-chosen {
background-color: rgba(220, 235, 245, 0.4) !important;
opacity: 0.7;
}
.sortable-ghost {
background: #cce5ff !important;
height: 3px !important;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
<% if api.is_js_luci() then -%>
<script type="text/javascript">
var cbi_t = [];
function cbi_t_add(section, tab) {
var t = document.getElementById('tab.' + section + '.' + tab);
var c = document.getElementById('container.' + section + '.' + tab);
if( t && c ) {
cbi_t[section] = (cbi_t[section] || [ ]);
cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
}
}
function cbi_t_switch(section, tab) {
if( cbi_t[section] && cbi_t[section][tab] ) {
// Before switching tabs, first deselect all currently active tabs.
var o = cbi_t[section][tab];
var h = document.getElementById('tab.' + section);
for( var tid in cbi_t[section] ) {
var o2 = cbi_t[section][tid];
if( o.tab.id != o2.tab.id ) {
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
o2.container.style.display = 'none';
}
else {
if(h) h.value = tab;
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
o2.container.style.display = 'block';
}
}
}
return false
}
</script>
<%- end %>
<script type="text/javascript">
//<![CDATA[
function toPage(action, cbi_id) {
if (action == "add") {
const dom = document.getElementsByClassName("cbi-tab")[0];
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
const add_name_val = document.getElementById("add_name_" + current_group).value;
if (add_name_val.length > 1) {
XHR.get('<%=api.url("add_shunt_rule")%>', {
add_name: add_name_val,
group: current_group
},
function(x, result) {
if (x && x.status === 200) {
if (result.code == 1) {
window.location.href = result.data.redirect_url;
} else {
alert(result.data.message);
}
} else {
alert("<%:Save failed!%>");
}
}
);
}
}
if (action == "edit") {
window.location.href='<%=api.url("shunt_rules")%>/' + cbi_id;
}
if (action == "del") {
window.location.href = '<%=api.url("delete_select_shunt_rules")%>' + "?redirect=1&ids=" + cbi_id;
}
}
function row_top(btn, group) {
const row = btn.closest("tr");
if (!row) return;
const parent = row.parentNode;
let firstDataRow = parent.querySelector("tr:not(.cbi-section-table-titles)");
if (firstDataRow && firstDataRow !== row) {
parent.insertBefore(row, firstDataRow);
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_shunt_rule_order(group);
if (!arraysEqual(new_order, origin_group_shunt_rule_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
}
}
}
}
function get_shunt_rule_order(group) {
let table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
if (!table) {
return;
}
let rows = table.querySelectorAll("tr.cbi-section-table-row");
if (!rows || rows.length === 0) {
return;
}
var ids = [];
rows.forEach(function(row) {
var id = row.id.replace("cbi-<%=appname%>-", "");
ids.push(id);
});
return ids;
}
function save_current_page_order(group) {
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
if (!table) {
alert("<%:No table!%>");
return;
}
var rows = table.querySelectorAll("tr.cbi-section-table-row");
if (!rows || rows.length === 0) {
alert("<%:No shunt_rules!%>");
return;
}
var btn = document.getElementById("save_order_btn_" + group);
if (btn) {
btn.style.display = "none";
btn.disabled = true;
}
var ids = [];
rows.forEach(function(row) {
var id = row.id.replace("cbi-<%=appname%>-", "");
ids.push(id);
});
XHR.get('<%=api.url("save_shunt_rule_order")%>', {
group: group,
ids: ids.join(",")
},
function(x, result) {
if (btn) {
btn.style.display = null;
btn.disabled = false;
}
if (x && x.status === 200) {
origin_group_shunt_rule_order[group] = get_shunt_rule_order(group);
alert("<%:Saved current page order successfully.%>");
if (btn) {
btn.style.display = "none";
}
} else {
alert("<%:Save failed!%>");
}
}
);
}
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
// List drag and rearrange
function initSortableForTable(table) {
if (!table) return null;
let group = table.id.replace("cbi-<%=appname%>-shunt_rules-", "").replace("-table", "")
var root = table.querySelector('tbody') || table;
if (root._sortable_initialized) return root._sortable_instance;
root._sortable_initialized = true;
var opts = {
handle: ".drag-handle",
draggable: "tr.cbi-section-table-row",
animation: 150,
ghostClass: "dragging-row",
fallbackOnBody: true,
forceFallback: false,
swapThreshold: 0.65,
onEnd: function (evt) {
//save_current_page_order(group); // Auto save
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_shunt_rule_order(group);
if (!arraysEqual(new_order, origin_group_shunt_rule_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
}
}
}
};
try {
var instance = Sortable.create(root, opts);
root._sortable_instance = instance;
return instance;
} catch (err) {
root._sortable_initialized = false;
console.error("Sortable init failed:", err);
return null;
}
}
function initAllSortable(group_shunt_rules) {
if (typeof Sortable === 'undefined') {
var retries = 0;
var maxRetries = 25;
var t = setInterval(function () {
retries++;
if (typeof Sortable !== 'undefined') {
clearInterval(t);
for (var group in group_shunt_rules) {
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
initSortableForTable(table);
}
} else if (retries >= maxRetries) {
clearInterval(t);
}
}, 200);
} else {
for (var group in group_shunt_rules) {
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
initSortableForTable(table);
}
}
}
function escape_html(s) {
return s.replace(/[&<>"']/g, c => ({
"&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;"
}[c]));
}
</script>
<script type="text/template" id="shunt_rules-table-template">
<fieldset class="cbi-section cbi-tblsection" id="cbi-<%=appname%>-shunt_rules-{{group}}-fieldset">
<table class="table cbi-section-table" id="cbi-<%=appname%>-shunt_rules-{{group}}-table" style="">
<tr class="tr cbi-section-table-titles anonymous">
<th class="th cbi-section-table-cell" style="width:30%">ID</th>
<th class="th cbi-section-table-cell" style="width:30%"><%:Remarks%></th>
<th class="th cbi-section-table-cell cbi-section-actions"></th>
</tr>
{{shunt_rule-tr}}
</table>
<div class="cbi-section-create cbi-tblsection-create">
<input type="text" id="add_name_{{group}}">
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="toPage('add')">
<input class="cbi-button cbi-button-apply" style="display: none" type="button" id="save_order_btn_{{group}}" value="<%:Save Order%>" onclick="save_current_page_order('{{group}}')">
</div>
</fieldset>
</script>
<script type="text/template" id="shunt_rule-tr-template">
<tr class="tr cbi-section-table-row" id="cbi-<%=appname%>-{{id}}">
<td class="td cbi-value-field"><b>{{id}}</b></td>
<td class="td cbi-value-field">{{remarks}}</td>
<td class="td cbi-section-table-cell nowrap cbi-section-actions td-actions">
<div class="shunt_rule-wrapper">
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="row_top(this, '{{group}}')" title="<%:To Top%>"/>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="toPage('edit', '{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="toPage('del', '{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
<span class="drag-handle center" title="<%:Drag to reorder%>">⠿</span>
</div>
</td>
</tr>
</script>
<div class="cbi-section">
<h3>Sing-Box/Xray <%:Shunt Rule%></h3>
<div class="cbi-section-descr"><a style="color: red"><%:Please note attention to the priority, the higher the order, the higher the priority.%></a></div>
<div id="shunt_rule_list"></div>
</div>
<script type="text/javascript">
function loadNodeList() {
XHR.get('<%=api.url("get_shunt_rules")%>', null, function(x, result) {
var shunt_rule_list = result
var group_shunt_rules = {}
for (let i = 0; i < shunt_rule_list.length; i++) {
let _shunt_rule = shunt_rule_list[i]
if (!_shunt_rule.group || _shunt_rule.group === "") {
_shunt_rule.group = "default"
}
if (!group_shunt_rules[_shunt_rule.group]) {
group_shunt_rules[_shunt_rule.group] = []
}
group_shunt_rules[_shunt_rule.group].push(_shunt_rule)
}
var tab_ul_li_html = '';
var tab_content_html = '';
var shunt_rules_table_template = document.getElementById("shunt_rules-table-template");
var shunt_rule_template = document.getElementById("shunt_rule-tr-template");
var default_group = null
for (let group in group_shunt_rules) {
if (default_group == null)
default_group = group
var table_html = "";
if (true) {
var new_shunt_rules_table_dom = shunt_rules_table_template.cloneNode(true);
var _html = new_shunt_rules_table_dom.innerHTML;
_html = _html.split("{{group}}").join(group);
var shunt_rule_tr_html = "";
for (var i = 0; i < group_shunt_rules[group].length; i++) {
let o = group_shunt_rules[group][i]
var newDom = shunt_rule_template.cloneNode(true);
newDom.classList.add("cbi-rowstyle-" + (i % 2 + 1));
var innerHTML = newDom.innerHTML;
innerHTML = innerHTML.split("{{id}}").join(o[".name"]);
innerHTML = innerHTML.split("{{group}}").join(o["group"] || "");
innerHTML = innerHTML.split("{{remarks}}").join(o["remarks"] || "");
shunt_rule_tr_html += innerHTML
}
_html = _html.split("{{shunt_rule-tr}}").join(shunt_rule_tr_html);
table_html = _html;
}
var group_name = group
if (group === "default") {
group_name = "<%:default%>"
}
tab_ul_li_html +=
'<li group_name="' + group + '" id="tab.<%=appname%>.shunt_rules.' + group + '" class="cbi-tab">' +
'<a onclick="this.blur(); return cbi_t_switch(\'<%=appname%>.shunt_rules\', \'' + group + '\')" href="<%=REQUEST_URI%>?tab.<%=appname%>.shunt_rules=' + group + '">' + escape_html(group_name) + " | " + "<font style='color: red'>" + group_shunt_rules[group].length + '</font></a>' +
'</li>'
tab_content_html +=
'<div class="cbi-tabcontainer" id="container.<%=appname%>.shunt_rules.' + group + '" style="display: none;">' +
'' + table_html +
'</div>'
}
function addTabDOM(li, content) {
let tab_ul_html = '<ul class="cbi-tabmenu">';
let tab_content_html = '<fieldset class="cbi-section-shunt_rule cbi-section-shunt_rule-tabbed" id="cbi-<%=appname%>-shunt_rules">';
if (li) {
tab_ul_html += li;
}
if (content) {
tab_content_html += content;
}
tab_ul_html += '</ul>';
tab_content_html += '</fieldset>';
return tab_ul_html + tab_content_html;
}
document.getElementById("shunt_rule_list").innerHTML = addTabDOM(tab_ul_li_html, tab_content_html);
for (let group in group_shunt_rules) {
cbi_t_add("<%=appname%>.shunt_rules", group)
}
if (default_group) {
cbi_t_switch("<%=appname%>.shunt_rules", default_group)
}
origin_group_shunt_rule_order = {};
for (let group in group_shunt_rules) {
origin_group_shunt_rule_order[group] = get_shunt_rule_order(group);
}
initAllSortable(group_shunt_rules);
});
}
loadNodeList();
//]]>
</script>
+3
View File
@@ -1094,6 +1094,9 @@ msgstr "启用后,规则列表可以支持 GeoIP/Geosite 规则。"
msgid "Shunt Rule" msgid "Shunt Rule"
msgstr "分流规则" msgstr "分流规则"
msgid "Shunt Rule Group"
msgstr "分流规则组"
msgid "Please note attention to the priority, the higher the order, the higher the priority." msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "请注意优先级问题,排序越上面优先级越高。" msgstr "请注意优先级问题,排序越上面优先级越高。"
@@ -391,7 +391,7 @@ if IS_SHUNT_NODE then
local default_node_id = t["default_node"] or "_direct" local default_node_id = t["default_node"] or "_direct"
uci:foreach(appname, "shunt_rules", function(s) uci:foreach(appname, "shunt_rules", function(s)
local _node_id = t[s[".name"]] local _node_id = t[s[".name"]]
if _node_id and _node_id ~= "_blackhole" then if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then
if _node_id == "_default" then if _node_id == "_default" then
_node_id = default_node_id _node_id = default_node_id
end end
@@ -619,7 +619,7 @@ function add_rule(var)
local default_node_id = t["default_node"] or "_direct" local default_node_id = t["default_node"] or "_direct"
uci:foreach(appname, "shunt_rules", function(s) uci:foreach(appname, "shunt_rules", function(s)
local _node_id = t[s[".name"]] local _node_id = t[s[".name"]]
if _node_id and _node_id ~= "_blackhole" then if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then
if _node_id == "_default" then if _node_id == "_default" then
_node_id = default_node_id _node_id = default_node_id
end end
@@ -559,7 +559,7 @@ if IS_SHUNT_NODE then
local default_node_id = t["default_node"] or "_direct" local default_node_id = t["default_node"] or "_direct"
uci:foreach(appname, "shunt_rules", function(s) uci:foreach(appname, "shunt_rules", function(s)
local _node_id = t[s[".name"]] local _node_id = t[s[".name"]]
if _node_id and _node_id ~= "_blackhole" then if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then
if _node_id == "_default" then if _node_id == "_default" then
_node_id = default_node_id _node_id = default_node_id
end end
@@ -952,7 +952,14 @@ add_firewall_rule() {
[ "$USE_SHUNT_NODE" = "1" ] && { [ "$USE_SHUNT_NODE" = "1" ] && {
local GEOIP_CODE="" local GEOIP_CODE=""
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}') local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
local shunt_group
if [ "${USE_SHUNT_TCP}" = "1" ]; then
shunt_group=$(config_n_get $_TCP_NODE shunt_group)
elif [ "${USE_SHUNT_UDP}" = "1" ]; then
shunt_group=$(config_n_get $_UDP_NODE shunt_group)
fi
for shunt_id in $shunt_ids; do for shunt_id in $shunt_ids; do
[ "${shunt_group}" != "$(config_n_get ${shunt_id} group)" ] && continue
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | grep -v "^#" | sed -e "/^$/d" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | sed -e "s/^/add $IPSET_SHUNT &/g" -e "s/$/ timeout 0/g" | ipset -! -R config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | grep -v "^#" | sed -e "/^$/d" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | sed -e "s/^/add $IPSET_SHUNT &/g" -e "s/$/ timeout 0/g" | ipset -! -R
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | grep -v "^#" | sed -e "/^$/d" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "s/^/add $IPSET_SHUNT6 &/g" -e "s/$/ timeout 0/g" | ipset -! -R config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | grep -v "^#" | sed -e "/^$/d" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "s/^/add $IPSET_SHUNT6 &/g" -e "s/$/ timeout 0/g" | ipset -! -R
[ "$USE_GEOVIEW" = "1" ] && { [ "$USE_GEOVIEW" = "1" ] && {
@@ -1054,7 +1054,14 @@ add_firewall_rule() {
[ "$USE_SHUNT_NODE" = "1" ] && { [ "$USE_SHUNT_NODE" = "1" ] && {
local GEOIP_CODE="" local GEOIP_CODE=""
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}') local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
local shunt_group
if [ "${USE_SHUNT_TCP}" = "1" ]; then
shunt_group=$(config_n_get $_TCP_NODE shunt_group)
elif [ "${USE_SHUNT_UDP}" = "1" ]; then
shunt_group=$(config_n_get $_UDP_NODE shunt_group)
fi
for shunt_id in $shunt_ids; do for shunt_id in $shunt_ids; do
[ "${shunt_group}" != "$(config_n_get ${shunt_id} group)" ] && continue
config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT_STATIC config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT_STATIC
config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6_STATIC config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6_STATIC
[ "$USE_GEOVIEW" = "1" ] && { [ "$USE_GEOVIEW" = "1" ] && {