mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-02 14:13:56 +08:00
update 2026-07-16 04:54:23
This commit is contained in:
1 parent
6bc60cfbf2
commit
151daa8c75
42 files changed
+645
-689
No files matched your search
@@ -6,7 +6,7 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-passwall2
|
PKG_NAME:=luci-app-passwall2
|
||||||
PKG_VERSION:=26.7.12
|
PKG_VERSION:=26.7.16
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
function is_json(str) {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(str);
|
||||||
|
return typeof result === 'object' && result !== null;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_timehhmm(str) {
|
||||||
|
const match = String(str).match(/^(\d?\d):(\d\d)$/);
|
||||||
|
if (match) {
|
||||||
|
const hour = parseInt(match[1], 10);
|
||||||
|
const minute = parseInt(match[2], 10);
|
||||||
|
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof cbi_validators !== 'undefined' && cbi_validators !== null) {
|
||||||
|
cbi_validators['json'] = function() {
|
||||||
|
return is_json(this);
|
||||||
|
}
|
||||||
|
cbi_validators['timehhmm'] = function() {
|
||||||
|
return is_timehhmm(this);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
L.require('validation').then(function(validation) {
|
||||||
|
validation.types['json'] = function() {
|
||||||
|
return this.assert(is_json(this.value), _('Must be JSON text!'));
|
||||||
|
}
|
||||||
|
validation.types['timehhmm'] = function() {
|
||||||
|
return this.assert(is_timehhmm(this.value), _('valid time (hh:mm)'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -113,6 +113,8 @@ function index()
|
|||||||
|
|
||||||
--[[geoview]]
|
--[[geoview]]
|
||||||
entry({"admin", "services", appname, "geo_view"}, call("geo_view")).leaf = true
|
entry({"admin", "services", appname, "geo_view"}, call("geo_view")).leaf = true
|
||||||
|
|
||||||
|
entry({"admin", "services", appname, "fetch_certsha256"}, call("fetch_certsha256")).leaf = true
|
||||||
end
|
end
|
||||||
|
|
||||||
local function http_write_json(content)
|
local function http_write_json(content)
|
||||||
@@ -939,3 +941,17 @@ function flush_set()
|
|||||||
http.redirect(api.url("log"))
|
http.redirect(api.url("log"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function fetch_certsha256()
|
||||||
|
local id = http.formvalue("id") or ""
|
||||||
|
local address = (id ~= "") and uci:get(appname, id, "address") or ""
|
||||||
|
local port = (id ~= "") and uci:get(appname, id, "port") or 0
|
||||||
|
local sni = (id ~= "") and uci:get(appname, id, "tls_serverName") or ""
|
||||||
|
sni = (sni ~= "") and sni or address
|
||||||
|
if address == "" or port == 0 then
|
||||||
|
http_write_json_error()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local data = api.fetch_cert_sha256(address, port, sni, 5)
|
||||||
|
http_write_json(data ~= "" and { code = 1, data = data } or { code = 0 })
|
||||||
|
end
|
||||||
@@ -2,6 +2,8 @@ local api = require "luci.passwall2.api"
|
|||||||
local appname = api.appname
|
local appname = api.appname
|
||||||
local sys = api.sys
|
local sys = api.sys
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -13,7 +15,8 @@ o.rmempty = false
|
|||||||
o.default = false
|
o.default = false
|
||||||
|
|
||||||
-- [[ ACLs Settings ]]--
|
-- [[ ACLs Settings ]]--
|
||||||
s = m:section(TypedSection, "acl_rule")
|
local cfgname = "acl_rule"
|
||||||
|
s = m:section(TypedSection, cfgname)
|
||||||
s.template = "cbi/tblsection"
|
s.template = "cbi/tblsection"
|
||||||
s.sortable = true
|
s.sortable = true
|
||||||
s.anonymous = true
|
s.anonymous = true
|
||||||
@@ -65,4 +68,9 @@ i.cfgvalue = function(t, n)
|
|||||||
return v
|
return v
|
||||||
end
|
end
|
||||||
|
|
||||||
return m
|
local sortable = Template(appname .. "/cbi/sortable")
|
||||||
|
sortable.api = api
|
||||||
|
sortable.target_cfgname = cfgname
|
||||||
|
m:append(sortable)
|
||||||
|
|
||||||
|
return api.return_map(m)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
local appname = api.appname
|
local appname = api.appname
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -379,4 +381,4 @@ for k, v in pairs(nodes_table) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -2,6 +2,8 @@ local api = require "luci.passwall2.api"
|
|||||||
local com = require "luci.passwall2.com"
|
local com = require "luci.passwall2.com"
|
||||||
local appname = api.appname
|
local appname = api.appname
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -28,4 +30,4 @@ o.cfgvalue = function(t, n)
|
|||||||
return string.format('<font color="red">%s</font>', translate("if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."))
|
return string.format('<font color="red">%s</font>', translate("if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."))
|
||||||
end
|
end
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
local appname = "passwall2"
|
local appname = api.appname
|
||||||
local fs = api.fs
|
local fs = api.fs
|
||||||
local uci = api.uci
|
local uci = api.uci
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ datatypes = api.datatypes
|
|||||||
has_singbox = api.finded_com("sing-box")
|
has_singbox = api.finded_com("sing-box")
|
||||||
has_xray = api.finded_com("xray")
|
has_xray = api.finded_com("xray")
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -386,4 +388,4 @@ footer.shunt_list = api.jsonc.stringify(shunt_list)
|
|||||||
|
|
||||||
m:append(footer)
|
m:append(footer)
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -15,6 +15,8 @@ for k, e in ipairs(api.get_valid_nodes()) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -100,7 +102,8 @@ end
|
|||||||
o:depends("health_check_type", "passwall_logic")
|
o:depends("health_check_type", "passwall_logic")
|
||||||
|
|
||||||
-- [[ Balancing Settings ]]--
|
-- [[ Balancing Settings ]]--
|
||||||
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
|
local cfgname = "haproxy_config"
|
||||||
|
s = m:section(TypedSection, cfgname, translate("Node List"))
|
||||||
s.description = "<font color='red'>" ..
|
s.description = "<font color='red'>" ..
|
||||||
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "<br>" ..
|
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "<br>" ..
|
||||||
translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "</font>"
|
translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "</font>"
|
||||||
@@ -176,6 +179,11 @@ o:value(0, translate("Primary"))
|
|||||||
o:value(1, translate("Standby"))
|
o:value(1, translate("Standby"))
|
||||||
o.rmempty = false
|
o.rmempty = false
|
||||||
|
|
||||||
|
local sortable = Template(appname .. "/cbi/sortable")
|
||||||
|
sortable.api = api
|
||||||
|
sortable.target_cfgname = cfgname
|
||||||
|
m:append(sortable)
|
||||||
|
|
||||||
m:append(Template(appname .. "/haproxy/js"))
|
m:append(Template(appname .. "/haproxy/js"))
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
api = require "luci.passwall2.api"
|
api = require "luci.passwall2.api"
|
||||||
appname = api.appname
|
appname = api.appname
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname, translate("Node Config"))
|
m = Map(appname, translate("Node Config"))
|
||||||
m.redirect = api.url()
|
m.redirect = api.url()
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
@@ -94,7 +96,6 @@ local footer = Template(appname .. "/node_config/footer")
|
|||||||
footer.api = api
|
footer.api = api
|
||||||
footer.config = m.config
|
footer.config = m.config
|
||||||
footer.section = arg[1]
|
footer.section = arg[1]
|
||||||
|
|
||||||
m:append(footer)
|
m:append(footer)
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -3,6 +3,8 @@ local appname = api.appname
|
|||||||
local datatypes = api.datatypes
|
local datatypes = api.datatypes
|
||||||
local sys = api.sys
|
local sys = api.sys
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -33,4 +35,4 @@ s:append(Template(appname .. "/node_list/link_add_node"))
|
|||||||
|
|
||||||
m:append(Template(appname .. "/node_list/node_list"))
|
m:append(Template(appname .. "/node_list/node_list"))
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -38,6 +38,8 @@ if has_hysteria2 then
|
|||||||
table.insert(hysteria2_type, s)
|
table.insert(hysteria2_type, s)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -124,7 +126,8 @@ o.cfgvalue = function(self, section)
|
|||||||
translate("Manual subscription All"))
|
translate("Manual subscription All"))
|
||||||
end
|
end
|
||||||
|
|
||||||
s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
|
local cfgname = "subscribe_list"
|
||||||
|
s = m:section(TypedSection, cfgname, "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
|
||||||
s.addremove = true
|
s.addremove = true
|
||||||
s.anonymous = true
|
s.anonymous = true
|
||||||
s.sortable = true
|
s.sortable = true
|
||||||
@@ -144,7 +147,7 @@ o.validate = function(self, value, section)
|
|||||||
return nil, translate("Remark cannot be empty.")
|
return nil, translate("Remark cannot be empty.")
|
||||||
end
|
end
|
||||||
local duplicate = false
|
local duplicate = false
|
||||||
m.uci:foreach(appname, "subscribe_list", function(e)
|
m.uci:foreach(appname, cfgname, function(e)
|
||||||
if e[".name"] ~= section and e["remark"] and e["remark"]:lower() == value:lower() then
|
if e[".name"] ~= section and e["remark"] and e["remark"]:lower() == value:lower() then
|
||||||
duplicate = true
|
duplicate = true
|
||||||
return false
|
return false
|
||||||
@@ -219,6 +222,11 @@ o.cfgvalue = function(self, section)
|
|||||||
section, translate("Manual subscription"))
|
section, translate("Manual subscription"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local sortable = Template(appname .. "/cbi/sortable")
|
||||||
|
sortable.api = api
|
||||||
|
sortable.target_cfgname = cfgname
|
||||||
|
m:append(sortable)
|
||||||
|
|
||||||
m:append(Template(appname .. "/node_subscribe/js"))
|
m:append(Template(appname .. "/node_subscribe/js"))
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
local appname = api.appname
|
local appname = api.appname
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
m.redirect = api.url("node_subscribe")
|
m.redirect = api.url("node_subscribe")
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
@@ -255,13 +257,7 @@ for t = 0, 23 do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
o.default = "0:00"
|
o.default = "0:00"
|
||||||
o.validate = function(self, value)
|
o.datatype = "timehhmm"
|
||||||
local b = api.is_timehhmm(value)
|
|
||||||
if b then
|
|
||||||
return value
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
o:depends("update_week_mode", "0")
|
o:depends("update_week_mode", "0")
|
||||||
o:depends("update_week_mode", "1")
|
o:depends("update_week_mode", "1")
|
||||||
o:depends("update_week_mode", "2")
|
o:depends("update_week_mode", "2")
|
||||||
@@ -336,4 +332,4 @@ o.description = translate("Some subscription providers may require a hardware id
|
|||||||
translate("Enabling this option will send a hashed HWID when updating subscriptions.") .. "<br>" ..
|
translate("Enabling this option will send a hashed HWID when updating subscriptions.") .. "<br>" ..
|
||||||
translate("This may reveal limited hardware information. Enable only if you trust your provider.")
|
translate("This may reveal limited hardware information. Enable only if you trust your provider.")
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -8,6 +8,8 @@ local port_validate = function(self, value, t)
|
|||||||
return value:gsub("-", ":")
|
return value:gsub("-", ":")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -53,13 +55,7 @@ for index, value in ipairs({"stop", "start", "restart"}) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
o.default = "0:00"
|
o.default = "0:00"
|
||||||
o.validate = function(self, value)
|
o.datatype = "timehhmm"
|
||||||
local b = api.is_timehhmm(value)
|
|
||||||
if b then
|
|
||||||
return value
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
o:depends(value .. "_week_mode", "0")
|
o:depends(value .. "_week_mode", "0")
|
||||||
o:depends(value .. "_week_mode", "1")
|
o:depends(value .. "_week_mode", "1")
|
||||||
o:depends(value .. "_week_mode", "2")
|
o:depends(value .. "_week_mode", "2")
|
||||||
@@ -312,4 +308,4 @@ if has_singbox then
|
|||||||
o.default = 0
|
o.default = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
local appname = api.appname
|
local appname = api.appname
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -59,13 +61,7 @@ for t = 0, 23 do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
o.default = "0:00"
|
o.default = "0:00"
|
||||||
o.validate = function(self, value)
|
o.datatype = "timehhmm"
|
||||||
local b = api.is_timehhmm(value)
|
|
||||||
if b then
|
|
||||||
return value
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
o:depends("update_week_mode", "0")
|
o:depends("update_week_mode", "0")
|
||||||
o:depends("update_week_mode", "1")
|
o:depends("update_week_mode", "1")
|
||||||
o:depends("update_week_mode", "2")
|
o:depends("update_week_mode", "2")
|
||||||
@@ -92,7 +88,8 @@ end
|
|||||||
|
|
||||||
s:append(Template(appname .. "/rule/rule_version"))
|
s:append(Template(appname .. "/rule/rule_version"))
|
||||||
|
|
||||||
s = m:section(TypedSection, "shunt_rules", "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>")
|
local cfgname = "shunt_rules"
|
||||||
|
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>")
|
||||||
s.template = "cbi/tblsection"
|
s.template = "cbi/tblsection"
|
||||||
s.anonymous = false
|
s.anonymous = false
|
||||||
s.addremove = true
|
s.addremove = true
|
||||||
@@ -113,4 +110,9 @@ end
|
|||||||
|
|
||||||
o = s:option(DummyValue, "remarks", translate("Remarks"))
|
o = s:option(DummyValue, "remarks", translate("Remarks"))
|
||||||
|
|
||||||
return m
|
local sortable = Template(appname .. "/cbi/sortable")
|
||||||
|
sortable.api = api
|
||||||
|
sortable.target_cfgname = cfgname
|
||||||
|
m:append(sortable)
|
||||||
|
|
||||||
|
return api.return_map(m)
|
||||||
@@ -2,6 +2,8 @@ local api = require "luci.passwall2.api"
|
|||||||
local appname = api.appname
|
local appname = api.appname
|
||||||
local datatypes = api.datatypes
|
local datatypes = api.datatypes
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule"))
|
m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule"))
|
||||||
m.redirect = api.url("rule")
|
m.redirect = api.url("rule")
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
@@ -248,4 +250,4 @@ ip_list.description = "<br /><ul>"
|
|||||||
|
|
||||||
o = s:option(Flag, "invert", "invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
|
o = s:option(Flag, "invert", "invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
local appname = api.appname
|
local appname = api.appname
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map(appname)
|
m = Map(appname)
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
|
|
||||||
@@ -80,23 +82,14 @@ o = s:option(Flag, "enable_autoswitch", translate("Auto Switch"))
|
|||||||
o.default = 0
|
o.default = 0
|
||||||
o.rmempty = false
|
o.rmempty = false
|
||||||
|
|
||||||
o = s:option(Value, "autoswitch_testing_time", translate("How often to test"), translate("Units:seconds"))
|
o = s:option(ListValue, "backup_node_add_mode", translate("Backup Node Addition Method"))
|
||||||
o.datatype = "min(10)"
|
|
||||||
o.default = 30
|
|
||||||
o:depends("enable_autoswitch", true)
|
o:depends("enable_autoswitch", true)
|
||||||
|
o.default = "manual"
|
||||||
|
o:value("manual", translate("Manual"))
|
||||||
|
o:value("batch", translate("Batch"))
|
||||||
|
|
||||||
o = s:option(Value, "autoswitch_connect_timeout", translate("Timeout seconds"), translate("Units:seconds"))
|
|
||||||
o.datatype = "min(1)"
|
|
||||||
o.default = 3
|
|
||||||
o:depends("enable_autoswitch", true)
|
|
||||||
|
|
||||||
o = s:option(Value, "autoswitch_retry_num", translate("Timeout retry num"))
|
|
||||||
o.datatype = "min(1)"
|
|
||||||
o.default = 1
|
|
||||||
o:depends("enable_autoswitch", true)
|
|
||||||
|
|
||||||
o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes"))
|
o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes"))
|
||||||
o:depends("enable_autoswitch", true)
|
o:depends("backup_node_add_mode", "manual")
|
||||||
o.template = appname .. "/cbi/nodes_dynamiclist"
|
o.template = appname .. "/cbi/nodes_dynamiclist"
|
||||||
o.group = {}
|
o.group = {}
|
||||||
o.write = function(self, section, value)
|
o.write = function(self, section, value)
|
||||||
@@ -118,6 +111,44 @@ for i, v in pairs(nodes_table) do
|
|||||||
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
o = s:option(MultiValue, "backup_node_group", translate("Select Group"))
|
||||||
|
o:depends("backup_node_add_mode", "batch")
|
||||||
|
o.widget = "checkbox"
|
||||||
|
o:value("default", translate("default"))
|
||||||
|
local groups = {}
|
||||||
|
m.uci:foreach(appname, "nodes", function(s)
|
||||||
|
if s.group and s.group ~= "" then
|
||||||
|
groups[s.group] = true
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
for k, v in pairs(groups) do
|
||||||
|
o:value(api.UrlEncode(k), k)
|
||||||
|
end
|
||||||
|
|
||||||
|
o = s:option(Value, "backup_node_match_rule", translate("Node Matching Rules"))
|
||||||
|
o:depends("backup_node_add_mode", "batch")
|
||||||
|
local descrStr = "Example: <code>^A && B && !C && D$</code><br>"
|
||||||
|
descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).<br>"
|
||||||
|
descrStr = descrStr .. "Conditions are joined by <code>&&</code> (AND), and their order does not affect the result.<br>"
|
||||||
|
descrStr = descrStr .. "Multiple groups can be separated by <code>||</code> (OR), matching succeeds if any group matches.<br>"
|
||||||
|
descrStr = descrStr .. "Example: <code>A && B || C && D</code> means (A AND B) OR (C AND D)."
|
||||||
|
o.description = translate(descrStr)
|
||||||
|
|
||||||
|
o = s:option(Value, "autoswitch_testing_time", translate("How often to test"), translate("Units:seconds"))
|
||||||
|
o.datatype = "min(10)"
|
||||||
|
o.default = 30
|
||||||
|
o:depends("enable_autoswitch", true)
|
||||||
|
|
||||||
|
o = s:option(Value, "autoswitch_connect_timeout", translate("Timeout seconds"), translate("Units:seconds"))
|
||||||
|
o.datatype = "min(1)"
|
||||||
|
o.default = 3
|
||||||
|
o:depends("enable_autoswitch", true)
|
||||||
|
|
||||||
|
o = s:option(Value, "autoswitch_retry_num", translate("Timeout retry num"))
|
||||||
|
o.datatype = "min(1)"
|
||||||
|
o.default = 1
|
||||||
|
o:depends("enable_autoswitch", true)
|
||||||
|
|
||||||
o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node."))
|
o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node."))
|
||||||
o:depends("enable_autoswitch", true)
|
o:depends("enable_autoswitch", true)
|
||||||
|
|
||||||
@@ -134,6 +165,6 @@ o:depends("enable_autoswitch", true)
|
|||||||
|
|
||||||
o = s:option(DummyValue, "btn")
|
o = s:option(DummyValue, "btn")
|
||||||
o.template = appname .. "/socks_auto_switch/btn"
|
o.template = appname .. "/socks_auto_switch/btn"
|
||||||
o:depends("enable_autoswitch", true)
|
o:depends("backup_node_add_mode", "manual")
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -442,22 +442,15 @@ o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain"))
|
|||||||
o:depends({ [_n("tls")] = true })
|
o:depends({ [_n("tls")] = true })
|
||||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||||
|
|
||||||
if api.compare_versions(os.date("%Y.%m.%d"), "<", "2026.6.1") then
|
o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)"))
|
||||||
o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
|
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
|
||||||
o.default = "0"
|
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||||
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
|
o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
|
||||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
|
||||||
end
|
|
||||||
|
|
||||||
if api.compare_versions(xray_version, ">=", "26.1.31") then
|
o = s:option(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
|
||||||
o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)"))
|
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
|
||||||
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
|
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
|
||||||
|
|
||||||
o = s:option(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
|
|
||||||
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
|
|
||||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
|
||||||
end
|
|
||||||
|
|
||||||
o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)"))
|
o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)"))
|
||||||
o.default = "0"
|
o.default = "0"
|
||||||
@@ -671,6 +664,14 @@ o = s:option(TextValue, _n("xhttp_extra"), " ", translate("An XHttpObject in J
|
|||||||
o:depends({ [_n("use_xhttp_extra")] = true })
|
o:depends({ [_n("use_xhttp_extra")] = true })
|
||||||
o.rows = 10
|
o.rows = 10
|
||||||
o.wrap = "off"
|
o.wrap = "off"
|
||||||
|
o.datatype = "json"
|
||||||
|
local o_validate = o.validate
|
||||||
|
o.validate = function(self, value)
|
||||||
|
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
||||||
|
local v = o_validate(self, value)
|
||||||
|
if v then return v end
|
||||||
|
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
|
||||||
|
end
|
||||||
o.custom_cfgvalue = function(self, section, value)
|
o.custom_cfgvalue = function(self, section, value)
|
||||||
local raw = m:get(section, "xhttp_extra")
|
local raw = m:get(section, "xhttp_extra")
|
||||||
if raw then
|
if raw then
|
||||||
@@ -693,14 +694,6 @@ o.custom_write = function(self, section, value)
|
|||||||
m:del(section, "download_address")
|
m:del(section, "download_address")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
o.validate = function(self, value)
|
|
||||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
|
||||||
if api.jsonc.parse(value) then
|
|
||||||
return value
|
|
||||||
else
|
|
||||||
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
o.custom_remove = function(self, section, value)
|
o.custom_remove = function(self, section, value)
|
||||||
m:del(section, "xhttp_extra")
|
m:del(section, "xhttp_extra")
|
||||||
m:del(section, "download_address")
|
m:del(section, "download_address")
|
||||||
@@ -758,6 +751,14 @@ o.rows = 10
|
|||||||
o.wrap = "off"
|
o.wrap = "off"
|
||||||
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
|
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
|
||||||
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.")
|
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.")
|
||||||
|
o.datatype = "json"
|
||||||
|
local o_validate = o.validate
|
||||||
|
o.validate = function(self, value)
|
||||||
|
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
||||||
|
local v = o_validate(self, value)
|
||||||
|
if v then return v end
|
||||||
|
return nil, "FinalMask " .. translate("Must be JSON text!")
|
||||||
|
end
|
||||||
o.custom_cfgvalue = function(self, section, value)
|
o.custom_cfgvalue = function(self, section, value)
|
||||||
local raw = m:get(section, "finalmask")
|
local raw = m:get(section, "finalmask")
|
||||||
if raw then
|
if raw then
|
||||||
@@ -767,14 +768,6 @@ end
|
|||||||
o.custom_write = function(self, section, value)
|
o.custom_write = function(self, section, value)
|
||||||
m:set(section, "finalmask", api.base64Encode(value) or "")
|
m:set(section, "finalmask", api.base64Encode(value) or "")
|
||||||
end
|
end
|
||||||
o.validate = function(self, value)
|
|
||||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
|
||||||
if api.jsonc.parse(value) then
|
|
||||||
return value
|
|
||||||
else
|
|
||||||
return nil, "FinalMask " .. translate("Must be JSON text!")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[Fast Open]]
|
--[[Fast Open]]
|
||||||
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
|
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
|
local appname = api.appname
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map("passwall2_server", translate("Server-Side"))
|
m = Map("passwall2_server", translate("Server-Side"))
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
@@ -10,7 +13,8 @@ t.addremove = false
|
|||||||
e = t:option(Flag, "enable", translate("Enable"))
|
e = t:option(Flag, "enable", translate("Enable"))
|
||||||
e.rmempty = false
|
e.rmempty = false
|
||||||
|
|
||||||
t = m:section(TypedSection, "user", translate("Users Manager"))
|
local cfgname = "user"
|
||||||
|
t = m:section(TypedSection, cfgname, translate("Users Manager"))
|
||||||
t.anonymous = true
|
t.anonymous = true
|
||||||
t.addremove = true
|
t.addremove = true
|
||||||
t.sortable = true
|
t.sortable = true
|
||||||
@@ -85,8 +89,14 @@ e = t:option(Flag, "log", translate("Log"))
|
|||||||
e.default = "1"
|
e.default = "1"
|
||||||
e.rmempty = false
|
e.rmempty = false
|
||||||
|
|
||||||
m:append(Template("passwall2/server/log"))
|
local sortable = Template(appname .. "/cbi/sortable")
|
||||||
|
sortable.api = api
|
||||||
|
sortable.appname = m.config
|
||||||
|
sortable.target_cfgname = cfgname
|
||||||
|
m:append(sortable)
|
||||||
|
|
||||||
m:append(Template("passwall2/server/users_list_status"))
|
m:append(Template(appname .. "/server/log"))
|
||||||
return m
|
|
||||||
|
|
||||||
|
m:append(Template(appname .. "/server/users_list_status"))
|
||||||
|
|
||||||
|
return api.return_map(m)
|
||||||
@@ -129,12 +129,12 @@ o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
|
|||||||
o.rows = 10
|
o.rows = 10
|
||||||
o.wrap = "off"
|
o.wrap = "off"
|
||||||
o:depends({ [_n("custom")] = true })
|
o:depends({ [_n("custom")] = true })
|
||||||
o.validate = function(self, value, t)
|
o.datatype = "json"
|
||||||
if value and api.jsonc.parse(value) then
|
local o_validate = o.validate
|
||||||
return value
|
o.validate = function(self, value)
|
||||||
else
|
local v = o_validate(self, value)
|
||||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
if v then return v end
|
||||||
end
|
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||||
end
|
end
|
||||||
o.custom_cfgvalue = function(self, section, value)
|
o.custom_cfgvalue = function(self, section, value)
|
||||||
local config_str = m:get(section, "config_str")
|
local config_str = m:get(section, "config_str")
|
||||||
|
|||||||
@@ -412,6 +412,7 @@ o = s:option(TextValue, _n("finalmask"), "FinalMask JSON")
|
|||||||
o:depends({ [_n("use_finalmask")] = true })
|
o:depends({ [_n("use_finalmask")] = true })
|
||||||
o.rows = 10
|
o.rows = 10
|
||||||
o.wrap = "off"
|
o.wrap = "off"
|
||||||
|
o.datatype = "json"
|
||||||
o.custom_cfgvalue = function(self, section, value)
|
o.custom_cfgvalue = function(self, section, value)
|
||||||
local raw = m:get(section, "finalmask")
|
local raw = m:get(section, "finalmask")
|
||||||
if raw then
|
if raw then
|
||||||
@@ -421,14 +422,6 @@ end
|
|||||||
o.custom_write = function(self, section, value)
|
o.custom_write = function(self, section, value)
|
||||||
m:set(section, "finalmask", api.base64Encode(value) or "")
|
m:set(section, "finalmask", api.base64Encode(value) or "")
|
||||||
end
|
end
|
||||||
o.validate = function(self, value)
|
|
||||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
|
||||||
if api.jsonc.parse(value) then
|
|
||||||
return value
|
|
||||||
else
|
|
||||||
return nil, "FinalMask " .. translate("Must be JSON text!")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[acceptProxyProtocol]]
|
--[[acceptProxyProtocol]]
|
||||||
o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
|
o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
|
||||||
@@ -523,16 +516,16 @@ for _, d in ipairs(netdev_list) do
|
|||||||
o:value(d.name, d.label)
|
o:value(d.name, d.label)
|
||||||
end
|
end
|
||||||
|
|
||||||
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
|
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
|
||||||
o.rows = 10
|
o.rows = 10
|
||||||
o.wrap = "off"
|
o.wrap = "off"
|
||||||
o:depends({ [_n("custom")] = true })
|
o:depends({ [_n("custom")] = true })
|
||||||
o.validate = function(self, value, t)
|
o.datatype = "json"
|
||||||
if value and api.jsonc.parse(value) then
|
local o_validate = o.validate
|
||||||
return value
|
o.validate = function(self, value)
|
||||||
else
|
local v = o_validate(self, value)
|
||||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
if v then return v end
|
||||||
end
|
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||||
end
|
end
|
||||||
o.custom_cfgvalue = function(self, section, value)
|
o.custom_cfgvalue = function(self, section, value)
|
||||||
local config_str = m:get(section, "config_str")
|
local config_str = m:get(section, "config_str")
|
||||||
|
|||||||
@@ -489,16 +489,16 @@ for _, d in ipairs(netdev_list) do
|
|||||||
o:value(d.name, d.label)
|
o:value(d.name, d.label)
|
||||||
end
|
end
|
||||||
|
|
||||||
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
|
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
|
||||||
o.rows = 10
|
o.rows = 10
|
||||||
o.wrap = "off"
|
o.wrap = "off"
|
||||||
o:depends({ [_n("custom")] = true })
|
o:depends({ [_n("custom")] = true })
|
||||||
o.validate = function(self, value, t)
|
o.datatype = "json"
|
||||||
if value and api.jsonc.parse(value) then
|
local o_validate = o.validate
|
||||||
return value
|
o.validate = function(self, value)
|
||||||
else
|
local v = o_validate(self, value)
|
||||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
if v then return v end
|
||||||
end
|
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||||
end
|
end
|
||||||
o.custom_cfgvalue = function(self, section, value)
|
o.custom_cfgvalue = function(self, section, value)
|
||||||
local config_str = m:get(section, "config_str")
|
local config_str = m:get(section, "config_str")
|
||||||
|
|||||||
@@ -49,16 +49,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
|
|||||||
o.default = "0"
|
o.default = "0"
|
||||||
o:depends({ [_n("custom")] = false })
|
o:depends({ [_n("custom")] = false })
|
||||||
|
|
||||||
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
|
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
|
||||||
o.rows = 10
|
o.rows = 10
|
||||||
o.wrap = "off"
|
o.wrap = "off"
|
||||||
o:depends({ [_n("custom")] = true })
|
o:depends({ [_n("custom")] = true })
|
||||||
o.validate = function(self, value, t)
|
o.datatype = "json"
|
||||||
if value and api.jsonc.parse(value) then
|
local o_validate = o.validate
|
||||||
return value
|
o.validate = function(self, value)
|
||||||
else
|
local v = o_validate(self, value)
|
||||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
if v then return v end
|
||||||
end
|
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||||
end
|
end
|
||||||
o.custom_cfgvalue = function(self, section, value)
|
o.custom_cfgvalue = function(self, section, value)
|
||||||
local config_str = m:get(section, "config_str")
|
local config_str = m:get(section, "config_str")
|
||||||
|
|||||||
@@ -76,16 +76,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
|
|||||||
o.default = "0"
|
o.default = "0"
|
||||||
o:depends({ [_n("custom")] = false })
|
o:depends({ [_n("custom")] = false })
|
||||||
|
|
||||||
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
|
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
|
||||||
o.rows = 10
|
o.rows = 10
|
||||||
o.wrap = "off"
|
o.wrap = "off"
|
||||||
o:depends({ [_n("custom")] = true })
|
o:depends({ [_n("custom")] = true })
|
||||||
o.validate = function(self, value, t)
|
o.datatype = "json"
|
||||||
if value and api.jsonc.parse(value) then
|
local o_validate = o.validate
|
||||||
return value
|
o.validate = function(self, value)
|
||||||
else
|
local v = o_validate(self, value)
|
||||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
if v then return v end
|
||||||
end
|
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||||
end
|
end
|
||||||
o.custom_cfgvalue = function(self, section, value)
|
o.custom_cfgvalue = function(self, section, value)
|
||||||
local config_str = m:get(section, "config_str")
|
local config_str = m:get(section, "config_str")
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ api = require "luci.passwall2.api"
|
|||||||
appname = api.appname
|
appname = api.appname
|
||||||
fs = api.fs
|
fs = api.fs
|
||||||
|
|
||||||
|
api.set_default_cbi()
|
||||||
|
|
||||||
m = Map("passwall2_server", translate("Server Config"))
|
m = Map("passwall2_server", translate("Server Config"))
|
||||||
m.redirect = api.url("server")
|
m.redirect = api.url("server")
|
||||||
api.set_apply_on_parse(m)
|
api.set_apply_on_parse(m)
|
||||||
@@ -54,4 +56,4 @@ footer.config = m.config
|
|||||||
footer.section = arg[1]
|
footer.section = arg[1]
|
||||||
m:append(footer)
|
m:append(footer)
|
||||||
|
|
||||||
return m
|
return api.return_map(m)
|
||||||
@@ -367,8 +367,16 @@ function strToTable(str)
|
|||||||
return loadstring("return " .. str)()
|
return loadstring("return " .. str)()
|
||||||
end
|
end
|
||||||
|
|
||||||
function is_timehhmm(timeStr)
|
function is_json(str)
|
||||||
local hour, minute = string.match(timeStr, "^(%d?%d):(%d%d)$")
|
if str and jsonc.parse(str) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
datatypes.json = is_json
|
||||||
|
|
||||||
|
function is_timehhmm(str)
|
||||||
|
local hour, minute = string.match(str, "^(%d?%d):(%d%d)$")
|
||||||
if hour and minute then
|
if hour and minute then
|
||||||
hour = tonumber(hour)
|
hour = tonumber(hour)
|
||||||
minute = tonumber(minute)
|
minute = tonumber(minute)
|
||||||
@@ -378,6 +386,7 @@ function is_timehhmm(timeStr)
|
|||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
datatypes.timehhmm = is_timehhmm
|
||||||
|
|
||||||
function is_normal_node(e)
|
function is_normal_node(e)
|
||||||
if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface" or e.protocol == "_urltest") then
|
if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface" or e.protocol == "_urltest") then
|
||||||
@@ -1380,6 +1389,40 @@ function set_apply_on_parse(map)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function set_default_cbi()
|
||||||
|
local cbi = require "luci.cbi"
|
||||||
|
if true then
|
||||||
|
--TextValue
|
||||||
|
local TextValue = cbi.TextValue
|
||||||
|
local original_init = TextValue.__init__
|
||||||
|
function TextValue.__init__(self, ...)
|
||||||
|
original_init(self, ...)
|
||||||
|
self.template = appname .. "/cbi/tvalue"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function return_map(map)
|
||||||
|
local cbi = require "luci.cbi"
|
||||||
|
local api = require "luci.passwall2.api"
|
||||||
|
if true then
|
||||||
|
-- header
|
||||||
|
local header = cbi.Template(appname .. "/cbi/header")
|
||||||
|
header.api = api
|
||||||
|
header.config = map.config
|
||||||
|
table.insert(map.children, 1, header)
|
||||||
|
end
|
||||||
|
if true then
|
||||||
|
-- footer
|
||||||
|
local footer = cbi.Template(appname .. "/cbi/footer")
|
||||||
|
footer.api = api
|
||||||
|
footer.config = map.config
|
||||||
|
map:append(footer)
|
||||||
|
end
|
||||||
|
|
||||||
|
return map
|
||||||
|
end
|
||||||
|
|
||||||
function luci_types(id, m, s, type_name, option_prefix)
|
function luci_types(id, m, s, type_name, option_prefix)
|
||||||
local fv_type
|
local fv_type
|
||||||
local field_type = s.fields["type"]
|
local field_type = s.fields["type"]
|
||||||
@@ -1585,6 +1628,56 @@ function match_node_rule(name, rule)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local normal_nodes = {}
|
||||||
|
function get_batch_nodes(node)
|
||||||
|
if #normal_nodes == 0 then
|
||||||
|
for k, e in ipairs(get_valid_nodes()) do
|
||||||
|
if e.node_type == "normal" and (not e.chain_proxy or e.chain_proxy == "") then
|
||||||
|
normal_nodes[#normal_nodes + 1] = {
|
||||||
|
id = e[".name"],
|
||||||
|
remarks = e["remarks"],
|
||||||
|
group = e["group"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not node.node_group or node.node_group == "" then return {} end
|
||||||
|
local nodes = {}
|
||||||
|
for g in node.node_group:gmatch("%S+") do
|
||||||
|
g = UrlDecode(g)
|
||||||
|
for k, v in pairs(normal_nodes) do
|
||||||
|
local gn = (v.group and v.group ~= "") and v.group or "default"
|
||||||
|
if gn:lower() == g:lower() and match_node_rule(v.remarks, node.node_match_rule) then
|
||||||
|
nodes[#nodes + 1] = v.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nodes
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_socks_backup_nodes(id)
|
||||||
|
id = trim(id)
|
||||||
|
if id == "" then return "" end
|
||||||
|
local socks = uci:get_all(appname, id)
|
||||||
|
local nodes
|
||||||
|
if socks.backup_node_add_mode and socks.backup_node_add_mode == "batch" then
|
||||||
|
local node = {}
|
||||||
|
node.node_group = socks.backup_node_group
|
||||||
|
node.node_match_rule = socks.backup_node_match_rule
|
||||||
|
nodes = get_batch_nodes(node)
|
||||||
|
else
|
||||||
|
nodes = socks.autoswitch_backup_node
|
||||||
|
end
|
||||||
|
local backup_nodes, seen = {}, {}
|
||||||
|
for _, v in ipairs(nodes or {}) do
|
||||||
|
if v ~= socks.node and not seen[v] then
|
||||||
|
seen[v] = true
|
||||||
|
table.insert(backup_nodes, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.concat(backup_nodes, " ")
|
||||||
|
end
|
||||||
|
|
||||||
function get_core(field, candidates)
|
function get_core(field, candidates)
|
||||||
local v = uci:get(appname, "@global_subscribe[0]", field)
|
local v = uci:get(appname, "@global_subscribe[0]", field)
|
||||||
if v and v ~= "" then
|
if v and v ~= "" then
|
||||||
@@ -1610,6 +1703,26 @@ function cleanEmptyTables(t)
|
|||||||
return next(t) and t or nil
|
return next(t) and t or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function fetch_cert_sha256(host, port, sni, timeout)
|
||||||
|
if not host then return "" end
|
||||||
|
port = tonumber(port) or 443
|
||||||
|
sni = sni or host
|
||||||
|
timeout = tonumber(timeout) or 5
|
||||||
|
local cmd = string.format(
|
||||||
|
"timeout %d openssl s_client -connect %s:%d -servername %s -showcerts </dev/null 2>/dev/null " ..
|
||||||
|
"| awk 'BEGIN{c=0}/BEGIN CERT/{c++} c==1{print} /END CERT/{if(c==1)exit}' " ..
|
||||||
|
"| openssl x509 -outform der 2>/dev/null " ..
|
||||||
|
"| sha256sum 2>/dev/null",
|
||||||
|
timeout, host, port, sni
|
||||||
|
)
|
||||||
|
local out = trim(sys.exec(cmd))
|
||||||
|
local fp = out:match("^([0-9a-fA-F]+)")
|
||||||
|
if not fp or fp:lower():match("^e3b0c44298fc1c149afbf4c8996fb924") then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
return fp:upper()
|
||||||
|
end
|
||||||
|
|
||||||
function get_dnsmasq_server_domain()
|
function get_dnsmasq_server_domain()
|
||||||
local dnsmasq_server = uci:get("dhcp", "@dnsmasq[0]", "server")
|
local dnsmasq_server = uci:get("dhcp", "@dnsmasq[0]", "server")
|
||||||
local dnsmasq_server_t = {}
|
local dnsmasq_server_t = {}
|
||||||
|
|||||||
@@ -1329,33 +1329,6 @@ function gen_config(var)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
local nodes_list = {}
|
|
||||||
function get_urltest_batch_nodes(_node)
|
|
||||||
if #nodes_list == 0 then
|
|
||||||
for k, e in ipairs(api.get_valid_nodes()) do
|
|
||||||
if e.node_type == "normal" and (not e.chain_proxy or e.chain_proxy == "") then
|
|
||||||
nodes_list[#nodes_list + 1] = {
|
|
||||||
id = e[".name"],
|
|
||||||
remarks = e["remarks"],
|
|
||||||
group = e["group"]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not _node.node_group or _node.node_group == "" then return {} end
|
|
||||||
local nodes = {}
|
|
||||||
for g in _node.node_group:gmatch("%S+") do
|
|
||||||
g = api.UrlDecode(g)
|
|
||||||
for k, v in pairs(nodes_list) do
|
|
||||||
local gn = (v.group and v.group ~= "") and v.group or "default"
|
|
||||||
if gn:lower() == g:lower() and api.match_node_rule(v.remarks, _node.node_match_rule) then
|
|
||||||
nodes[#nodes + 1] = v.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
function get_node_by_id(node_id)
|
function get_node_by_id(node_id)
|
||||||
if not node_id or node_id == "" or node_id == "nil" then return nil end
|
if not node_id or node_id == "" or node_id == "nil" then return nil end
|
||||||
if node_id:find("Socks_") then
|
if node_id:find("Socks_") then
|
||||||
@@ -1377,7 +1350,7 @@ function gen_config(var)
|
|||||||
-- new urltest
|
-- new urltest
|
||||||
local ut_nodes
|
local ut_nodes
|
||||||
if _node.node_add_mode and _node.node_add_mode == "batch" then
|
if _node.node_add_mode and _node.node_add_mode == "batch" then
|
||||||
ut_nodes = get_urltest_batch_nodes(_node)
|
ut_nodes = api.get_batch_nodes(_node)
|
||||||
else
|
else
|
||||||
ut_nodes = _node.urltest_node
|
ut_nodes = _node.urltest_node
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -176,21 +176,9 @@ function gen_outbound(flag, node, tag, proxy_table)
|
|||||||
security = node.stream_security,
|
security = node.stream_security,
|
||||||
tlsSettings = (node.stream_security == "tls") and {
|
tlsSettings = (node.stream_security == "tls") and {
|
||||||
serverName = node.tls_serverName,
|
serverName = node.tls_serverName,
|
||||||
allowInsecure = (function()
|
|
||||||
if node.tls_pinSHA256 and node.tls_pinSHA256 ~= "" then return nil end
|
|
||||||
if api.compare_versions(os.date("%Y.%m.%d"), "<", "2026.6.1") and node.tls_allowInsecure == "1" then return true end
|
|
||||||
end)(),
|
|
||||||
fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
|
fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
|
||||||
pinnedPeerCertSha256 = (function()
|
pinnedPeerCertSha256 = node.tls_pinSHA256,
|
||||||
if api.compare_versions(xray_version, "<", "26.1.31") then return nil end
|
verifyPeerCertByName = node.tls_CertByName,
|
||||||
if not node.tls_pinSHA256 then return "" end
|
|
||||||
return node.tls_pinSHA256
|
|
||||||
end)(),
|
|
||||||
verifyPeerCertByName = (function()
|
|
||||||
if api.compare_versions(xray_version, "<", "26.1.31") then return nil end
|
|
||||||
if not node.tls_CertByName then return "" end
|
|
||||||
return node.tls_CertByName
|
|
||||||
end)(),
|
|
||||||
echConfigList = (node.ech == "1") and node.ech_config or nil,
|
echConfigList = (node.ech == "1") and node.ech_config or nil,
|
||||||
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
|
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
|
||||||
certificate = api.split(node.tls_certificate_pem, "\n"),
|
certificate = api.split(node.tls_certificate_pem, "\n"),
|
||||||
@@ -839,7 +827,8 @@ function gen_config_server(node)
|
|||||||
serverNames = node.reality_serverNames or {},
|
serverNames = node.reality_serverNames or {},
|
||||||
privateKey = node.reality_private_key,
|
privateKey = node.reality_private_key,
|
||||||
shortIds = node.reality_shortId or "",
|
shortIds = node.reality_shortId or "",
|
||||||
mldsa65Seed = (node.use_mldsa65Seed == "1") and node.reality_mldsa65Seed or nil
|
mldsa65Seed = (node.use_mldsa65Seed == "1") and node.reality_mldsa65Seed or nil,
|
||||||
|
minClientVer = "1.0.0"
|
||||||
} or nil
|
} or nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1032,33 +1021,6 @@ function gen_config(var)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local nodes_list = {}
|
|
||||||
function get_balancer_batch_nodes(_node)
|
|
||||||
if #nodes_list == 0 then
|
|
||||||
for k, e in ipairs(api.get_valid_nodes()) do
|
|
||||||
if e.node_type == "normal" and (not e.chain_proxy or e.chain_proxy == "") then
|
|
||||||
nodes_list[#nodes_list + 1] = {
|
|
||||||
id = e[".name"],
|
|
||||||
remarks = e["remarks"],
|
|
||||||
group = e["group"]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not _node.node_group or _node.node_group == "" then return {} end
|
|
||||||
local nodes = {}
|
|
||||||
for g in _node.node_group:gmatch("%S+") do
|
|
||||||
g = api.UrlDecode(g)
|
|
||||||
for k, v in pairs(nodes_list) do
|
|
||||||
local gn = (v.group and v.group ~= "") and v.group or "default"
|
|
||||||
if gn:lower() == g:lower() and api.match_node_rule(v.remarks, _node.node_match_rule) then
|
|
||||||
nodes[#nodes + 1] = v.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
function gen_loopback(outbound_tag, loopback_dst)
|
function gen_loopback(outbound_tag, loopback_dst)
|
||||||
if not outbound_tag or outbound_tag == "" then return nil end
|
if not outbound_tag or outbound_tag == "" then return nil end
|
||||||
local inbound_tag = loopback_dst and "lo-to-" .. loopback_dst or outbound_tag .. "-lo"
|
local inbound_tag = loopback_dst and "lo-to-" .. loopback_dst or outbound_tag .. "-lo"
|
||||||
@@ -1090,7 +1052,7 @@ function gen_config(var)
|
|||||||
-- new balancer
|
-- new balancer
|
||||||
local blc_nodes
|
local blc_nodes
|
||||||
if _node.node_add_mode and _node.node_add_mode == "batch" then
|
if _node.node_add_mode and _node.node_add_mode == "batch" then
|
||||||
blc_nodes = get_balancer_batch_nodes(_node)
|
blc_nodes = api.get_batch_nodes(_node)
|
||||||
else
|
else
|
||||||
blc_nodes = _node.balancing_node
|
blc_nodes = _node.balancing_node
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<!-- map footer -->
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<!-- map header -->
|
||||||
|
<script src="<%=resource%>/view/<%=self.api.appname%>/cbi.js"></script>
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<%
|
||||||
|
local api = self.api
|
||||||
|
local appname = self.appname or api.appname
|
||||||
|
local target_cfgname = self.target_cfgname
|
||||||
|
-%>
|
||||||
|
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
|
||||||
|
<style>
|
||||||
|
table .cbi-button-up,
|
||||||
|
table .cbi-button-down,
|
||||||
|
.td.cbi-section-actions .cbi-button-up,
|
||||||
|
.td.cbi-section-actions .cbi-button-down {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
vertical-align: middle;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging-row {
|
||||||
|
background-color: rgba(131, 191, 255, 0.7) !important;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1152px) {
|
||||||
|
#cbi-<%=appname%>-<%=target_cfgname%> .cbi-section-table-titles {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
//<![CDATA[
|
||||||
|
//Add drag-and-drop sorting
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
function initSortableForTable() {
|
||||||
|
var section = document.getElementById("cbi-<%=appname%>-<%=target_cfgname%>");
|
||||||
|
if (!section) return;
|
||||||
|
|
||||||
|
// === insert drag handle ===
|
||||||
|
var rows = section.querySelectorAll("tr");
|
||||||
|
rows.forEach(function(row) {
|
||||||
|
var btn = row.querySelector(".cbi-button-remove:last-of-type");
|
||||||
|
if (!btn) return;
|
||||||
|
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
|
||||||
|
var handle = document.createElement("span");
|
||||||
|
handle.className = "drag-handle center";
|
||||||
|
handle.title = "<%:Drag to reorder%>";
|
||||||
|
handle.innerHTML = "⠿";
|
||||||
|
btn.after(handle);
|
||||||
|
});
|
||||||
|
|
||||||
|
// === init Sortable ===
|
||||||
|
var table = section.getElementsByTagName("table")[0];
|
||||||
|
if (!table) return;
|
||||||
|
var root = table.tBodies[0] || table;
|
||||||
|
if (root._sortable_initialized) return root._sortable_instance;
|
||||||
|
root._sortable_initialized = true;
|
||||||
|
|
||||||
|
// save origin order
|
||||||
|
root._origOrder = getCurrentOrder(root);
|
||||||
|
|
||||||
|
try {
|
||||||
|
root._sortable_instance = Sortable.create(root, {
|
||||||
|
handle: ".drag-handle",
|
||||||
|
draggable: "tr.cbi-section-table-row",
|
||||||
|
animation: 150,
|
||||||
|
ghostClass: "dragging-row",
|
||||||
|
fallbackOnBody: true,
|
||||||
|
forceFallback: false,
|
||||||
|
swapThreshold: 0.65,
|
||||||
|
onEnd: function (evt) {
|
||||||
|
updateHiddenInput(root, section);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return root._sortable_instance;
|
||||||
|
} catch (e) {
|
||||||
|
root._sortable_initialized = false;
|
||||||
|
console.error("Sortable init failed:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get table current line order
|
||||||
|
function getCurrentOrder(root) {
|
||||||
|
var order = [];
|
||||||
|
var rows = root.querySelectorAll("tr.cbi-section-table-row");
|
||||||
|
rows.forEach(function (tr) {
|
||||||
|
var id = tr.id || "";
|
||||||
|
if (id.startsWith("cbi-<%=appname%>-"))
|
||||||
|
id = id.replace("cbi-<%=appname%>-", "");
|
||||||
|
order.push(id);
|
||||||
|
});
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update after drag and drop hidden input
|
||||||
|
function updateHiddenInput(root, section) {
|
||||||
|
var newOrder = getCurrentOrder(root);
|
||||||
|
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
|
||||||
|
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
|
||||||
|
if (hiddenInput) {
|
||||||
|
hiddenInput.value = changed ? newOrder.join(" ") : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide lua luci version up/down
|
||||||
|
function hideSortColumn(section) {
|
||||||
|
var table = section.getElementsByTagName("table")[0];
|
||||||
|
if(!table) return;
|
||||||
|
var rows = table.querySelectorAll("tr.cbi-section-table-row");
|
||||||
|
if(!rows.length) return;
|
||||||
|
var colCount = rows[0].children.length, sortCol = -1;
|
||||||
|
for(var col=0; col<colCount; col++){
|
||||||
|
var hasSort=false, invalid=false;
|
||||||
|
rows.forEach(function(tr){
|
||||||
|
var td=tr.children[col]; if(!td) return;
|
||||||
|
if(td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid=true;
|
||||||
|
if(td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort=true;
|
||||||
|
});
|
||||||
|
if(!invalid && hasSort){ sortCol=col; break; }
|
||||||
|
}
|
||||||
|
if(sortCol===-1) return;
|
||||||
|
table.querySelectorAll("tr").forEach(function(tr){
|
||||||
|
var c=tr.children[sortCol]; if(c) c.style.display="none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === wait TypedSection ===
|
||||||
|
(function waitStable() {
|
||||||
|
var last = 0, stable = 0;
|
||||||
|
var THRESHOLD = 5;
|
||||||
|
function tick() {
|
||||||
|
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
|
||||||
|
if (count && count === last) stable++;
|
||||||
|
else stable = 0;
|
||||||
|
|
||||||
|
last = count;
|
||||||
|
if (stable >= THRESHOLD)
|
||||||
|
setTimeout(initSortableForTable, 200);
|
||||||
|
else
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<%+cbi/valueheader%>
|
||||||
|
<textarea class="cbi-input-textarea" <% if not self.size then %> style="width: 100%"<% else %> cols="<%=self.size%>"<% end %>
|
||||||
|
data-update="change"<%=
|
||||||
|
attr("name", cbid) ..
|
||||||
|
attr("id", cbid) ..
|
||||||
|
ifattr(self.rows, "rows") ..
|
||||||
|
ifattr(self.wrap, "wrap") ..
|
||||||
|
ifattr(self.readonly, "readonly") ..
|
||||||
|
ifattr(self.datatype, "data-type", self.datatype)
|
||||||
|
%>>
|
||||||
|
<%-=pcdata(self:cfgvalue(section) or self.default)-%>
|
||||||
|
</textarea>
|
||||||
|
<%+cbi/valuefooter%>
|
||||||
@@ -2,46 +2,8 @@
|
|||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
local appname = api.appname
|
local appname = api.appname
|
||||||
-%>
|
-%>
|
||||||
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table .cbi-button-up,
|
|
||||||
table .cbi-button-down,
|
|
||||||
.td.cbi-section-actions .cbi-button-up,
|
|
||||||
.td.cbi-section-actions .cbi-button-down {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle {
|
|
||||||
vertical-align: middle;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dragging-row {
|
|
||||||
background-color: rgba(131, 191, 255, 0.7) !important;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 1152px) {
|
|
||||||
#cbi-<%=appname%>-haproxy_config .cbi-section-table-titles {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 1152px) {
|
@media screen and (min-width: 1152px) {
|
||||||
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"],
|
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"],
|
||||||
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] {
|
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] {
|
||||||
@@ -110,129 +72,5 @@ table .cbi-button-down,
|
|||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
//节点列表添加拖拽排序
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
function initSortableForTable() {
|
|
||||||
var section = document.getElementById("cbi-<%=appname%>-haproxy_config");
|
|
||||||
if (!section) return;
|
|
||||||
|
|
||||||
hideSortColumn(section);
|
|
||||||
|
|
||||||
// === 插入 drag handle ===
|
|
||||||
var rows = section.querySelectorAll("tr");
|
|
||||||
rows.forEach(function(row) {
|
|
||||||
var btn = row.querySelector(".cbi-button-remove:last-of-type");
|
|
||||||
if (!btn) return;
|
|
||||||
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
|
|
||||||
var handle = document.createElement("span");
|
|
||||||
handle.className = "drag-handle center";
|
|
||||||
handle.title = "<%:Drag to reorder%>";
|
|
||||||
handle.innerHTML = "⠿";
|
|
||||||
btn.after(handle);
|
|
||||||
});
|
|
||||||
|
|
||||||
// === 初始化 Sortable ===
|
|
||||||
var table = section.getElementsByTagName("table")[0];
|
|
||||||
if (!table) return;
|
|
||||||
var root = table.tBodies[0] || table;
|
|
||||||
if (root._sortable_initialized) return root._sortable_instance;
|
|
||||||
root._sortable_initialized = true;
|
|
||||||
|
|
||||||
// 保存原始顺序
|
|
||||||
root._origOrder = getCurrentOrder(root);
|
|
||||||
|
|
||||||
try {
|
|
||||||
root._sortable_instance = Sortable.create(root, {
|
|
||||||
handle: ".drag-handle",
|
|
||||||
draggable: "tr.cbi-section-table-row",
|
|
||||||
animation: 150,
|
|
||||||
ghostClass: "dragging-row",
|
|
||||||
fallbackOnBody: true,
|
|
||||||
forceFallback: false,
|
|
||||||
swapThreshold: 0.65,
|
|
||||||
onEnd: function (evt) {
|
|
||||||
updateHiddenInput(root, section);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return root._sortable_instance;
|
|
||||||
} catch (e) {
|
|
||||||
root._sortable_initialized = false;
|
|
||||||
console.error("Sortable init failed:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 table 当前行顺序
|
|
||||||
function getCurrentOrder(root) {
|
|
||||||
var order = [];
|
|
||||||
var rows = root.querySelectorAll("tr.cbi-section-table-row");
|
|
||||||
rows.forEach(function (tr) {
|
|
||||||
var id = tr.id || "";
|
|
||||||
if (id.startsWith("cbi-<%=appname%>-"))
|
|
||||||
id = id.replace("cbi-<%=appname%>-", "");
|
|
||||||
order.push(id);
|
|
||||||
});
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拖拽完成后更新 hidden input
|
|
||||||
function updateHiddenInput(root, section) {
|
|
||||||
var newOrder = getCurrentOrder(root);
|
|
||||||
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
|
|
||||||
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
|
|
||||||
if (hiddenInput) {
|
|
||||||
hiddenInput.value = changed ? newOrder.join(" ") : "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏18.06 up/down 列
|
|
||||||
function hideSortColumn(section) {
|
|
||||||
var table = section.querySelector("table");
|
|
||||||
if (!table) return;
|
|
||||||
var ths = Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-titles > th"));
|
|
||||||
var dataRows = table.querySelectorAll("tr.cbi-section-table-row");
|
|
||||||
if (!ths.length || !dataRows.length) return;
|
|
||||||
var sortCol = -1;
|
|
||||||
for (var i = 0; i < ths.length; i++) {
|
|
||||||
var hasSort = false, invalid = false;
|
|
||||||
dataRows.forEach(function(tr) {
|
|
||||||
var td = tr.querySelectorAll(":scope > td")[i];
|
|
||||||
if (!td) return;
|
|
||||||
if (td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid = true;
|
|
||||||
if (td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort = true;
|
|
||||||
});
|
|
||||||
if (!invalid && hasSort) { sortCol = i; break; }
|
|
||||||
}
|
|
||||||
if (sortCol === -1) return;
|
|
||||||
var rows = [table.querySelector("tr.cbi-section-table-titles")].concat(
|
|
||||||
Array.prototype.slice.call(dataRows),
|
|
||||||
Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-descr"))
|
|
||||||
);
|
|
||||||
rows.forEach(function(tr) {
|
|
||||||
var cells = Array.prototype.filter.call(tr.children, function(el) {
|
|
||||||
return el.tagName === "TH" || el.tagName === "TD";
|
|
||||||
});
|
|
||||||
if (cells[sortCol]) cells[sortCol].style.display = "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// === 等待 TypedSection 行稳定 ===
|
|
||||||
(function waitStable() {
|
|
||||||
var last = 0, stable = 0;
|
|
||||||
var THRESHOLD = 5;
|
|
||||||
function tick() {
|
|
||||||
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
|
|
||||||
if (count && count === last) stable++;
|
|
||||||
else stable = 0;
|
|
||||||
|
|
||||||
last = count;
|
|
||||||
if (stable >= THRESHOLD)
|
|
||||||
setTimeout(initSortableForTable, 200);
|
|
||||||
else
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
}
|
|
||||||
tick();
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
//]]>
|
//]]>
|
||||||
</script>
|
</script>
|
||||||
@@ -52,5 +52,29 @@ local api = self.api
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchCertSha256(el) {
|
||||||
|
if (el.getAttribute("data-loading") === "1") return;
|
||||||
|
el.setAttribute("data-loading", "1");
|
||||||
|
el.style.pointerEvents = "none";
|
||||||
|
el.style.color = "red";
|
||||||
|
XHR.get('<%=api.url("fetch_certsha256")%>', { id: node_id }, function(x, res) {
|
||||||
|
el.setAttribute("data-loading", "0");
|
||||||
|
el.style.pointerEvents = "";
|
||||||
|
el.style.color = "";
|
||||||
|
if (!x || x.status !== 200) {
|
||||||
|
alert("<%:Error%>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res || res.code !== 1 || !res.data) {
|
||||||
|
alert(res && res.msg ? res.msg : "<%:Fetch Failed%> !");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inputs = document.querySelectorAll('input[id$=".xray_tls_pinSHA256"]');
|
||||||
|
inputs.forEach(function(input) {
|
||||||
|
input.value = res.data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
//]]>
|
//]]>
|
||||||
</script>
|
</script>
|
||||||
@@ -635,11 +635,15 @@ table td, .table .td {
|
|||||||
if (cacheData && cacheData.savetime && (now - cacheData.timestamp) < cacheData.savetime) {
|
if (cacheData && cacheData.savetime && (now - cacheData.timestamp) < cacheData.savetime) {
|
||||||
const a = ping_value[i].firstElementChild;
|
const a = ping_value[i].firstElementChild;
|
||||||
let color = "red";
|
let color = "red";
|
||||||
if (cacheData.value < 100)
|
if (cacheData.value >= 0) {
|
||||||
color = "green";
|
a.innerText = cacheData.value + " ms";
|
||||||
else if (cacheData.value < 200)
|
if (cacheData.value < 100)
|
||||||
color = "#fb9a05";
|
color = "green";
|
||||||
a.innerText = cacheData.value + " ms";
|
else if (cacheData.value < 200)
|
||||||
|
color = "#fb9a05";
|
||||||
|
} else {
|
||||||
|
a.innerText = "<%:Timeout%>";
|
||||||
|
}
|
||||||
a.style.color = color;
|
a.style.color = color;
|
||||||
a.onclick = function () {
|
a.onclick = function () {
|
||||||
ping_node(cbi_id, this, auto_detection_time);
|
ping_node(cbi_id, this, auto_detection_time);
|
||||||
@@ -679,15 +683,23 @@ table td, .table .td {
|
|||||||
const cache_data = {
|
const cache_data = {
|
||||||
dom_id: strs[i],
|
dom_id: strs[i],
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
savetime: 60 * 1000,
|
savetime: 150 * 1000,
|
||||||
value: ping
|
value: ping
|
||||||
};
|
};
|
||||||
text = result.ping + " ms";
|
|
||||||
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
|
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
|
||||||
|
text = result.ping + " ms";
|
||||||
if (ping < 100)
|
if (ping < 100)
|
||||||
color = "green";
|
color = "green";
|
||||||
else if (ping < 200)
|
else if (ping < 200)
|
||||||
color = "#fb9a05";
|
color = "#fb9a05";
|
||||||
|
} else {
|
||||||
|
const cache_data = {
|
||||||
|
dom_id: strs[i],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
savetime: 60 * 1000,
|
||||||
|
value: -1
|
||||||
|
};
|
||||||
|
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
|
||||||
}
|
}
|
||||||
a.innerText = text;
|
a.innerText = text;
|
||||||
a.style.color = color;
|
a.style.color = color;
|
||||||
@@ -1101,6 +1113,15 @@ table td, .table .td {
|
|||||||
onChange("auto_detection_time", auto_detection_time);
|
onChange("auto_detection_time", auto_detection_time);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const links = document.querySelectorAll('a');
|
||||||
|
links.forEach(link => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
if (link.getAttribute('target') !== '_blank') {
|
||||||
|
ajaxAbortAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
//]]>
|
//]]>
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,46 +1,6 @@
|
|||||||
<%
|
<%
|
||||||
local api = require "luci.passwall2.api"
|
local api = require "luci.passwall2.api"
|
||||||
-%>
|
-%>
|
||||||
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
table .cbi-button-up,
|
|
||||||
table .cbi-button-down,
|
|
||||||
.td.cbi-section-actions .cbi-button-up,
|
|
||||||
.td.cbi-section-actions .cbi-button-down {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle {
|
|
||||||
vertical-align: middle;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dragging-row {
|
|
||||||
background-color: rgba(131, 191, 255, 0.7) !important;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 1152px) {
|
|
||||||
#cbi-<%=api.appname%>-subscribe_list .cbi-section-table-titles {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
//<![CDATA[
|
//<![CDATA[
|
||||||
@@ -148,129 +108,5 @@ table .cbi-button-down,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//订阅列表添加拖拽排序
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
function initSortableForTable() {
|
|
||||||
var section = document.getElementById("cbi-<%=api.appname%>-subscribe_list");
|
|
||||||
if (!section) return;
|
|
||||||
|
|
||||||
hideSortColumn(section);
|
|
||||||
|
|
||||||
// === 插入 drag handle ===
|
|
||||||
var rows = section.querySelectorAll("tr");
|
|
||||||
rows.forEach(function(row) {
|
|
||||||
var btn = row.querySelector(".cbi-button-remove:last-of-type");
|
|
||||||
if (!btn) return;
|
|
||||||
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
|
|
||||||
var handle = document.createElement("span");
|
|
||||||
handle.className = "drag-handle center";
|
|
||||||
handle.title = "<%:Drag to reorder%>";
|
|
||||||
handle.innerHTML = "⠿";
|
|
||||||
btn.after(handle);
|
|
||||||
});
|
|
||||||
|
|
||||||
// === 初始化 Sortable ===
|
|
||||||
var table = section.getElementsByTagName("table")[0];
|
|
||||||
if (!table) return;
|
|
||||||
var root = table.tBodies[0] || table;
|
|
||||||
if (root._sortable_initialized) return root._sortable_instance;
|
|
||||||
root._sortable_initialized = true;
|
|
||||||
|
|
||||||
// 保存原始顺序
|
|
||||||
root._origOrder = getCurrentOrder(root);
|
|
||||||
|
|
||||||
try {
|
|
||||||
root._sortable_instance = Sortable.create(root, {
|
|
||||||
handle: ".drag-handle",
|
|
||||||
draggable: "tr.cbi-section-table-row",
|
|
||||||
animation: 150,
|
|
||||||
ghostClass: "dragging-row",
|
|
||||||
fallbackOnBody: true,
|
|
||||||
forceFallback: false,
|
|
||||||
swapThreshold: 0.65,
|
|
||||||
onEnd: function (evt) {
|
|
||||||
updateHiddenInput(root, section);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return root._sortable_instance;
|
|
||||||
} catch (e) {
|
|
||||||
root._sortable_initialized = false;
|
|
||||||
console.error("Sortable init failed:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 table 当前行顺序
|
|
||||||
function getCurrentOrder(root) {
|
|
||||||
var order = [];
|
|
||||||
var rows = root.querySelectorAll("tr.cbi-section-table-row");
|
|
||||||
rows.forEach(function (tr) {
|
|
||||||
var id = tr.id || "";
|
|
||||||
if (id.startsWith("cbi-<%=api.appname%>-"))
|
|
||||||
id = id.replace("cbi-<%=api.appname%>-", "");
|
|
||||||
order.push(id);
|
|
||||||
});
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拖拽完成后更新 hidden input
|
|
||||||
function updateHiddenInput(root, section) {
|
|
||||||
var newOrder = getCurrentOrder(root);
|
|
||||||
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
|
|
||||||
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
|
|
||||||
if (hiddenInput) {
|
|
||||||
hiddenInput.value = changed ? newOrder.join(" ") : "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏18.06 up/down 列
|
|
||||||
function hideSortColumn(section) {
|
|
||||||
var table = section.querySelector("table");
|
|
||||||
if (!table) return;
|
|
||||||
var ths = Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-titles > th"));
|
|
||||||
var dataRows = table.querySelectorAll("tr.cbi-section-table-row");
|
|
||||||
if (!ths.length || !dataRows.length) return;
|
|
||||||
var sortCol = -1;
|
|
||||||
for (var i = 0; i < ths.length; i++) {
|
|
||||||
var hasSort = false, invalid = false;
|
|
||||||
dataRows.forEach(function(tr) {
|
|
||||||
var td = tr.querySelectorAll(":scope > td")[i];
|
|
||||||
if (!td) return;
|
|
||||||
if (td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid = true;
|
|
||||||
if (td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort = true;
|
|
||||||
});
|
|
||||||
if (!invalid && hasSort) { sortCol = i; break; }
|
|
||||||
}
|
|
||||||
if (sortCol === -1) return;
|
|
||||||
var rows = [table.querySelector("tr.cbi-section-table-titles")].concat(
|
|
||||||
Array.prototype.slice.call(dataRows),
|
|
||||||
Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-descr"))
|
|
||||||
);
|
|
||||||
rows.forEach(function(tr) {
|
|
||||||
var cells = Array.prototype.filter.call(tr.children, function(el) {
|
|
||||||
return el.tagName === "TH" || el.tagName === "TD";
|
|
||||||
});
|
|
||||||
if (cells[sortCol]) cells[sortCol].style.display = "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// === 等待 TypedSection 行稳定 ===
|
|
||||||
(function waitStable() {
|
|
||||||
var last = 0, stable = 0;
|
|
||||||
var THRESHOLD = 5;
|
|
||||||
function tick() {
|
|
||||||
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
|
|
||||||
if (count && count === last) stable++;
|
|
||||||
else stable = 0;
|
|
||||||
|
|
||||||
last = count;
|
|
||||||
if (stable >= THRESHOLD)
|
|
||||||
setTimeout(initSortableForTable, 200);
|
|
||||||
else
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
}
|
|
||||||
tick();
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
//]]>
|
//]]>
|
||||||
</script>
|
</script>
|
||||||
@@ -4,44 +4,12 @@ local fs = api.fs
|
|||||||
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
|
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
|
||||||
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
|
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
|
||||||
-%>
|
-%>
|
||||||
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table .cbi-button-up,
|
|
||||||
table .cbi-button-down,
|
|
||||||
.td.cbi-section-actions .cbi-button-up,
|
|
||||||
.td.cbi-section-actions .cbi-button-down {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.cbi-value[id$="-geoip_update"],
|
div.cbi-value[id$="-geoip_update"],
|
||||||
div.cbi-value[id$="-geosite_update"] {
|
div.cbi-value[id$="-geosite_update"] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle {
|
|
||||||
vertical-align: middle;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dragging-row {
|
|
||||||
background-color: rgba(131, 191, 255, 0.7) !important;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="cbi-value" id="_rule_div">
|
<div class="cbi-value" id="_rule_div">
|
||||||
@@ -157,118 +125,5 @@ local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//分流规则添加拖拽排序
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
function initSortableForTable() {
|
|
||||||
var section = document.getElementById("cbi-<%=api.appname%>-shunt_rules");
|
|
||||||
if (!section) return;
|
|
||||||
|
|
||||||
// === 插入 drag handle ===
|
|
||||||
var rows = section.querySelectorAll("tr");
|
|
||||||
rows.forEach(function(row) {
|
|
||||||
var btn = row.querySelector(".cbi-button-remove:last-of-type");
|
|
||||||
if (!btn) return;
|
|
||||||
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
|
|
||||||
var handle = document.createElement("span");
|
|
||||||
handle.className = "drag-handle center";
|
|
||||||
handle.title = "<%:Drag to reorder%>";
|
|
||||||
handle.innerHTML = "⠿";
|
|
||||||
btn.after(handle);
|
|
||||||
});
|
|
||||||
|
|
||||||
// === 初始化 Sortable ===
|
|
||||||
var table = section.getElementsByTagName("table")[0];
|
|
||||||
if (!table) return;
|
|
||||||
var root = table.tBodies[0] || table;
|
|
||||||
if (root._sortable_initialized) return root._sortable_instance;
|
|
||||||
root._sortable_initialized = true;
|
|
||||||
|
|
||||||
// 保存原始顺序
|
|
||||||
root._origOrder = getCurrentOrder(root);
|
|
||||||
|
|
||||||
try {
|
|
||||||
root._sortable_instance = Sortable.create(root, {
|
|
||||||
handle: ".drag-handle",
|
|
||||||
draggable: "tr.cbi-section-table-row",
|
|
||||||
animation: 150,
|
|
||||||
ghostClass: "dragging-row",
|
|
||||||
fallbackOnBody: true,
|
|
||||||
forceFallback: false,
|
|
||||||
swapThreshold: 0.65,
|
|
||||||
onEnd: function (evt) {
|
|
||||||
updateHiddenInput(root, section);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return root._sortable_instance;
|
|
||||||
} catch (e) {
|
|
||||||
root._sortable_initialized = false;
|
|
||||||
console.error("Sortable init failed:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 table 当前行顺序
|
|
||||||
function getCurrentOrder(root) {
|
|
||||||
var order = [];
|
|
||||||
var rows = root.querySelectorAll("tr.cbi-section-table-row");
|
|
||||||
rows.forEach(function (tr) {
|
|
||||||
var id = tr.id || "";
|
|
||||||
if (id.startsWith("cbi-<%=api.appname%>-"))
|
|
||||||
id = id.replace("cbi-<%=api.appname%>-", "");
|
|
||||||
order.push(id);
|
|
||||||
});
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拖拽完成后更新 hidden input
|
|
||||||
function updateHiddenInput(root, section) {
|
|
||||||
var newOrder = getCurrentOrder(root);
|
|
||||||
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
|
|
||||||
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
|
|
||||||
if (hiddenInput) {
|
|
||||||
hiddenInput.value = changed ? newOrder.join(" ") : "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏18.06 up/down 列
|
|
||||||
function hideSortColumn(section) {
|
|
||||||
var table = section.getElementsByTagName("table")[0];
|
|
||||||
if(!table) return;
|
|
||||||
var rows = table.querySelectorAll("tr.cbi-section-table-row");
|
|
||||||
if(!rows.length) return;
|
|
||||||
var colCount = rows[0].children.length, sortCol = -1;
|
|
||||||
for(var col=0; col<colCount; col++){
|
|
||||||
var hasSort=false, invalid=false;
|
|
||||||
rows.forEach(function(tr){
|
|
||||||
var td=tr.children[col]; if(!td) return;
|
|
||||||
if(td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid=true;
|
|
||||||
if(td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort=true;
|
|
||||||
});
|
|
||||||
if(!invalid && hasSort){ sortCol=col; break; }
|
|
||||||
}
|
|
||||||
if(sortCol===-1) return;
|
|
||||||
table.querySelectorAll("tr").forEach(function(tr){
|
|
||||||
var c=tr.children[sortCol]; if(c) c.style.display="none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// === 等待 TypedSection 行稳定 ===
|
|
||||||
(function waitStable() {
|
|
||||||
var last = 0, stable = 0;
|
|
||||||
var THRESHOLD = 5;
|
|
||||||
function tick() {
|
|
||||||
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
|
|
||||||
if (count && count === last) stable++;
|
|
||||||
else stable = 0;
|
|
||||||
|
|
||||||
last = count;
|
|
||||||
if (stable >= THRESHOLD)
|
|
||||||
setTimeout(initSortableForTable, 200);
|
|
||||||
else
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
}
|
|
||||||
tick();
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
//]]>
|
//]]>
|
||||||
</script>
|
</script>
|
||||||
@@ -379,6 +379,9 @@ msgstr "توزیع بار"
|
|||||||
msgid "Node Addition Method"
|
msgid "Node Addition Method"
|
||||||
msgstr "روش افزودن گره"
|
msgstr "روش افزودن گره"
|
||||||
|
|
||||||
|
msgid "Backup Node Addition Method"
|
||||||
|
msgstr "روش افزودن گره پشتیبان"
|
||||||
|
|
||||||
msgid "Manual"
|
msgid "Manual"
|
||||||
msgstr "دستی"
|
msgstr "دستی"
|
||||||
|
|
||||||
@@ -1651,6 +1654,12 @@ msgstr "گواهی TLS (PEM)"
|
|||||||
msgid "Full certificate (chain), PEM format."
|
msgid "Full certificate (chain), PEM format."
|
||||||
msgstr "گواهی کامل (زنجیرهای)، فرمت PEM."
|
msgstr "گواهی کامل (زنجیرهای)، فرمت PEM."
|
||||||
|
|
||||||
|
msgid "Fetch Manually"
|
||||||
|
msgstr "دریافت دستی"
|
||||||
|
|
||||||
|
msgid "Fetch Failed"
|
||||||
|
msgstr "واکشی ناموفق"
|
||||||
|
|
||||||
msgid "ECH Config"
|
msgid "ECH Config"
|
||||||
msgstr "پیکربندی ECH"
|
msgstr "پیکربندی ECH"
|
||||||
|
|
||||||
@@ -2400,4 +2409,7 @@ msgid "Gecko Packet Size (min)"
|
|||||||
msgstr "اندازه بسته Gecko (دقیقه)"
|
msgstr "اندازه بسته Gecko (دقیقه)"
|
||||||
|
|
||||||
msgid "Gecko Packet Size (max)"
|
msgid "Gecko Packet Size (max)"
|
||||||
msgstr "اندازه بسته Gecko (حداکثر)"
|
msgstr "اندازه بسته Gecko (حداکثر)"
|
||||||
|
|
||||||
|
msgid "valid time (hh:mm)"
|
||||||
|
msgstr "زمان معتبر (ساعت:میلیمتر)"
|
||||||
@@ -376,6 +376,9 @@ msgstr "Балансировка"
|
|||||||
msgid "Node Addition Method"
|
msgid "Node Addition Method"
|
||||||
msgstr "Способ добавления узлов"
|
msgstr "Способ добавления узлов"
|
||||||
|
|
||||||
|
msgid "Backup Node Addition Method"
|
||||||
|
msgstr "Метод добавления резервного узла"
|
||||||
|
|
||||||
msgid "Manual"
|
msgid "Manual"
|
||||||
msgstr "Вручную"
|
msgstr "Вручную"
|
||||||
|
|
||||||
@@ -1652,6 +1655,12 @@ msgstr "Сертификат TLS (PEM)"
|
|||||||
msgid "Full certificate (chain), PEM format."
|
msgid "Full certificate (chain), PEM format."
|
||||||
msgstr "Полный сертификат (цепочка), формат PEM."
|
msgstr "Полный сертификат (цепочка), формат PEM."
|
||||||
|
|
||||||
|
msgid "Fetch Manually"
|
||||||
|
msgstr "Получить вручную"
|
||||||
|
|
||||||
|
msgid "Fetch Failed"
|
||||||
|
msgstr "Не удалось получить данные."
|
||||||
|
|
||||||
msgid "ECH Config"
|
msgid "ECH Config"
|
||||||
msgstr "Конфигурация ECH"
|
msgstr "Конфигурация ECH"
|
||||||
|
|
||||||
@@ -2399,3 +2408,6 @@ msgstr "Размер упаковки Gecko (мин)"
|
|||||||
|
|
||||||
msgid "Gecko Packet Size (max)"
|
msgid "Gecko Packet Size (max)"
|
||||||
msgstr "Размер упаковки Gecko (макс)"
|
msgstr "Размер упаковки Gecko (макс)"
|
||||||
|
|
||||||
|
msgid "valid time (hh:mm)"
|
||||||
|
msgstr "Время действия (чч:мм)"
|
||||||
@@ -376,6 +376,9 @@ msgstr "负载均衡"
|
|||||||
msgid "Node Addition Method"
|
msgid "Node Addition Method"
|
||||||
msgstr "节点添加方式"
|
msgstr "节点添加方式"
|
||||||
|
|
||||||
|
msgid "Backup Node Addition Method"
|
||||||
|
msgstr "后备节点添加方式"
|
||||||
|
|
||||||
msgid "Manual"
|
msgid "Manual"
|
||||||
msgstr "手动"
|
msgstr "手动"
|
||||||
|
|
||||||
@@ -1643,6 +1646,12 @@ msgstr "TLS 证书(PEM)"
|
|||||||
msgid "Full certificate (chain), PEM format."
|
msgid "Full certificate (chain), PEM format."
|
||||||
msgstr "完整证书(链),PEM 格式。"
|
msgstr "完整证书(链),PEM 格式。"
|
||||||
|
|
||||||
|
msgid "Fetch Manually"
|
||||||
|
msgstr "手动获取"
|
||||||
|
|
||||||
|
msgid "Fetch Failed"
|
||||||
|
msgstr "获取失败"
|
||||||
|
|
||||||
msgid "ECH Config"
|
msgid "ECH Config"
|
||||||
msgstr "ECH 配置"
|
msgstr "ECH 配置"
|
||||||
|
|
||||||
@@ -2387,3 +2396,6 @@ msgstr "Gecko 包大小(最小)"
|
|||||||
|
|
||||||
msgid "Gecko Packet Size (max)"
|
msgid "Gecko Packet Size (max)"
|
||||||
msgstr "Gecko 包大小(最大)"
|
msgstr "Gecko 包大小(最大)"
|
||||||
|
|
||||||
|
msgid "valid time (hh:mm)"
|
||||||
|
msgstr "有效时间(hh:mm)"
|
||||||
@@ -376,6 +376,9 @@ msgstr "負載均衡"
|
|||||||
msgid "Node Addition Method"
|
msgid "Node Addition Method"
|
||||||
msgstr "節點新增方式"
|
msgstr "節點新增方式"
|
||||||
|
|
||||||
|
msgid "Backup Node Addition Method"
|
||||||
|
msgstr "備用節點新增方式"
|
||||||
|
|
||||||
msgid "Manual"
|
msgid "Manual"
|
||||||
msgstr "手動"
|
msgstr "手動"
|
||||||
|
|
||||||
@@ -1643,6 +1646,12 @@ msgstr "TLS 憑證(PEM)"
|
|||||||
msgid "Full certificate (chain), PEM format."
|
msgid "Full certificate (chain), PEM format."
|
||||||
msgstr "完整憑證(鏈),PEM 格式。"
|
msgstr "完整憑證(鏈),PEM 格式。"
|
||||||
|
|
||||||
|
msgid "Fetch Manually"
|
||||||
|
msgstr "手動獲取"
|
||||||
|
|
||||||
|
msgid "Fetch Failed"
|
||||||
|
msgstr "獲取失敗"
|
||||||
|
|
||||||
msgid "ECH Config"
|
msgid "ECH Config"
|
||||||
msgstr "ECH 配置"
|
msgstr "ECH 配置"
|
||||||
|
|
||||||
@@ -2393,3 +2402,6 @@ msgstr "Gecko 包大小(最小)"
|
|||||||
|
|
||||||
msgid "Gecko Packet Size (max)"
|
msgid "Gecko Packet Size (max)"
|
||||||
msgstr "Gecko 包大小(最大)"
|
msgstr "Gecko 包大小(最大)"
|
||||||
|
|
||||||
|
msgid "valid time (hh:mm)"
|
||||||
|
msgstr "有效時間(hh:mm)"
|
||||||
@@ -166,9 +166,8 @@ start() {
|
|||||||
retry_num=$(config_n_get $id autoswitch_retry_num 1)
|
retry_num=$(config_n_get $id autoswitch_retry_num 1)
|
||||||
restore_switch=$(config_n_get $id autoswitch_restore_switch 0)
|
restore_switch=$(config_n_get $id autoswitch_restore_switch 0)
|
||||||
probe_url=$(config_n_get $id autoswitch_probe_url "https://www.google.com/generate_204")
|
probe_url=$(config_n_get $id autoswitch_probe_url "https://www.google.com/generate_204")
|
||||||
backup_node=$(config_n_get $id autoswitch_backup_node)
|
backup_node=$(lua_api "get_socks_backup_nodes(\"${id}\")")
|
||||||
if [ -n "$backup_node" ]; then
|
if [ -n "$backup_node" ]; then
|
||||||
backup_node=$(echo "$backup_node" | tr -s ' ' '\n' | uniq | tr -s '\n' ' ')
|
|
||||||
backup_node_num=$(printf "%s\n" "$backup_node" | wc -w)
|
backup_node_num=$(printf "%s\n" "$backup_node" | wc -w)
|
||||||
if [ "$backup_node_num" -eq 1 ]; then
|
if [ "$backup_node_num" -eq 1 ]; then
|
||||||
[ "$main_node" = "$backup_node" ] && return
|
[ "$main_node" = "$backup_node" ] && return
|
||||||
|
|||||||
Reference in new issue
Block a user