mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
🏅 Sync 2026-08-13 09:29:33
This commit is contained in:
@@ -151,6 +151,16 @@ return L.view.extend({
|
||||
o.value(0, _("In-kernel path manager"));
|
||||
o.value(1, _("Userspace path manager"));
|
||||
o.default = 0;
|
||||
// LuCI's CBI removes an option instead of writing it whenever the
|
||||
// submitted value equals .default and rmempty is left at its class
|
||||
// default of true (see issue #4348 for the same pattern). That would
|
||||
// silently delete network.globals.mptcp_pm_type on every Save & Apply
|
||||
// of this page (not just when this field itself is touched), and the
|
||||
// init script's own fallback for an absent value is "1" (userspace
|
||||
// path manager) -- the opposite of this field's default -- which
|
||||
// disables in-kernel fullmesh subflow creation entirely and breaks
|
||||
// multi-WAN bonding down to the master WAN only (issue #4349).
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, "mptcp_disable_initial_config", _("Initial MPTCP configuration"));
|
||||
o.depends("mptcp_pm_type","1");
|
||||
@@ -283,6 +293,13 @@ return L.view.extend({
|
||||
o.value("backup", _("backup"));
|
||||
//o.value("handover", _("handover"));
|
||||
o.default = "off";
|
||||
// Same rmempty/default collision as mptcp_pm_type above: without this,
|
||||
// every interface currently set to "off" has network.<iface>.multipath
|
||||
// silently deleted on each Save & Apply of this page. Currently harmless
|
||||
// (the init script's own fallback for an absent value is also "off"),
|
||||
// but keep the persisted value explicit so a future fallback change
|
||||
// can't quietly flip behavior for these interfaces too.
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, "multipath_weight", _("Weight"), _("Only used by *weight schedulers/path managers. Ignored if no weight scheduler is selected.") + '<br />' + _("A weight >100 make it more attractive, a weight <100 make it less attractive. Max 256"));
|
||||
o.datatype = "uinteger";
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.8.10
|
||||
PKG_RELEASE:=78
|
||||
PKG_RELEASE:=79
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -20,19 +20,31 @@ function is_timehhmm(str) {
|
||||
}
|
||||
|
||||
if (typeof cbi_validators !== 'undefined' && cbi_validators !== null) {
|
||||
cbi_validators['base64'] = function() {
|
||||
return isBase64(this);
|
||||
}
|
||||
cbi_validators['json'] = function() {
|
||||
return is_json(this);
|
||||
}
|
||||
cbi_validators['timehhmm'] = function() {
|
||||
return is_timehhmm(this);
|
||||
}
|
||||
cbi_validators['uuid'] = function() {
|
||||
return isUUID(this);
|
||||
}
|
||||
} else {
|
||||
L.require('validation').then(function(validation) {
|
||||
validation.types['base64'] = function() {
|
||||
return this.assert(isBase64(this.value), _('Must be Base64 text!'));
|
||||
}
|
||||
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)'));
|
||||
}
|
||||
validation.types['uuid'] = function() {
|
||||
return this.assert(isUUID(this.value), _('Must be UUID format!'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ function arraysEqual(a, b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isBase64(str) {
|
||||
const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
|
||||
return base64Regex.test(str);
|
||||
}
|
||||
|
||||
function decodeIfBase64(str) {
|
||||
try {
|
||||
let s = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
@@ -24,6 +29,11 @@ function decodeIfBase64(str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
function isUUID(str) {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(str);
|
||||
}
|
||||
|
||||
function waitForElement(selector, callback) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) return callback(el);
|
||||
|
||||
@@ -56,12 +56,13 @@ function index()
|
||||
|
||||
--[[ Server ]]
|
||||
entry({"admin", "services", appname, "server"}, cbi(appname .. "/server/index"), _("Server-Side"), 99).leaf = true
|
||||
entry({"admin", "services", appname, "server_user"}, cbi(appname .. "/server/user")).leaf = true
|
||||
entry({"admin", "services", appname, "server_config"}, cbi(appname .. "/server/server_config")).leaf = true
|
||||
entry({"admin", "services", appname, "server_user_config"}, cbi(appname .. "/server/user_config")).leaf = true
|
||||
|
||||
--[[ API ]]
|
||||
entry({"admin", "services", appname, "server_update_config"}, call("server_update_config")).leaf = true
|
||||
entry({"admin", "services", appname, "server_user_status"}, call("server_user_status")).leaf = true
|
||||
entry({"admin", "services", appname, "server_user_log"}, call("server_user_log")).leaf = true
|
||||
entry({"admin", "services", appname, "server_status"}, call("server_status")).leaf = true
|
||||
entry({"admin", "services", appname, "server_log"}, call("server_log")).leaf = true
|
||||
entry({"admin", "services", appname, "server_get_log"}, call("server_get_log")).leaf = true
|
||||
entry({"admin", "services", appname, "server_clear_log"}, call("server_clear_log")).leaf = true
|
||||
entry({"admin", "services", appname, "link_add_node"}, call("link_add_node")).leaf = true
|
||||
@@ -88,7 +89,6 @@ function index()
|
||||
entry({"admin", "services", appname, "delete_select_nodes"}, call("delete_select_nodes")).leaf = true
|
||||
entry({"admin", "services", appname, "reassign_group"}, call("reassign_group")).leaf = true
|
||||
entry({"admin", "services", appname, "get_node"}, call("get_node")).leaf = true
|
||||
entry({"admin", "services", appname, "save_node_order"}, call("save_node_order")).leaf = true
|
||||
entry({"admin", "services", appname, "save_node_list_opt"}, call("save_node_list_opt")).leaf = true
|
||||
entry({"admin", "services", appname, "update_rules"}, call("update_rules")).leaf = true
|
||||
entry({"admin", "services", appname, "rollback_rules"}, call("rollback_rules")).leaf = true
|
||||
@@ -100,7 +100,6 @@ function index()
|
||||
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
|
||||
|
||||
--[[Components update]]
|
||||
entry({"admin", "services", appname, "check_passwall2"}, call("app_check")).leaf = true
|
||||
@@ -649,19 +648,6 @@ function get_node()
|
||||
http_write_json(result)
|
||||
end
|
||||
|
||||
function save_node_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
|
||||
|
||||
function reassign_group()
|
||||
local ids = http.formvalue("ids") or ""
|
||||
local group = http.formvalue("group") or "default"
|
||||
@@ -725,14 +711,14 @@ function server_update_config()
|
||||
http_write_json_error()
|
||||
end
|
||||
|
||||
function server_user_status()
|
||||
function server_status()
|
||||
local e = {}
|
||||
e.index = http.formvalue("index")
|
||||
e.status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep -i '%s' >/dev/null", appname .. "_server", http.formvalue("id"))) == 0
|
||||
http_write_json(e)
|
||||
end
|
||||
|
||||
function server_user_log()
|
||||
function server_log()
|
||||
local id = http.formvalue("id")
|
||||
if nixio.fs.access("/tmp/etc/passwall2_server/" .. id .. ".log") then
|
||||
local content = luci.sys.exec("cat /tmp/etc/passwall2_server/" .. id .. ".log")
|
||||
@@ -1059,7 +1045,7 @@ function add_shunt_rule()
|
||||
if add_name then
|
||||
local has = uci:get(appname, uid)
|
||||
if has then
|
||||
http_write_json_error({ message = "This ID already exists." })
|
||||
http_write_json_error({ message = i18n.translate("This ID already exists.") })
|
||||
return
|
||||
end
|
||||
else
|
||||
@@ -1099,16 +1085,3 @@ function delete_select_shunt_rules()
|
||||
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
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
local api = require "luci.passwall2.api"
|
||||
local appname = api.appname
|
||||
local datatypes = api.datatypes
|
||||
local sys = api.sys
|
||||
|
||||
api.set_default_cbi()
|
||||
|
||||
@@ -30,6 +28,29 @@ o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud
|
||||
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
|
||||
o.default = o.keylist[3]
|
||||
|
||||
m:append(Template(appname .. "/node_list/node_list"))
|
||||
if true then
|
||||
local o = Template(appname .. "/node_list/node_list")
|
||||
o.map = m
|
||||
o.api = api
|
||||
m:append(o)
|
||||
|
||||
if luci.http.formvalue("cbi.submit") == "1" then
|
||||
local group_order = {}
|
||||
group_order = luci.http.formvaluetable("group.order")
|
||||
if group_order then
|
||||
for k, v in pairs(group_order) do
|
||||
if v and v~= "" then
|
||||
local new_order = {}
|
||||
string.gsub(v, "[^" .. " " .. "]+", function(w)
|
||||
new_order[#new_order + 1] = w
|
||||
end)
|
||||
for idx, name in ipairs(new_order) do
|
||||
m.uci:reorder(appname, name, idx - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return api.return_map(m)
|
||||
|
||||
@@ -88,6 +88,27 @@ end
|
||||
|
||||
s:append(Template(appname .. "/rule/rule_version"))
|
||||
|
||||
m:append(Template(appname .. "/rule/shunt_rule_list"))
|
||||
if true then
|
||||
local o = Template(appname .. "/rule/shunt_rule_list")
|
||||
o.api = api
|
||||
m:append(o)
|
||||
|
||||
if luci.http.formvalue("cbi.submit") == "1" then
|
||||
local group_order = luci.http.formvaluetable("group.order")
|
||||
if group_order then
|
||||
for k, v in pairs(group_order) do
|
||||
if v and v~= "" then
|
||||
local new_order = {}
|
||||
string.gsub(v, "[^" .. " " .. "]+", function(w)
|
||||
new_order[#new_order + 1] = w
|
||||
end)
|
||||
for idx, name in ipairs(new_order) do
|
||||
m.uci:reorder(appname, name, idx - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return api.return_map(m)
|
||||
|
||||
@@ -13,38 +13,38 @@ t.addremove = false
|
||||
e = t:option(Flag, "enable", translate("Enable"))
|
||||
e.rmempty = false
|
||||
|
||||
local cfgname = "user"
|
||||
t = m:section(TypedSection, cfgname, translate("Users Manager"))
|
||||
t.anonymous = true
|
||||
t.addremove = true
|
||||
t.sortable = true
|
||||
t.template = "cbi/tblsection"
|
||||
t.extedit = api.url("server_user", "%s")
|
||||
function t.create(e, t)
|
||||
s_server = m:section(TypedSection, "server", translate("Servers Manager"))
|
||||
s_server.anonymous = true
|
||||
s_server.addremove = true
|
||||
s_server.sortable = true
|
||||
s_server.template = "cbi/tblsection"
|
||||
s_server.extedit = api.url("server_config", "%s")
|
||||
function s_server.create(self, section)
|
||||
local uid = api.gen_random_char()
|
||||
TypedSection.create(e, uid)
|
||||
luci.http.redirect(e.extedit:format(uid))
|
||||
TypedSection.create(self, uid)
|
||||
luci.http.redirect(self.extedit:format(uid))
|
||||
end
|
||||
function t.remove(e, t)
|
||||
e.map.proceed = true
|
||||
e.map:del(t)
|
||||
luci.http.redirect(api.url("server"))
|
||||
function s_server.remove(self, section)
|
||||
local o = m:get(section) or {}
|
||||
if o[".type"] == self.sectiontype then
|
||||
m:del(section)
|
||||
end
|
||||
end
|
||||
|
||||
e = t:option(Flag, "enable", translate("Enable"))
|
||||
e = s_server:option(Flag, "enable", translate("Enable"))
|
||||
e.width = "5%"
|
||||
e.rmempty = false
|
||||
|
||||
e = t:option(DummyValue, "status", translate("Status"))
|
||||
e = s_server:option(DummyValue, "status", translate("Status"))
|
||||
e.rawhtml = true
|
||||
e.cfgvalue = function(t, n)
|
||||
return string.format('<font class="_users_status">%s</font>', translate("Collecting data..."))
|
||||
return string.format('<font class="_servers_status">%s</font>', translate("Collecting data..."))
|
||||
end
|
||||
|
||||
e = t:option(DummyValue, "remarks", translate("Remarks"))
|
||||
e = s_server:option(DummyValue, "remarks", translate("Remarks"))
|
||||
e.width = "15%"
|
||||
|
||||
e = t:option(DummyValue, "type", translate("Type"))
|
||||
e = s_server:option(DummyValue, "type", translate("Type"))
|
||||
e.width = "20%"
|
||||
e.rawhtml = true
|
||||
e.cfgvalue = function(t, n)
|
||||
@@ -82,20 +82,74 @@ e.cfgvalue = function(t, n)
|
||||
return str
|
||||
end
|
||||
|
||||
e = t:option(DummyValue, "port", translate("Port"))
|
||||
e = s_server:option(DummyValue, "port", translate("Port"))
|
||||
|
||||
e = t:option(Flag, "log", translate("Log"))
|
||||
e = s_server:option(Flag, "log", translate("Log"))
|
||||
e.default = "1"
|
||||
e.rmempty = false
|
||||
|
||||
local sortable = Template(appname .. "/cbi/sortable")
|
||||
sortable.api = api
|
||||
sortable.appname = m.config
|
||||
sortable.target_cfgname = cfgname
|
||||
sortable.target_cfgname = s_server.sectiontype
|
||||
m:append(sortable)
|
||||
|
||||
local server_list_status = Template(appname .. "/server/server_list_status")
|
||||
server_list_status.api = api
|
||||
server_list_status.appname = m.config
|
||||
server_list_status.sectiontype = s_server.sectiontype
|
||||
m:append(server_list_status)
|
||||
|
||||
s_user = m:section(TypedSection, "user", translate("Users Manager"))
|
||||
s_user.anonymous = true
|
||||
s_user.addremove = true
|
||||
s_user.sortable = true
|
||||
s_user.template = "cbi/tblsection"
|
||||
s_user.extedit = api.url("server_user_config", "%s")
|
||||
s_user.create = function(e, section)
|
||||
local uid = api.gen_random_char()
|
||||
TypedSection.create(e, uid)
|
||||
luci.http.redirect(e.extedit:format(uid))
|
||||
end
|
||||
s_user.remove = function(self, section)
|
||||
local o = m:get(section) or {}
|
||||
if o[".type"] == self.sectiontype then
|
||||
m.uci:foreach(m.config, "server", function(o)
|
||||
if o.user and section == o.user then
|
||||
m:set(o[".name"], "user", "")
|
||||
end
|
||||
local changed = false
|
||||
local users = o.users or {}
|
||||
for i = #users, 1, -1 do
|
||||
if section == users[i] then
|
||||
table.remove(users, i)
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
if changed then
|
||||
m:set(o[".name"], "users", users)
|
||||
end
|
||||
end)
|
||||
m:del(section)
|
||||
luci.http.redirect(api.url("server"))
|
||||
end
|
||||
end
|
||||
|
||||
e = s_user:option(DummyValue, "username", translate("Username"))
|
||||
e.width = "25%"
|
||||
|
||||
e = s_user:option(DummyValue, "password", translate("Password"))
|
||||
e.width = "25%"
|
||||
|
||||
e = s_user:option(DummyValue, "uuid", "UUID")
|
||||
e.width = "25%"
|
||||
|
||||
local sortable = Template(appname .. "/cbi/sortable")
|
||||
sortable.api = api
|
||||
sortable.appname = m.config
|
||||
sortable.target_cfgname = s_user.sectiontype
|
||||
m:append(sortable)
|
||||
|
||||
m:append(Template(appname .. "/server/log"))
|
||||
|
||||
m:append(Template(appname .. "/server/users_list_status"))
|
||||
|
||||
return api.return_map(m)
|
||||
|
||||
+6
-1
@@ -20,7 +20,12 @@ m:append(header)
|
||||
|
||||
m:append(Template(appname .. "/cbi/nodes_listvalue_com"))
|
||||
|
||||
s = m:section(NamedSection, arg[1], "user", "")
|
||||
user_list = {}
|
||||
m.uci:foreach(m.config, "user", function(s)
|
||||
user_list[#user_list + 1] = s
|
||||
end)
|
||||
|
||||
s = m:section(NamedSection, arg[1], "server", "")
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
@@ -26,6 +26,12 @@ o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(DynamicList, _n("users"), translate("User"))
|
||||
for i, v in ipairs(user_list) do
|
||||
o:value(v[".name"], v.username)
|
||||
end
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Flag, _n("realms"), translate("Realms"))
|
||||
o.default = "0"
|
||||
o.rewrite_option = o.option
|
||||
@@ -46,11 +52,6 @@ o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ [_n("realms")] = "1" })
|
||||
|
||||
o = s:option(Value, _n("auth_password"), translate("Auth Password"))
|
||||
o.password = true
|
||||
o.rewrite_option = o.option
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(ListValue, _n("obfs_type"), translate("Obfs Type"))
|
||||
o:value("", translate("Disable"))
|
||||
o:value("salamander")
|
||||
|
||||
@@ -48,27 +48,17 @@ o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Flag, _n("auth"), translate("Auth"))
|
||||
o.validate = function(self, value, t)
|
||||
if value and value == "1" then
|
||||
local user_v = s.fields[_n("username")] and s.fields[_n("username")]:formvalue(t) or ""
|
||||
local pass_v = s.fields[_n("password")] and s.fields[_n("password")]:formvalue(t) or ""
|
||||
if user_v == "" or pass_v == "" then
|
||||
return nil, translate("Username and Password must be used together!")
|
||||
end
|
||||
end
|
||||
return value
|
||||
o = s:option(DynamicList, _n("users"), translate("User"))
|
||||
for i, v in ipairs(user_list) do
|
||||
o:value(v[".name"], v.username)
|
||||
end
|
||||
o:depends({ [_n("protocol")] = "socks" })
|
||||
o:depends({ [_n("protocol")] = "http" })
|
||||
|
||||
o = s:option(Value, _n("username"), translate("Username"))
|
||||
o:depends({ [_n("auth")] = true })
|
||||
|
||||
o = s:option(Value, _n("password"), translate("Password"))
|
||||
o.password = true
|
||||
o:depends({ [_n("auth")] = true })
|
||||
o:depends({ [_n("protocol")] = "socks" })
|
||||
o:depends({ [_n("protocol")] = "shadowsocks" })
|
||||
o:depends({ [_n("protocol")] = "vmess" })
|
||||
o:depends({ [_n("protocol")] = "vless" })
|
||||
o:depends({ [_n("protocol")] = "trojan" })
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
|
||||
o = s:option(ListValue, _n("d_protocol"), translate("Destination protocol"))
|
||||
o:value("tcp", "TCP")
|
||||
@@ -97,6 +87,9 @@ o.rewrite_option = "method"
|
||||
for a, t in ipairs(x_ss_method_list) do o:value(t) end
|
||||
o:depends({ [_n("protocol")] = "shadowsocks" })
|
||||
|
||||
o = s:option(Value, _n("ss_password"), translate("Password"))
|
||||
o:depends({ [_n("protocol")] = "shadowsocks" })
|
||||
|
||||
o = s:option(ListValue, _n("ss_network"), translate("Transport"))
|
||||
o.default = "tcp,udp"
|
||||
o:value("tcp", "TCP")
|
||||
@@ -109,14 +102,6 @@ o.default = "1"
|
||||
o.rmempty = false
|
||||
o:depends({ [_n("protocol")] = "socks" })
|
||||
|
||||
o = s:option(DynamicList, _n("uuid"), translate("ID") .. "/" .. translate("Password"))
|
||||
for i = 1, 3 do
|
||||
o:value(api.gen_uuid())
|
||||
end
|
||||
o:depends({ [_n("protocol")] = "vmess" })
|
||||
o:depends({ [_n("protocol")] = "vless" })
|
||||
o:depends({ [_n("protocol")] = "trojan" })
|
||||
|
||||
o = s:option(ListValue, _n("flow"), translate("flow"))
|
||||
o.default = ""
|
||||
o:value("", translate("Disable"))
|
||||
@@ -141,10 +126,6 @@ o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN"))
|
||||
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
|
||||
o:depends({ [_n("hysteria2_realms")] = "1" })
|
||||
|
||||
o = s:option(Value, _n("hysteria2_auth_password"), translate("Auth Password"))
|
||||
o.password = true
|
||||
o:depends({ [_n("protocol")] = "hysteria2"})
|
||||
|
||||
o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type"))
|
||||
o:value("", translate("Disable"))
|
||||
o:value("salamander")
|
||||
|
||||
@@ -60,35 +60,27 @@ o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Flag, _n("auth"), translate("Auth"))
|
||||
o.validate = function(self, value, t)
|
||||
if value and value == "1" then
|
||||
local user_v = s.fields[_n("username")] and s.fields[_n("username")]:formvalue(t) or ""
|
||||
local pass_v = s.fields[_n("password")] and s.fields[_n("password")]:formvalue(t) or ""
|
||||
if user_v == "" or pass_v == "" then
|
||||
return nil, translate("Username and Password must be used together!")
|
||||
end
|
||||
end
|
||||
return value
|
||||
o = s:option(DynamicList, _n("users"), translate("User"))
|
||||
for i, v in ipairs(user_list) do
|
||||
o:value(v[".name"], v.username)
|
||||
end
|
||||
o:depends({ [_n("protocol")] = "mixed" })
|
||||
o:depends({ [_n("protocol")] = "socks" })
|
||||
o:depends({ [_n("protocol")] = "http" })
|
||||
|
||||
o = s:option(Value, _n("username"), translate("Username"))
|
||||
o:depends({ [_n("auth")] = true })
|
||||
o:depends({ [_n("protocol")] = "naive" })
|
||||
o:depends({ [_n("protocol")] = "anytls" })
|
||||
|
||||
o = s:option(Value, _n("password"), translate("Password"))
|
||||
o.password = true
|
||||
o:depends({ [_n("auth")] = true })
|
||||
o:depends({ [_n("protocol")] = "shadowsocks" })
|
||||
o:depends({ [_n("protocol")] = "vmess" })
|
||||
o:depends({ [_n("protocol")] = "vless" })
|
||||
o:depends({ [_n("protocol")] = "trojan" })
|
||||
o:depends({ [_n("protocol")] = "naive" })
|
||||
o:depends({ [_n("protocol")] = "hysteria" })
|
||||
o:depends({ [_n("protocol")] = "tuic" })
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
o:depends({ [_n("protocol")] = "anytls" })
|
||||
|
||||
if singbox_tags:find("with_quic") then
|
||||
o = s:option(Value, _n("hysteria_obfs"), translate("Obfs Password"))
|
||||
o:depends({ [_n("protocol")] = "hysteria" })
|
||||
|
||||
o = s:option(Value, _n("hysteria_up_mbps"), translate("Max upload Mbps"))
|
||||
o.default = "100"
|
||||
o:depends({ [_n("protocol")] = "hysteria" })
|
||||
@@ -97,20 +89,6 @@ if singbox_tags:find("with_quic") then
|
||||
o.default = "100"
|
||||
o:depends({ [_n("protocol")] = "hysteria" })
|
||||
|
||||
o = s:option(Value, _n("hysteria_obfs"), translate("Obfs Password"))
|
||||
o:depends({ [_n("protocol")] = "hysteria" })
|
||||
|
||||
o = s:option(ListValue, _n("hysteria_auth_type"), translate("Auth Type"))
|
||||
o:value("disable", translate("Disable"))
|
||||
o:value("string", translate("STRING"))
|
||||
o:value("base64", translate("BASE64"))
|
||||
o:depends({ [_n("protocol")] = "hysteria" })
|
||||
|
||||
o = s:option(Value, _n("hysteria_auth_password"), translate("Auth Password"))
|
||||
o.password = true
|
||||
o:depends({ [_n("protocol")] = "hysteria", [_n("hysteria_auth_type")] = "string"})
|
||||
o:depends({ [_n("protocol")] = "hysteria", [_n("hysteria_auth_type")] = "base64"})
|
||||
|
||||
o = s:option(Value, _n("hysteria_recv_window_conn"), translate("QUIC stream receive window"))
|
||||
o:depends({ [_n("protocol")] = "hysteria" })
|
||||
|
||||
@@ -174,10 +152,6 @@ if singbox_tags:find("with_quic") then
|
||||
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
|
||||
o:depends({ [_n("hysteria2_realms")] = "1" })
|
||||
|
||||
o = s:option(Value, _n("hysteria2_auth_password"), translate("Auth Password"))
|
||||
o.password = true
|
||||
o:depends({ [_n("protocol")] = "hysteria2"})
|
||||
|
||||
o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type"))
|
||||
o:value("", translate("Disable"))
|
||||
o:value("salamander")
|
||||
@@ -233,14 +207,8 @@ o.rewrite_option = "method"
|
||||
for a, t in ipairs(ss_method_list) do o:value(t) end
|
||||
o:depends({ [_n("protocol")] = "shadowsocks" })
|
||||
|
||||
o = s:option(DynamicList, _n("uuid"), translate("ID") .. "/" .. translate("Password"))
|
||||
for i = 1, 3 do
|
||||
o:value(api.gen_uuid())
|
||||
end
|
||||
o:depends({ [_n("protocol")] = "vmess" })
|
||||
o:depends({ [_n("protocol")] = "vless" })
|
||||
o:depends({ [_n("protocol")] = "trojan" })
|
||||
o:depends({ [_n("protocol")] = "tuic" })
|
||||
o = s:option(Value, _n("ss_password"), translate("Password"))
|
||||
o:depends({ [_n("protocol")] = "shadowsocks" })
|
||||
|
||||
o = s:option(ListValue, _n("flow"), translate("flow"))
|
||||
o.default = ""
|
||||
|
||||
@@ -32,8 +32,10 @@ o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Value, _n("password"), translate("Password"))
|
||||
o.password = true
|
||||
o = s:option(ListValue, _n("user"), translate("User"))
|
||||
for i, v in ipairs(user_list) do
|
||||
o:value(v[".name"], v.username)
|
||||
end
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(ListValue, _n("method"), translate("Encrypt Method"))
|
||||
|
||||
@@ -45,8 +45,10 @@ o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Value, _n("password"), translate("Password"))
|
||||
o.password = true
|
||||
o = s:option(ListValue, _n("user"), translate("User"))
|
||||
for i, v in ipairs(user_list) do
|
||||
o:value(v[".name"], v.username)
|
||||
end
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(ListValue, _n("method"), translate("Encrypt Method"))
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
api = require "luci.passwall2.api"
|
||||
appname = api.appname
|
||||
fs = api.fs
|
||||
|
||||
api.set_default_cbi()
|
||||
|
||||
m = Map("passwall2_server", translate("User Config"))
|
||||
m.redirect = api.url("server")
|
||||
api.set_apply_on_parse(m)
|
||||
|
||||
if not arg[1] or not m:get(arg[1]) then
|
||||
luci.http.redirect(m.redirect)
|
||||
end
|
||||
|
||||
s = m:section(NamedSection, arg[1], "user", "")
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
o = s:option(Value, "username", translate("Username"))
|
||||
o.datatype = "and(uciname,maxlength(24))"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "password", translate("Password"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "uuid", "UUID")
|
||||
o.datatype = "uuid"
|
||||
o.default = api.gen_uuid()
|
||||
o.rmempty = false
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -388,6 +388,12 @@ function is_timehhmm(str)
|
||||
end
|
||||
datatypes.timehhmm = is_timehhmm
|
||||
|
||||
function is_uuid(str)
|
||||
local pattern = "^%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$"
|
||||
return string.match(str, pattern) ~= nil
|
||||
end
|
||||
datatypes.uuid = is_uuid
|
||||
|
||||
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
|
||||
return false
|
||||
|
||||
@@ -102,78 +102,78 @@ local function start()
|
||||
nft_file:write('flush chain inet fw4 PSW2-SERVER\n')
|
||||
nft_file:write('insert rule inet fw4 input position 0 jump PSW2-SERVER comment "PSW2-SERVER"\n')
|
||||
end
|
||||
uci:foreach(CONFIG, "user", function(user)
|
||||
local id = user[".name"]
|
||||
local enable = user.enable
|
||||
uci:foreach(CONFIG, "server", function(server)
|
||||
local id = server[".name"]
|
||||
local enable = server.enable
|
||||
if enable and tonumber(enable) == 1 then
|
||||
local enable_log = user.log
|
||||
local enable_log = server.log
|
||||
local log_path = nil
|
||||
if enable_log and enable_log == "1" then
|
||||
log_path = CONFIG_PATH .. "/" .. id .. ".log"
|
||||
else
|
||||
log_path = nil
|
||||
end
|
||||
local remarks = user.remarks
|
||||
local port = tonumber(user.port)
|
||||
local remarks = server.remarks
|
||||
local port = tonumber(server.port)
|
||||
local bin
|
||||
local config = {}
|
||||
local config_file = CONFIG_PATH .. "/" .. id .. ".json"
|
||||
local udp_forward = 1
|
||||
local type = user.type or ""
|
||||
local type = server.type or ""
|
||||
if type == "SSR" then
|
||||
if user.custom == "1" and user.config_str then
|
||||
config = jsonc.parse(api.base64Decode(user.config_str))
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
else
|
||||
config = require(require_dir .. "util_shadowsocks").gen_config_server(user)
|
||||
config = require(require_dir .. "util_shadowsocks").gen_config_server(server)
|
||||
end
|
||||
local udp_param = ""
|
||||
udp_forward = tonumber(user.udp_forward) or 1
|
||||
udp_forward = tonumber(server.udp_forward) or 1
|
||||
if udp_forward == 1 then
|
||||
udp_param = "-u"
|
||||
end
|
||||
type = type:lower()
|
||||
bin = ln_run("/usr/bin/" .. type .. "-server", type .. "-server", "-c " .. config_file .. " " .. udp_param, log_path)
|
||||
elseif type == "SS-Rust" then
|
||||
if user.custom == "1" and user.config_str then
|
||||
config = jsonc.parse(api.base64Decode(user.config_str))
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
else
|
||||
config = require(require_dir .. "util_shadowsocks").gen_config_server(user)
|
||||
config = require(require_dir .. "util_shadowsocks").gen_config_server(server)
|
||||
end
|
||||
bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path)
|
||||
elseif type == "Xray" then
|
||||
if user.custom == "1" and user.config_str then
|
||||
config = jsonc.parse(api.base64Decode(user.config_str))
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
if log_path then
|
||||
if not config.log then
|
||||
config.log = {}
|
||||
end
|
||||
config.log.loglevel = user.loglevel
|
||||
config.log.loglevel = server.loglevel
|
||||
end
|
||||
else
|
||||
config = require(require_dir .. "util_xray").gen_config_server(user)
|
||||
config = require(require_dir .. "util_xray").gen_config_server(server)
|
||||
end
|
||||
bin = ln_run(api.get_app_path("xray"), "xray", "run -c " .. config_file, log_path)
|
||||
elseif type == "sing-box" then
|
||||
if user.custom == "1" and user.config_str then
|
||||
config = jsonc.parse(api.base64Decode(user.config_str))
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
if log_path then
|
||||
if not config.log then
|
||||
config.log = {}
|
||||
end
|
||||
config.log.timestamp = true
|
||||
config.log.disabled = false
|
||||
config.log.level = user.loglevel
|
||||
config.log.level = server.loglevel
|
||||
config.log.output = log_path
|
||||
end
|
||||
else
|
||||
config = require(require_dir .. "util_sing-box").gen_config_server(user)
|
||||
config = require(require_dir .. "util_sing-box").gen_config_server(server)
|
||||
end
|
||||
bin = ln_run(api.get_app_path("sing-box"), "sing-box", "run -c " .. config_file, log_path)
|
||||
elseif type == "Hysteria2" then
|
||||
if user.custom == "1" and user.config_str then
|
||||
config = jsonc.parse(api.base64Decode(user.config_str))
|
||||
if server.custom == "1" and server.config_str then
|
||||
config = jsonc.parse(api.base64Decode(server.config_str))
|
||||
else
|
||||
config = require(require_dir .. "util_hysteria2").gen_config_server(user)
|
||||
config = require(require_dir .. "util_hysteria2").gen_config_server(server)
|
||||
end
|
||||
bin = ln_run(api.get_app_path("hysteria"), "hysteria", "-c " .. config_file .. " server", log_path)
|
||||
end
|
||||
@@ -191,7 +191,7 @@ local function start()
|
||||
cmd(bin)
|
||||
end
|
||||
|
||||
local bind_local = user.bind_local or 0
|
||||
local bind_local = server.bind_local or 0
|
||||
if bind_local and tonumber(bind_local) ~= 1 and port then
|
||||
if nft_flag == "0" then
|
||||
ipt(string.format('-A PSW2-SERVER -p tcp --dport %s -m comment --comment "%s" -j ACCEPT', port, remarks))
|
||||
|
||||
@@ -4,6 +4,20 @@ local uci = api.uci
|
||||
local jsonc = api.jsonc
|
||||
|
||||
function gen_config_server(node)
|
||||
local users = node.users or {}
|
||||
local users = nil
|
||||
if node.users and #node.users > 0 then
|
||||
users = {}
|
||||
for i, v in ipairs(node.users) do
|
||||
local user = uci:get_all("passwall2_server", v) or {}
|
||||
if user[".type"] == "user" then
|
||||
users[user.username] = user.password
|
||||
end
|
||||
end
|
||||
if not next(users) then
|
||||
users = nil
|
||||
end
|
||||
end
|
||||
local config = {
|
||||
listen = (function()
|
||||
if node.hysteria2_realms and node.hysteria2_realm_url then
|
||||
@@ -25,10 +39,10 @@ function gen_config_server(node)
|
||||
password = node.hysteria2_obfs_password
|
||||
}
|
||||
} or nil,
|
||||
auth = {
|
||||
type = "password",
|
||||
password = node.hysteria2_auth_password
|
||||
},
|
||||
auth = users and {
|
||||
type = "userpass",
|
||||
userpass = users
|
||||
} or nil,
|
||||
bandwidth = (node.hysteria2_up_mbps or node.hysteria2_down_mbps) and {
|
||||
up = node.hysteria2_up_mbps and node.hysteria2_up_mbps .. " mbps" or nil,
|
||||
down = node.hysteria2_down_mbps and node.hysteria2_down_mbps .. " mbps" or nil
|
||||
|
||||
@@ -4,9 +4,14 @@ local uci = api.uci
|
||||
local jsonc = api.jsonc
|
||||
|
||||
function gen_config_server(node)
|
||||
local user = nil
|
||||
if node.user then
|
||||
user = uci:get_all("passwall2_server", node.user)
|
||||
end
|
||||
|
||||
local config = {}
|
||||
config.server_port = tonumber(node.port)
|
||||
config.password = node.password
|
||||
config.password = user and user.password or ""
|
||||
config.timeout = tonumber(node.timeout)
|
||||
config.fast_open = (node.tcp_fast_open and node.tcp_fast_open == "1") and true or false
|
||||
config.method = node.method
|
||||
|
||||
@@ -789,39 +789,71 @@ function gen_config_server(node)
|
||||
listen_port = tonumber(node.port),
|
||||
}
|
||||
|
||||
local users = node.users or {}
|
||||
local users = nil
|
||||
if node.users and #node.users > 0 then
|
||||
users = {}
|
||||
for i, v in ipairs(node.users) do
|
||||
local user = uci:get_all("passwall2_server", v) or {}
|
||||
if user[".type"] == "user" then
|
||||
local u = {}
|
||||
if node.protocol == "mixed" or node.protocol == "socks" or node.protocol == "http" or node.protocol == "naive" then
|
||||
u.username = user.username
|
||||
u.password = user.password
|
||||
end
|
||||
if node.protocol == "shadowsocks" or node.protocol == "trojan" then
|
||||
u.name = user.username
|
||||
u.password = user.password
|
||||
end
|
||||
if node.protocol == "vmess" then
|
||||
u.name = user.username
|
||||
u.uuid = user.uuid
|
||||
u.alterId = 0
|
||||
end
|
||||
if node.protocol == "vless" then
|
||||
u.name = user.username
|
||||
u.uuid = user.uuid
|
||||
u.flow = node.flow
|
||||
end
|
||||
if node.protocol == "hysteria" then
|
||||
u.name = user.username
|
||||
u.auth_str = user.password
|
||||
end
|
||||
if node.protocol == "tuic" then
|
||||
u.name = user.username
|
||||
u.password = user.password
|
||||
u.uuid = user.uuid
|
||||
end
|
||||
if node.protocol == "hysteria2" then
|
||||
u.name = user.username
|
||||
u.password = user.password
|
||||
end
|
||||
users[#users + 1] = u
|
||||
end
|
||||
end
|
||||
if #users == 0 then
|
||||
users = nil
|
||||
end
|
||||
end
|
||||
|
||||
local protocol_table = nil
|
||||
|
||||
if node.protocol == "mixed" then
|
||||
protocol_table = {
|
||||
users = (node.auth == "1") and {
|
||||
{
|
||||
username = node.username,
|
||||
password = node.password
|
||||
}
|
||||
} or nil,
|
||||
users = users,
|
||||
set_system_proxy = false
|
||||
}
|
||||
end
|
||||
|
||||
if node.protocol == "socks" then
|
||||
protocol_table = {
|
||||
users = (node.auth == "1") and {
|
||||
{
|
||||
username = node.username,
|
||||
password = node.password
|
||||
}
|
||||
} or nil
|
||||
users = users
|
||||
}
|
||||
end
|
||||
|
||||
if node.protocol == "http" then
|
||||
protocol_table = {
|
||||
users = (node.auth == "1") and {
|
||||
{
|
||||
username = node.username,
|
||||
password = node.password
|
||||
}
|
||||
} or nil,
|
||||
users = users,
|
||||
tls = (node.tls == "1") and tls or nil,
|
||||
}
|
||||
end
|
||||
@@ -829,21 +861,14 @@ function gen_config_server(node)
|
||||
if node.protocol == "shadowsocks" then
|
||||
protocol_table = {
|
||||
method = node.method,
|
||||
password = node.password,
|
||||
password = node.ss_password,
|
||||
users = users,
|
||||
multiplex = mux,
|
||||
}
|
||||
end
|
||||
|
||||
if node.protocol == "vmess" then
|
||||
if node.uuid then
|
||||
local users = {}
|
||||
for i = 1, #node.uuid do
|
||||
users[i] = {
|
||||
name = node.uuid[i],
|
||||
uuid = node.uuid[i],
|
||||
alterId = 0,
|
||||
}
|
||||
end
|
||||
if users then
|
||||
protocol_table = {
|
||||
users = users,
|
||||
tls = (node.tls == "1") and tls or nil,
|
||||
@@ -854,15 +879,7 @@ function gen_config_server(node)
|
||||
end
|
||||
|
||||
if node.protocol == "vless" then
|
||||
if node.uuid then
|
||||
local users = {}
|
||||
for i = 1, #node.uuid do
|
||||
users[i] = {
|
||||
name = node.uuid[i],
|
||||
uuid = node.uuid[i],
|
||||
flow = node.flow,
|
||||
}
|
||||
end
|
||||
if users then
|
||||
protocol_table = {
|
||||
users = users,
|
||||
tls = (node.tls == "1") and tls or nil,
|
||||
@@ -873,14 +890,7 @@ function gen_config_server(node)
|
||||
end
|
||||
|
||||
if node.protocol == "trojan" then
|
||||
if node.uuid then
|
||||
local users = {}
|
||||
for i = 1, #node.uuid do
|
||||
users[i] = {
|
||||
name = node.uuid[i],
|
||||
password = node.uuid[i],
|
||||
}
|
||||
end
|
||||
if users then
|
||||
protocol_table = {
|
||||
users = users,
|
||||
tls = (node.tls == "1") and tls or nil,
|
||||
@@ -893,15 +903,12 @@ function gen_config_server(node)
|
||||
end
|
||||
|
||||
if node.protocol == "naive" then
|
||||
protocol_table = {
|
||||
users = {
|
||||
{
|
||||
username = node.username,
|
||||
password = node.password
|
||||
}
|
||||
},
|
||||
tls = tls
|
||||
}
|
||||
if users then
|
||||
protocol_table = {
|
||||
users = users,
|
||||
tls = tls
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
if node.protocol == "hysteria" then
|
||||
@@ -911,13 +918,7 @@ function gen_config_server(node)
|
||||
up_mbps = tonumber(node.hysteria_up_mbps),
|
||||
down_mbps = tonumber(node.hysteria_down_mbps),
|
||||
obfs = node.hysteria_obfs,
|
||||
users = {
|
||||
{
|
||||
name = "user1",
|
||||
auth = (node.hysteria_auth_type == "base64") and node.hysteria_auth_password or nil,
|
||||
auth_str = (node.hysteria_auth_type == "string") and node.hysteria_auth_password or nil,
|
||||
}
|
||||
},
|
||||
users = users,
|
||||
recv_window_conn = node.hysteria_recv_window_conn and tonumber(node.hysteria_recv_window_conn) or nil, --1.14 to stream_receive_window
|
||||
recv_window_client = node.hysteria_recv_window_client and tonumber(node.hysteria_recv_window_client) or nil, --1.14 to connection_receive_window
|
||||
max_conn_client = node.hysteria_max_conn_client and tonumber(node.hysteria_max_conn_client) or nil, --1.14 to max_concurrent_streams
|
||||
@@ -936,13 +937,7 @@ function gen_config_server(node)
|
||||
return nil
|
||||
end)() or nil
|
||||
protocol_table = {
|
||||
users = {
|
||||
{
|
||||
name = "user1",
|
||||
uuid = node.uuid,
|
||||
password = node.password
|
||||
}
|
||||
},
|
||||
users = users,
|
||||
congestion_control = node.tuic_congestion_control or "cubic",
|
||||
zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1") and true or false,
|
||||
heartbeat = (tonumber(node.tuic_heartbeat) or 3) .. "s",
|
||||
@@ -972,12 +967,7 @@ function gen_config_server(node)
|
||||
end
|
||||
return o
|
||||
end)(node.hysteria2_obfs_type),
|
||||
users = {
|
||||
{
|
||||
name = "user1",
|
||||
password = node.hysteria2_auth_password or nil,
|
||||
}
|
||||
},
|
||||
users = users,
|
||||
ignore_client_bandwidth = (node.hysteria2_ignore_client_bandwidth == "1") and true or false,
|
||||
tls = tls,
|
||||
realm = node.hysteria2_realms and (function()
|
||||
@@ -997,15 +987,12 @@ function gen_config_server(node)
|
||||
end
|
||||
|
||||
if node.protocol == "anytls" then
|
||||
protocol_table = {
|
||||
users = {
|
||||
{
|
||||
name = (node.username and node.username ~= "") and node.username or "sekai",
|
||||
password = node.password
|
||||
}
|
||||
},
|
||||
tls = tls,
|
||||
}
|
||||
if users then
|
||||
protocol_table = {
|
||||
users = users,
|
||||
tls = tls,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
if node.protocol == "direct" then
|
||||
|
||||
@@ -496,17 +496,46 @@ function gen_config_server(node)
|
||||
{ protocol = "freedom", tag = "direct", settings = { finalRules = {{ action = "allow" }}}}, { protocol = "blackhole", tag = "blocked" }
|
||||
}
|
||||
|
||||
if node.protocol == "vmess" or node.protocol == "vless" then
|
||||
if node.uuid then
|
||||
local users = {}
|
||||
for i = 1, #node.uuid do
|
||||
users[i] = {
|
||||
id = node.uuid[i],
|
||||
flow = (node.protocol == "vless"
|
||||
and (node.tls == "1" or (node.decryption and node.decryption ~= "" and node.decryption ~= "none"))
|
||||
and node.flow and node.flow ~= "") and node.flow or nil
|
||||
}
|
||||
local users = node.users or {}
|
||||
local users = nil
|
||||
if node.users and #node.users > 0 then
|
||||
users = {}
|
||||
for i, v in ipairs(node.users) do
|
||||
local user = uci:get_all("passwall2_server", v) or {}
|
||||
if user[".type"] == "user" then
|
||||
local u = {}
|
||||
if node.protocol == "socks" or node.protocol == "http" then
|
||||
u.user = user.username
|
||||
u.pass = user.password
|
||||
end
|
||||
if node.protocol == "shadowsocks" or node.protocol == "trojan" then
|
||||
u.email = user.username
|
||||
u.password = user.password
|
||||
end
|
||||
if node.protocol == "vmess" then
|
||||
u.email = user.username
|
||||
u.id = user.uuid
|
||||
u.alterId = 0
|
||||
end
|
||||
if node.protocol == "vless" then
|
||||
u.email = user.username
|
||||
u.id = user.uuid
|
||||
u.flow = node.flow
|
||||
end
|
||||
if node.protocol == "hysteria2" then
|
||||
u.email = user.username
|
||||
u.auth = user.password
|
||||
end
|
||||
users[#users + 1] = u
|
||||
end
|
||||
end
|
||||
if #users == 0 then
|
||||
users = nil
|
||||
end
|
||||
end
|
||||
|
||||
if node.protocol == "vmess" or node.protocol == "vless" then
|
||||
if users then
|
||||
settings = {
|
||||
users = users,
|
||||
decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil
|
||||
@@ -515,40 +544,25 @@ function gen_config_server(node)
|
||||
elseif node.protocol == "socks" then
|
||||
settings = {
|
||||
udp = ("1" == node.udp_forward) and true or false,
|
||||
auth = ("1" == node.auth) and "password" or "noauth",
|
||||
users = ("1" == node.auth) and {
|
||||
{
|
||||
user = node.username,
|
||||
pass = node.password
|
||||
}
|
||||
} or nil
|
||||
auth = users and "password" or "noauth",
|
||||
users = users
|
||||
}
|
||||
elseif node.protocol == "http" then
|
||||
settings = {
|
||||
allowTransparent = false,
|
||||
users = ("1" == node.auth) and {
|
||||
{
|
||||
user = node.username,
|
||||
pass = node.password
|
||||
}
|
||||
} or nil
|
||||
users = users
|
||||
}
|
||||
node.transport = "tcp"
|
||||
node.tcp_guise = "none"
|
||||
elseif node.protocol == "shadowsocks" then
|
||||
settings = {
|
||||
method = node.method,
|
||||
password = node.password,
|
||||
network = node.ss_network or "TCP,UDP"
|
||||
password = node.ss_password,
|
||||
users = users,
|
||||
network = node.ss_network or "tcp,udp"
|
||||
}
|
||||
elseif node.protocol == "trojan" then
|
||||
if node.uuid then
|
||||
local users = {}
|
||||
for i = 1, #node.uuid do
|
||||
users[i] = {
|
||||
password = node.uuid[i],
|
||||
}
|
||||
end
|
||||
if users then
|
||||
settings = {
|
||||
users = users
|
||||
}
|
||||
@@ -556,9 +570,7 @@ function gen_config_server(node)
|
||||
elseif node.protocol == "hysteria2" then
|
||||
settings = {
|
||||
version = 2,
|
||||
users = node.hysteria2_auth_password and {
|
||||
{ auth = node.hysteria2_auth_password }
|
||||
}
|
||||
users = users
|
||||
}
|
||||
elseif node.protocol == "dokodemo-door" then
|
||||
settings = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%
|
||||
local api = require "luci.passwall2.api"
|
||||
local api = self.api
|
||||
local appname = api.appname
|
||||
local uci = api.uci
|
||||
local map = self.map
|
||||
|
||||
local default_node_type = ""
|
||||
local shunt_rule_list = {}
|
||||
@@ -12,7 +12,7 @@ if node then
|
||||
if (node_type == "Xray" or node_type == "sing-box") and node_protocol == "_shunt" then
|
||||
default_node_type = node_protocol
|
||||
local node_shunt_group = api.uci_get_type_id(node, "shunt_group")
|
||||
uci:foreach(appname, "shunt_rules", function(e)
|
||||
map.uci:foreach(appname, "shunt_rules", function(e)
|
||||
if e[".name"] and e.remarks and e.group == node_shunt_group then
|
||||
shunt_rule_list[#shunt_rule_list + 1] = e
|
||||
end
|
||||
@@ -517,15 +517,7 @@ table td, .table .td {
|
||||
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_node_order(group);
|
||||
if (!arraysEqual(new_order, origin_group_node_order[group])) {
|
||||
save_order_btn.style.display = null;
|
||||
} else {
|
||||
save_order_btn.style.display = "none";
|
||||
}
|
||||
}
|
||||
set_order(group);
|
||||
}
|
||||
break;
|
||||
case "del_all":
|
||||
@@ -669,48 +661,24 @@ table td, .table .td {
|
||||
return ids;
|
||||
}
|
||||
|
||||
function save_current_page_order(group) {
|
||||
var table = document.getElementById("cbi-<%=appname%>-nodes-" + group + "-table");
|
||||
function set_order(group) {
|
||||
let table = document.getElementById("cbi-<%=appname%>-nodes-" + group + "-table");
|
||||
if (!table) {
|
||||
alert("<%:No table!%>");
|
||||
return;
|
||||
}
|
||||
var rows = table.querySelectorAll("tr.cbi-section-table-row");
|
||||
let rows = table.querySelectorAll("tr.cbi-section-table-row");
|
||||
if (!rows || rows.length === 0) {
|
||||
alert("<%:No nodes!%>");
|
||||
return;
|
||||
}
|
||||
var btn = document.getElementById("save_order_btn_" + group);
|
||||
if (btn) {
|
||||
btn.style.display = "none";
|
||||
btn.disabled = true;
|
||||
}
|
||||
var ids = [];
|
||||
let ids = [];
|
||||
rows.forEach(function(row) {
|
||||
var id = row.id.replace("cbi-<%=appname%>-", "");
|
||||
let id = row.id.replace("cbi-<%=appname%>-", "");
|
||||
ids.push(id);
|
||||
});
|
||||
ajax.abortAll();
|
||||
XHR.get('<%=api.url("save_node_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_node_order[group] = get_node_order(group);
|
||||
alert("<%:Saved current page order successfully.%>");
|
||||
if (btn) {
|
||||
btn.style.display = "none";
|
||||
}
|
||||
} else {
|
||||
alert("<%:Save failed!%>");
|
||||
}
|
||||
}
|
||||
);
|
||||
const input_order = document.getElementsByName(`group.order.${group}`)[0];
|
||||
input_order.value = ids.join(" ");
|
||||
}
|
||||
|
||||
function get_now_use_node() {
|
||||
@@ -947,16 +915,7 @@ table td, .table .td {
|
||||
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_node_order(group);
|
||||
if (!arraysEqual(new_order, origin_group_node_order[group])) {
|
||||
save_order_btn.style.display = null;
|
||||
} else {
|
||||
save_order_btn.style.display = "none";
|
||||
}
|
||||
}
|
||||
set_order(group);
|
||||
}
|
||||
};
|
||||
try {
|
||||
@@ -1302,9 +1261,9 @@ table td, .table .td {
|
||||
</tr>
|
||||
{{node-tr}}
|
||||
</table>
|
||||
<input type="hidden" name="group.order.{{group}}">
|
||||
<div class="cbi-section-create cbi-tblsection-create">
|
||||
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="node_action('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>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<%
|
||||
local api = require "luci.passwall2.api"
|
||||
local api = self.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;
|
||||
@@ -182,15 +180,7 @@ table td, .table .td {
|
||||
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";
|
||||
}
|
||||
}
|
||||
set_order(group);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,47 +201,24 @@ table td, .table .td {
|
||||
return ids;
|
||||
}
|
||||
|
||||
function save_current_page_order(group) {
|
||||
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
|
||||
function set_order(group) {
|
||||
let table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
|
||||
if (!table) {
|
||||
alert("<%:No table!%>");
|
||||
return;
|
||||
}
|
||||
var rows = table.querySelectorAll("tr.cbi-section-table-row");
|
||||
let 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 = [];
|
||||
let ids = [];
|
||||
rows.forEach(function(row) {
|
||||
var id = row.id.replace("cbi-<%=appname%>-", "");
|
||||
let 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!%>");
|
||||
}
|
||||
}
|
||||
);
|
||||
const input_order = document.getElementsByName(`group.order.${group}`)[0];
|
||||
input_order.value = ids.join(" ");
|
||||
}
|
||||
|
||||
// List drag and rearrange
|
||||
@@ -270,16 +237,7 @@ table td, .table .td {
|
||||
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";
|
||||
}
|
||||
}
|
||||
set_order(group);
|
||||
}
|
||||
};
|
||||
try {
|
||||
@@ -335,10 +293,10 @@ table td, .table .td {
|
||||
</tr>
|
||||
{{shunt_rule-tr}}
|
||||
</table>
|
||||
<input type="hidden" name="group.order.{{group}}">
|
||||
<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>
|
||||
|
||||
@@ -4,7 +4,7 @@ local api = self.api
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
let node_id = "<%=self.section%>";
|
||||
let node_config_url = '<%=api.url("server_user")%>/' + node_id;
|
||||
let node_config_url = '<%=api.url("server_config")%>/' + node_id;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<%
|
||||
local api = self.api
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var _servers_status = document.getElementsByClassName('_servers_status');
|
||||
for(var i = 0; i < _servers_status.length; i++) {
|
||||
var id = _servers_status[i].parentElement.parentElement.parentElement.id;
|
||||
id = id.substr(id.lastIndexOf("-") + 1);
|
||||
XHR.get('<%=api.url("server_status")%>', {
|
||||
index: i,
|
||||
id: id
|
||||
},
|
||||
function(x, result) {
|
||||
_servers_status[result.index].setAttribute("style","font-weight:bold;");
|
||||
_servers_status[result.index].setAttribute("color",result.status ? "green":"red");
|
||||
_servers_status[result.index].innerHTML = (result.status ? '✓' : 'X');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var edit_btn = document.getElementById("cbi-<%=self.appname%>-<%=self.sectiontype%>").getElementsByClassName("cbi-button cbi-button-edit");
|
||||
for (var i = 0; i < edit_btn.length; i++) {
|
||||
try {
|
||||
var onclick_str = edit_btn[i].getAttribute("onclick");
|
||||
var id = onclick_str.substring(onclick_str.lastIndexOf('/') + 1, onclick_str.length - 1);
|
||||
var td = edit_btn[i].parentNode;
|
||||
var new_div = "";
|
||||
// Add `log` button
|
||||
new_div += '<input class="btn cbi-button cbi-button-add" type="button" value="<%:Log%>" onclick="window.open(\'' + '<%=api.url("server_log")%>' + '?id=' + id + '\', \'_blank\')"/> ';
|
||||
td.innerHTML = new_div + td.innerHTML;
|
||||
}
|
||||
catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
@@ -1,38 +0,0 @@
|
||||
<%
|
||||
local api = require "luci.passwall2.api"
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
var _users_status = document.getElementsByClassName('_users_status');
|
||||
for(var i = 0; i < _users_status.length; i++) {
|
||||
var id = _users_status[i].parentElement.parentElement.parentElement.id;
|
||||
id = id.substr(id.lastIndexOf("-") + 1);
|
||||
XHR.get('<%=api.url("server_user_status")%>', {
|
||||
index: i,
|
||||
id: id
|
||||
},
|
||||
function(x, result) {
|
||||
_users_status[result.index].setAttribute("style","font-weight:bold;");
|
||||
_users_status[result.index].setAttribute("color",result.status ? "green":"red");
|
||||
_users_status[result.index].innerHTML = (result.status ? '✓' : 'X');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var edit_btn = document.getElementById("cbi-passwall2_server-user").getElementsByClassName("cbi-button cbi-button-edit");
|
||||
for (var i = 0; i < edit_btn.length; i++) {
|
||||
try {
|
||||
var onclick_str = edit_btn[i].getAttribute("onclick");
|
||||
var id = onclick_str.substring(onclick_str.lastIndexOf('/') + 1, onclick_str.length - 1);
|
||||
var td = edit_btn[i].parentNode;
|
||||
var new_div = "";
|
||||
// Add `log` button
|
||||
new_div += '<input class="btn cbi-button cbi-button-add" type="button" value="<%:Log%>" onclick="window.open(\'' + '<%=api.url("server_user_log")%>' + '?id=' + id + '\', \'_blank\')"/> ';
|
||||
td.innerHTML = new_div + td.innerHTML;
|
||||
}
|
||||
catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
@@ -970,6 +970,9 @@ msgstr "گروه قوانین شنت"
|
||||
msgid "Please note attention to the priority, the higher the order, the higher the priority."
|
||||
msgstr "لطفاً به اولویت توجه کنید، هرچه ترتیب بالاتر باشد، اولویت بیشتر است."
|
||||
|
||||
msgid "This ID already exists."
|
||||
msgstr "این شناسه از قبل وجود دارد."
|
||||
|
||||
msgid "Update..."
|
||||
msgstr "بهروزرسانی..."
|
||||
|
||||
@@ -1486,9 +1489,15 @@ msgstr "سمت سرور"
|
||||
msgid "Server Config"
|
||||
msgstr "پیکربندی سرور"
|
||||
|
||||
msgid "Servers Manager"
|
||||
msgstr "مدیر سرورها"
|
||||
|
||||
msgid "Users Manager"
|
||||
msgstr "مدیریت کاربران"
|
||||
|
||||
msgid "User Config"
|
||||
msgstr "پیکربندی کاربر"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "گزارشها (Logs)"
|
||||
|
||||
@@ -1916,9 +1925,15 @@ msgstr "استفاده از پیکربندی سفارشی"
|
||||
msgid "Custom Config"
|
||||
msgstr "پیکربندی سفارشی"
|
||||
|
||||
msgid "Must be Base64 text!"
|
||||
msgstr "متن باید Base64 باشد!"
|
||||
|
||||
msgid "Must be JSON text!"
|
||||
msgstr "باید متن JSON باشد!"
|
||||
|
||||
msgid "Must be UUID format!"
|
||||
msgstr "باید فرمت UUID باشه!"
|
||||
|
||||
msgid "Geo View"
|
||||
msgstr "نمای Geo"
|
||||
|
||||
|
||||
@@ -971,6 +971,9 @@ msgstr "Группа правил шунтирования"
|
||||
msgid "Please note attention to the priority, the higher the order, the higher the priority."
|
||||
msgstr "Обратите внимание: правила выше в списке имеют более высокий приоритет."
|
||||
|
||||
msgid "This ID already exists."
|
||||
msgstr "Этот идентификатор уже существует."
|
||||
|
||||
msgid "Update..."
|
||||
msgstr "Обновление..."
|
||||
|
||||
@@ -1487,9 +1490,15 @@ msgstr "Серверная часть"
|
||||
msgid "Server Config"
|
||||
msgstr "Конфигурация сервера"
|
||||
|
||||
msgid "Servers Manager"
|
||||
msgstr "Диспетчер серверов"
|
||||
|
||||
msgid "Users Manager"
|
||||
msgstr "Управление пользователями"
|
||||
|
||||
msgid "User Config"
|
||||
msgstr "Пользовательская конфигурация"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Журнал"
|
||||
|
||||
@@ -1917,9 +1926,15 @@ msgstr "Использовать свою конфигурацию"
|
||||
msgid "Custom Config"
|
||||
msgstr "Пользовательская конфигурация"
|
||||
|
||||
msgid "Must be Base64 text!"
|
||||
msgstr "Это должен быть текст в формате Base64!"
|
||||
|
||||
msgid "Must be JSON text!"
|
||||
msgstr "Должен быть JSON-текст!"
|
||||
|
||||
msgid "Must be UUID format!"
|
||||
msgstr "Обязательно в формате UUID!"
|
||||
|
||||
msgid "Geo View"
|
||||
msgstr "Просмотр Geo"
|
||||
|
||||
|
||||
@@ -962,6 +962,9 @@ msgstr "分流规则组"
|
||||
msgid "Please note attention to the priority, the higher the order, the higher the priority."
|
||||
msgstr "请注意优先级问题,排序越上面优先级越高。"
|
||||
|
||||
msgid "This ID already exists."
|
||||
msgstr "该 ID 已存在。"
|
||||
|
||||
msgid "Update..."
|
||||
msgstr "更新中"
|
||||
|
||||
@@ -1478,9 +1481,15 @@ msgstr "服务器端"
|
||||
msgid "Server Config"
|
||||
msgstr "服务器配置"
|
||||
|
||||
msgid "Servers Manager"
|
||||
msgstr "服务器管理"
|
||||
|
||||
msgid "Users Manager"
|
||||
msgstr "用户管理"
|
||||
|
||||
msgid "User Config"
|
||||
msgstr "用户配置"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "日志"
|
||||
|
||||
@@ -1902,9 +1911,15 @@ msgstr "使用自定义配置"
|
||||
msgid "Custom Config"
|
||||
msgstr "自定义配置"
|
||||
|
||||
msgid "Must be Base64 text!"
|
||||
msgstr "必须是 Base64 内容!"
|
||||
|
||||
msgid "Must be JSON text!"
|
||||
msgstr "必须是 JSON 文本内容!"
|
||||
|
||||
msgid "Must be UUID format!"
|
||||
msgstr "必须是 UUID 格式!"
|
||||
|
||||
msgid "Geo View"
|
||||
msgstr "Geo 查询"
|
||||
|
||||
|
||||
@@ -962,6 +962,9 @@ msgstr "分流規則組"
|
||||
msgid "Please note attention to the priority, the higher the order, the higher the priority."
|
||||
msgstr "請注意優先級問題,排序越上面優先級越高。"
|
||||
|
||||
msgid "This ID already exists."
|
||||
msgstr "此 ID 已存在。"
|
||||
|
||||
msgid "Update..."
|
||||
msgstr "更新中"
|
||||
|
||||
@@ -1478,9 +1481,15 @@ msgstr "服務器端"
|
||||
msgid "Server Config"
|
||||
msgstr "服務器配置"
|
||||
|
||||
msgid "Servers Manager"
|
||||
msgstr "服務器管理"
|
||||
|
||||
msgid "Users Manager"
|
||||
msgstr "用戶管理"
|
||||
|
||||
msgid "User Config"
|
||||
msgstr "用戶配置"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "日誌"
|
||||
|
||||
@@ -1908,9 +1917,15 @@ msgstr "使用自定义配置"
|
||||
msgid "Custom Config"
|
||||
msgstr "自定义配置"
|
||||
|
||||
msgid "Must be Base64 text!"
|
||||
msgstr "必須是 Base64 内容!"
|
||||
|
||||
msgid "Must be JSON text!"
|
||||
msgstr "必須是 JSON 文本內容!"
|
||||
|
||||
msgid "Must be UUID format!"
|
||||
msgstr "必須是 UUID 格式!"
|
||||
|
||||
msgid "Geo View"
|
||||
msgstr "Geo 查詢"
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
|
||||
FOOTSTRAP_VERSION?=
|
||||
ifneq ($(FOOTSTRAP_VERSION),)
|
||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||
PKG_RELEASE:=14
|
||||
PKG_RELEASE:=15
|
||||
endif
|
||||
|
||||
LUCI_TITLE:=Footstrap Theme
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
const _fitters = [];
|
||||
let _rafPending = false;
|
||||
let _ro = null, _mo = null;
|
||||
/* one canvas for the whole document: wordFloor() measures text with it, and creating one per call
|
||||
* is the expensive half of that measurement */
|
||||
let _cx = null;
|
||||
const SPACE_RE = /\s+/;
|
||||
|
||||
/* Run every fitter NOW, synchronously. A fitter must be idempotent — this fires on every
|
||||
* relevant mutation. */
|
||||
@@ -119,6 +123,74 @@ return baseclass.extend({
|
||||
return el.scrollWidth > this.roomFor(el) + 1; /* +1: sub-pixel rounding */
|
||||
},
|
||||
|
||||
/* THE NARROWEST THIS TABLE CAN BE WITHOUT BREAKING A WORD THROUGH — its own breakpoint, in px.
|
||||
*
|
||||
* Every other "does it fit" question here is asked of the browser. This one cannot be: it is
|
||||
* exactly the table's min-content width, and `overflow-wrap: anywhere` (base/40-tables.css) makes
|
||||
* the browser answer ONE CHARACTER per column. That is deliberate — an unbreakable ICCID must not
|
||||
* push a table nobody measures out of its card — but it means a measured table can be starved to
|
||||
* a ribbon of fragments and still report, truthfully, that it fits. Asking the engine the honest
|
||||
* question by flipping `overflow-wrap` for one layout does not work either: Blink returns the same
|
||||
* min-content for `normal`, `break-word` and `anywhere` on a table (measured — 645px in all three,
|
||||
* with the widest word alone needing 367), so the number simply is not available from layout.
|
||||
*
|
||||
* So compute it: per COLUMN, the width of the widest WORD any of its cells has to show, plus that
|
||||
* cell's own side padding; summed across columns. A `nowrap`/`pre` column takes its whole text
|
||||
* instead, because that is what it has committed to showing. Nothing here is a threshold — the
|
||||
* result is the table's content measured in the table's own fonts, so every table gets its own
|
||||
* number and none was picked by anyone.
|
||||
*
|
||||
* Two approximations, both stated rather than hidden:
|
||||
* - the FONT is sampled once per column, from the first data row. A column that changes face
|
||||
* row by row would be measured in the first row's; no LuCI table does that, and the
|
||||
* alternative is a `getComputedStyle` per cell, which is the expensive call here.
|
||||
* - the widest word is picked by CHARACTER COUNT and only that one is measured, so a column of
|
||||
* one font is one `measureText` per new maximum instead of one per cell. Within a single font
|
||||
* that ranks `iiii` above `WWW`; it costs a few px on the floor and takes the walk over
|
||||
* Processes (114 rows) from 6ms to about 1ms.
|
||||
*
|
||||
* A `colSpan` cell contributes its whole floor to one column, which over-states that column and
|
||||
* under-states its neighbours by the same amount — the sum, which is what the caller compares, is
|
||||
* unaffected. */
|
||||
wordFloor(t) {
|
||||
const rows = t.querySelectorAll('.tr:not(.table-titles):not(.cbi-section-table-titles):not(.placeholder)');
|
||||
if (!rows.length) return 0;
|
||||
if (!_cx) _cx = document.createElement('canvas').getContext('2d');
|
||||
const floors = [], longest = [];
|
||||
let cols = null;
|
||||
for (const row of rows) {
|
||||
const cells = row.children;
|
||||
if (!cols) {
|
||||
cols = [];
|
||||
for (const c of cells) {
|
||||
const s = getComputedStyle(c);
|
||||
cols.push({
|
||||
font: `${s.fontStyle} ${s.fontWeight} ${s.fontSize} ${s.fontFamily}`,
|
||||
tracking: parseFloat(s.letterSpacing) || 0,
|
||||
pad: parseFloat(s.paddingLeft) + parseFloat(s.paddingRight),
|
||||
whole: (s.whiteSpace === 'nowrap' || s.whiteSpace === 'pre')
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const text = (cells[i].textContent || '').trim();
|
||||
if (!text) continue;
|
||||
const col = cols[i] || cols[cols.length - 1];
|
||||
let word = '';
|
||||
if (col.whole) word = text;
|
||||
else for (const w of text.split(SPACE_RE)) if (w.length > word.length) word = w;
|
||||
if (!(word.length > (longest[i] || 0))) continue;
|
||||
longest[i] = word.length;
|
||||
_cx.font = col.font;
|
||||
const need = _cx.measureText(word).width + (col.tracking * word.length) + col.pad;
|
||||
if (!(floors[i] >= need)) floors[i] = need;
|
||||
}
|
||||
}
|
||||
let sum = 0;
|
||||
for (const f of floors) sum += (f || 0);
|
||||
return sum;
|
||||
},
|
||||
|
||||
/* How many LINE BOXES of text does `el` render? The fact behind "this column has been
|
||||
* squeezed into a tower" — a cell that has to break its own words is a column that has run
|
||||
* out of width, which is a thing no viewport query can ask.
|
||||
|
||||
@@ -76,6 +76,63 @@ function clearViewIntervals() {
|
||||
}
|
||||
let _pollTimerWarned = false;
|
||||
|
||||
/* --- uci cache teardown for SPA nav ---
|
||||
* `uci.load()` does not answer "is this config present?" — it answers "which of these packages did
|
||||
* THIS call fetch", skipping every package already in its document-scoped cache:
|
||||
*
|
||||
* for (…) if (!self.state.values[packages[i]]) { pkgs.push(…); tasks.push(…) }
|
||||
* return Promise.all(tasks).then(() => pkgs); // uci.js, luci-base
|
||||
*
|
||||
* Four shipped views read that return value as an existence check and abort on an empty array —
|
||||
* luci-app-banip and luci-app-adblock's overview, luci-app-travelmate's overview and stations:
|
||||
*
|
||||
* if (!result[3] || result[3].length === 0) { ui.addNotification(…, _('No banIP config found!')); return; }
|
||||
*
|
||||
* On a full load the cache is empty, so the first visit always gets its package name back and the
|
||||
* check passes. Under SPA the document survives, so the SECOND visit gets `[]` and the page renders
|
||||
* as "no config found" — reported against banip, where switching between its own tabs and coming
|
||||
* back to Overview is the ordinary gesture, and unstickable by anything short of a reload.
|
||||
*
|
||||
* The apps' reading of `load()` is wrong, but the DIVERGENCE is ours: a cache that outlives the page
|
||||
* that filled it is state a fresh load does not have, exactly like the poll queue and the view's
|
||||
* intervals above. So drop it on navigation and let the incoming view fetch what it needs.
|
||||
*
|
||||
* `unload()` is upstream's own idiom for this, not a lever we found: `uci.save()` ends with
|
||||
* `self.unload(pkgs); return self.load(pkgs)`. Pending local edits (creates/changes/deletes) go with
|
||||
* it — as they do on a full load, which throws away the whole document. Saved changes are already
|
||||
* on the server and the Unsaved-changes banner reads them from there, so it is unaffected.
|
||||
*
|
||||
* Read through `window.L.uci` rather than a `'require uci'` pragma, deliberately: the class attaches
|
||||
* itself to L's prototype when the FIRST requirer compiles it, so this sees the instance the pages
|
||||
* actually use, and a router that required it would both bind it to its own prototypal L (the two-L
|
||||
* trap, docs/spa-router.md) and pull uci.js onto pages that never touch uci. No instance means no
|
||||
* cache to flush. */
|
||||
function flushUciCache() {
|
||||
const uci = window.L ? window.L.uci : null;
|
||||
if (!uci || typeof uci.unload !== 'function') return;
|
||||
/* `state.values` and `loaded` are private, so the same rule as L.Poll.timer applies: a shape we
|
||||
* do not recognise is a reason to do nothing, once, loudly — never to guess. The two hold
|
||||
* different halves of the cache (a package whose load is still in flight is in `loaded` alone),
|
||||
* and unload() clears both, so the names are taken from both. */
|
||||
if (!uci.state || typeof uci.state.values !== 'object' || typeof uci.loaded !== 'object') {
|
||||
if (!_uciCacheWarned) {
|
||||
_uciCacheWarned = true;
|
||||
console.error('footstrap: LuCI.uci keeps its cache somewhere this router does not know, so '
|
||||
+ 'it is left alone. An app that reads uci.load()\'s return value as an existence check '
|
||||
+ 'will report a missing config on the second SPA visit. fs-router.js needs updating for '
|
||||
+ 'this luci-base.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const names = Object.keys(uci.state.values).concat(Object.keys(uci.loaded));
|
||||
if (names.length) uci.unload(names);
|
||||
/* `state.reorder` is the one half unload() does not clear, and it is deliberately left alone
|
||||
* rather than reset by hand: with `values` gone, reorderSections() finds no sections to order,
|
||||
* emits no call and clears the map itself on the next save. Writing to that field would be us
|
||||
* editing another module's private state, for a difference nothing can observe. */
|
||||
}
|
||||
let _uciCacheWarned = false;
|
||||
|
||||
let _wired = false;
|
||||
/* The pathname whose view is CURRENTLY rendered — popstate compares against it to tell a real
|
||||
* navigation from a mere fragment change (see there). Seeded from the served page. */
|
||||
@@ -599,6 +656,9 @@ function navigate(pathname, push, kbd) {
|
||||
/* kill the outgoing view's plain setInterval pollers too (podkop's log tailer) — a full load
|
||||
* would have. L.Poll's own tick survives. */
|
||||
clearViewIntervals();
|
||||
/* and drop uci's document-scoped config cache, which a full load would not have carried into the
|
||||
* incoming page either (see flushUciCache) */
|
||||
flushUciCache();
|
||||
/* the outgoing page's links are about to become a detached tree — do not hold one of them */
|
||||
_lastHovered = null;
|
||||
/* run every registered navigation callback — today the search palette's recent-pages record and
|
||||
|
||||
@@ -172,10 +172,40 @@ function enhance(sel) {
|
||||
*
|
||||
* `.table`, not `table.table` — the SAME selector relevant() and STACKABLE use. Stock LuCI
|
||||
* happens to emit only real <table>s, but a third-party luci-app-* may emit a <div class="table">
|
||||
* (coverage rule, docs/conventions.md), which a tag qualifier would pass over so it could never card. */
|
||||
* (coverage rule, docs/conventions.md), which a tag qualifier would pass over so it could never card.
|
||||
*
|
||||
* `.table` is LuCI's own class, and everything the theme knows how to do with a table hangs off it.
|
||||
* A third-party app that emits a BARE `<table>` — no LuCI classes at all — therefore matched none of
|
||||
* this: nothing tagged it, nothing measured it, nothing carded it, and the only thing that reached it
|
||||
* was a phone-tier scrollbar (theme/90-responsive.css). Reported from a phone against a wifi-clients
|
||||
* dashboard whose last column was simply cut off.
|
||||
*
|
||||
* So the second half of this selector claims those too. It is deliberately UNMEASURABLE on the dev
|
||||
* stand — a census of `#view table:not(.table):not(.cbi-section-table)` over all 196 menu pages,
|
||||
* openclash / justclash / ssclash / dashboard / statistics included, found ZERO. That is the point:
|
||||
* every table anyone here emits already carries the class, so this changes nothing that can be
|
||||
* measured and covers the one shape that cannot be (docs/conventions.md: coverage is a contract). */
|
||||
const FOREIGN_TABLE = '#view .table:not(.cbi-section-table):not(.fs-dt), ' +
|
||||
'#view table:not(.table):not(.cbi-section-table):not(.fs-dt)';
|
||||
|
||||
/* The FOURTH header markup, and the one only a foreign table produces: `<table><tr><th>…`, with no
|
||||
* `<thead>` for the parser to imply and none of LuCI's class names. It is the exact shape the phone
|
||||
* tier's scroll fallback was written against, so it has to be recognisable here or that table can
|
||||
* still only ever scroll.
|
||||
*
|
||||
* "Every cell in the first row is a `<th>`" is the whole test, and it has to be EVERY: a data row
|
||||
* whose first cell is a row header (`<th scope=row>`) would otherwise be read as the header row and
|
||||
* every value below it captioned with a value. A table with no `<th>` at all — a layout table, a
|
||||
* matrix — returns null and keeps today's behaviour, which is what the scroll fallback is for. */
|
||||
function headerRow(t) {
|
||||
const row = t.rows && t.rows[0];
|
||||
if (!row || !row.cells.length) return null;
|
||||
return [ ...row.cells ].every((c) => c.tagName === 'TH') ? row : null;
|
||||
}
|
||||
|
||||
function tagDataTables() {
|
||||
document.querySelectorAll('#view .table:not(.cbi-section-table):not(.fs-dt)').forEach((t) => {
|
||||
/* THREE header markups, and each missing one cost a page. L.ui.Table emits
|
||||
document.querySelectorAll(FOREIGN_TABLE).forEach((t) => {
|
||||
/* FOUR header markups, and each missing one cost a page. L.ui.Table emits
|
||||
* `.tr.table-titles`; the apk Software page emits `.tr.cbi-section-table-titles` (missing
|
||||
* it is why the package list once needed a stacking block of its own); and a third-party
|
||||
* table may simply use a real `<thead>` — luci-mod-dashboard's device lists are
|
||||
@@ -188,16 +218,66 @@ function tagDataTables() {
|
||||
* to the `<thead>` — the parser's implied row never happens, so a `tr` in the selector finds
|
||||
* nothing. Read as "the header ROW-ISH element", which is what its children are cells of.
|
||||
*
|
||||
* ANY of the three = a data table; NONE = a key/value include (System, Memory), which must
|
||||
* ANY of the four = a data table; NONE = a key/value include (System, Memory), which must
|
||||
* never card. `thead` is the structural form of the same statement the two classes make, so
|
||||
* it belongs in the same list rather than in a rule of its own. */
|
||||
const head = t.querySelector('.tr.table-titles, .tr.cbi-section-table-titles, thead');
|
||||
* it belongs in the same list rather than in a rule of its own; headerRow() is the fourth and
|
||||
* cannot be, because "the first row is all `<th>`" is not a selector. */
|
||||
const head = t.querySelector('.tr.table-titles, .tr.cbi-section-table-titles, thead') || headerRow(t);
|
||||
if (!head) return;
|
||||
t.classList.add('fs-dt');
|
||||
/* `.table` as well as `.fs-dt`, and only ever ADDED: the theme's whole table vocabulary —
|
||||
* the frame, the cell padding, the card stack — is written against `.table`, so a foreign
|
||||
* table that has just been recognised as a data table has to join it or the tag buys nothing.
|
||||
* A no-op on everything LuCI renders, which carries the class already. */
|
||||
t.classList.add('table', 'fs-dt');
|
||||
adoptMarkup(t, head);
|
||||
labelCells(t, head);
|
||||
});
|
||||
}
|
||||
|
||||
/* ...AND THE ROWS AND CELLS INSIDE IT, or the claim is a trap.
|
||||
*
|
||||
* `.table` alone gets the frame and the padding, because those rules end at the table. Everything
|
||||
* that makes the CARD is written one level down — `.table.fs-stacked .tr { display: flex }`, the
|
||||
* `.td[data-title]::before` label, the hidden header row — and a bare foreign `<table>` carries none
|
||||
* of those class names. So the fitter would measure it, decide it no longer fits, set `.fs-stacked`
|
||||
* and change NOTHING: measured at 390px on a bare four-column table, the rows stayed `table-row`,
|
||||
* the cells `table-cell` at 80px each, no label was generated — and `#view .table.fs-dt.fs-stacked`
|
||||
* sets `overflow: hidden`, so the columns were CLIPPED with no scrollbar to reach them. That is
|
||||
* worse than the phone-tier scroll it replaced, which is the whole reason this exists.
|
||||
*
|
||||
* `.tr` / `.td` / `.th` are LuCI's own names for these roles (docs/third-party-apps.md: the shared
|
||||
* zone), and the theme is already writing `.table` onto the same element — this is that one act
|
||||
* carried down to the rows, not a new liberty. Additive only, and cheap enough to re-run every fit
|
||||
* pass: `classList.add` on an element that already has the class is the same `contains` check we
|
||||
* would write to skip it, and these tables are POLLED, so fresh rows arrive bare.
|
||||
*
|
||||
* The HEADER also has to be recognisable as one, or the card shows it as a first row of column
|
||||
* names: a `<thead>` becomes `.thead` and a plain first row of `<th>` becomes `.tr.table-titles` —
|
||||
* the two names theme/30-tables.css hides when stacked. */
|
||||
function adoptMarkup(t, head) {
|
||||
/* DECIDED ONCE, AT CLAIM TIME, and only for a table that speaks none of this vocabulary. Asking
|
||||
* the question every pass instead would answer "already adopted" the moment we adopted it — and
|
||||
* these tables are polled, so the fresh rows that arrive bare afterwards would never be taken.
|
||||
* Asking it at all is what keeps the theme's hands off LuCI's own markup: the apk Software list
|
||||
* heads its table with `.tr.cbi-section-table-titles`, and blindly adding `table-titles` to that
|
||||
* would be the theme rewriting a class LuCI chose. */
|
||||
if (t._fsAdopt === undefined) t._fsAdopt = !t.querySelector('.tr, .thead');
|
||||
if (!t._fsAdopt) return;
|
||||
if (head.tagName === 'THEAD') head.classList.add('thead');
|
||||
else head.classList.add('tr', 'table-titles');
|
||||
const titleRow = (head.firstElementChild && head.firstElementChild.tagName === 'TR') ? head.firstElementChild : head;
|
||||
for (const c of titleRow.children) c.classList.add('th');
|
||||
/* `t.rows` covers a real <table> whether or not it has a <tbody> — a table built with
|
||||
* createElement has its <tr> directly under the <table> and `tbody tr` finds nothing. A
|
||||
* `<div class="table">` has no `.rows` and is LuCI's own markup, which carries the classes. */
|
||||
if (!t.rows) return;
|
||||
for (const row of t.rows) {
|
||||
if (head.contains(row) || row === head) continue;
|
||||
row.classList.add('tr');
|
||||
for (const cell of row.children) cell.classList.add(cell.tagName === 'TH' ? 'th' : 'td');
|
||||
}
|
||||
}
|
||||
|
||||
/* Give every cell the column heading it will show once the table cards.
|
||||
*
|
||||
* The card layout prints `attr(data-title)` above each value (theme/30-tables.css), and LuCI's own
|
||||
@@ -212,7 +292,14 @@ function tagDataTables() {
|
||||
* matters because these tables are POLLED: the rows are replaced wholesale every few seconds, and
|
||||
* the fresh ones arrive without it. */
|
||||
function labelCells(t, head) {
|
||||
const titles = [ ...head.children ].map((c) => (c.textContent || '').trim());
|
||||
/* A `<thead>` that was WRITTEN as markup nests a real `<tr>` — the parser inserts one even where
|
||||
* the author left it out — while one built by E() holds the `<th>`s directly (see above). Reading
|
||||
* `head.children` blind therefore captioned every cell of a parsed table with the header row's
|
||||
* ENTIRE text: "HostAddressSignal" over the hostname, over the address and over the signal.
|
||||
* Measured against a bare `<table><thead><tr><th>` on the stand, which is the shape a
|
||||
* server-rendered or innerHTML-built foreign table has. */
|
||||
const titleRow = (head.firstElementChild && head.firstElementChild.tagName === 'TR') ? head.firstElementChild : head;
|
||||
const titles = [ ...titleRow.children ].map((c) => (c.textContent || '').trim());
|
||||
if (!titles.some(Boolean)) return;
|
||||
for (const row of t.querySelectorAll('.tr, tbody tr')) {
|
||||
if (row === head) continue;
|
||||
@@ -299,6 +386,30 @@ function idTower(t) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---- AND THE SAME RIBBON IN A COLUMN THAT IS NOT THE FIRST ----
|
||||
*
|
||||
* There is no second test for it, and that is the fix rather than an omission.
|
||||
*
|
||||
* `idTower` above is deliberately first-column-only, on the grounds that "a value column wrapping to
|
||||
* a few lines is a value being shown, not a table falling apart". That held right up until the value
|
||||
* was one unbreakable token: `overflow-wrap: anywhere` gave such a cell a min-content of ONE
|
||||
* CHARACTER, so auto table layout was free to starve its column to one character, and `overflows()`
|
||||
* then reported — truthfully, uselessly — that the table fit. Reported from a hardware router at
|
||||
* 700-790px of window: the v4 lease table cards there (its `nowrap` columns give it a floor, so it
|
||||
* really does overflow) while the v6 table beside it shredded the DUID, measured at 5 lines with
|
||||
* 674px of room and 7 at 654px, against 1 line at 1160px.
|
||||
*
|
||||
* A first pass answered it with a line count, which is a number somebody picks. This asks the table
|
||||
* instead: `fit.wordFloor()` returns the narrowest the table can be without breaking a word through —
|
||||
* per column, the widest WORD it must show, in that column's own font, summed. Past that width the
|
||||
* browser has to cut through a value, and the card view is what shows values whole. So every table
|
||||
* carries its own breakpoint, derived from its own content, and no threshold was chosen anywhere.
|
||||
*
|
||||
* On the reporting router, at 1190px of room: leases6 asks for 935, the associated-stations table
|
||||
* for 966, the v4 leases for 645, Processes for 794, Connections for 550 and Startup for 381 — so
|
||||
* the two that were unreadable card at roughly a 1000px window, and the four that were fine keep
|
||||
* being tables until the room they actually need runs out. */
|
||||
|
||||
function fitTables() {
|
||||
document.querySelectorAll(STACKABLE).forEach((t) => {
|
||||
const was = t.classList.contains('fs-stacked');
|
||||
@@ -309,8 +420,8 @@ function fitTables() {
|
||||
const room = fit.roomFor(t);
|
||||
if (!(room > 0)) { if (was) t.classList.add('fs-stacked'); return; }
|
||||
|
||||
/* idTower last: it is the only one that walks the rows */
|
||||
const stack = room < CRAMPED || fit.overflows(t) || idTower(t);
|
||||
/* the two row walks last, cheapest first: idTower reads one column, wordFloor every cell */
|
||||
const stack = room < CRAMPED || fit.overflows(t) || idTower(t) || fit.wordFloor(t) > room;
|
||||
/* write only on a real change: the poll re-renders these tables once a second, and
|
||||
* toggling the class off and on each tick would invalidate style for every row of
|
||||
* Processes/Leases for nothing */
|
||||
|
||||
@@ -83,14 +83,51 @@
|
||||
.table .td { overflow-wrap: anywhere; }
|
||||
.table .th { overflow-wrap: break-word; }
|
||||
|
||||
/* …AND THE FIRST COLUMN IS A LABEL TOO, whichever element it happens to be made of.
|
||||
*
|
||||
* `anywhere` costs a min-content of one character (above), and auto table layout hands each
|
||||
* column its share by MAX-content — so a two-column key/value table whose value is one enormous
|
||||
* token starves the key column down to that single character, and the header row above it with
|
||||
* it. Measured against luci-app-dockerman's Environment table (issue #36), which prints
|
||||
* `JSON.stringify()` of every field `docker info` returns: at 8 000 characters of value the key
|
||||
* column fell from 95px to 55px and "Driver" started wrapping; on the reporter's own host it
|
||||
* reached about one glyph, so the column header read "E N T R Y" straight down the page, on
|
||||
* Firefox 153 and on Blink alike.
|
||||
*
|
||||
* `break-word` puts the floor back at the longest WORD — the same trade `.th` makes one line up,
|
||||
* for the same reason: the first column is the row's identity, and it is what you read the table
|
||||
* by. fs-select.js makes the same judgement in the other direction (MAX_ID_LINES: past five
|
||||
* lines the identity has stopped being readable and the table cards instead).
|
||||
*
|
||||
* A CARDED row is exempt and must be: `.table.fs-stacked .td` (theme/30-tables.css) restates
|
||||
* `anywhere` from a later layer, so once every cell is on its own line there is no neighbouring
|
||||
* column left to starve it and a long value may break wherever it likes. */
|
||||
.table .tr > .td:first-child { overflow-wrap: break-word; }
|
||||
|
||||
.table .tr:first-child .th {
|
||||
padding-top: var(--fs-space-2);
|
||||
font-weight: var(--fs-weight-bold);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* the empty-table plate: three lines of leading plus the cell inset it sits in, so it keeps
|
||||
* the table's own height when there is nothing to show. */
|
||||
/* The empty-table plate: three lines of leading plus the cell inset it sits in, so it keeps
|
||||
* the table's own height when there is nothing to show.
|
||||
*
|
||||
* IT IS OUT OF FLOW ON PURPOSE and stays that way: a single-cell row in a `display: table` spans
|
||||
* only the FIRST column, so in flow the plate is a narrow box under column one — and, worse, its
|
||||
* sentence then becomes that column's max-content and WIDENS it (measured: the Container column
|
||||
* of an empty section went 275px -> 502px when the cell was put back in flow). Absolute takes it
|
||||
* out of the column sizing entirely and `inset-inline: 0` spans it across the table.
|
||||
*
|
||||
* WHAT IT MUST NOT DO IS PIN ITSELF TO THE BOTTOM. It used to say `bottom: 0`, which inside the
|
||||
* `position: relative` .table means the bottom of the whole TABLE — the same place as its own row
|
||||
* only while the placeholder is the last row there is. `form.TableSection` renders its `footer`
|
||||
* into a `<tfoot>` AFTER the `<tbody>`, so on every empty section that declares one the plate
|
||||
* printed straight across the totals: measured on luci-app-dockerman's Containers, Images and
|
||||
* Volumes tables (issue #36) as a 1190x35 overlap, the full width of the table, identically on
|
||||
* Blink and on Gecko 153. With no `bottom` at all an absolutely positioned box keeps its STATIC
|
||||
* position — where the row actually is — which is what was meant all along, and the reserved row
|
||||
* height below is what holds that space open. */
|
||||
.tr.placeholder {
|
||||
height: calc(3em + var(--fs-space-4));
|
||||
}
|
||||
@@ -98,9 +135,16 @@
|
||||
.tr.placeholder > .td {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
line-height: 3em;
|
||||
/* …and the plate must be exactly as tall as the row reserved for it, or it hangs out of the
|
||||
* table at whichever end it is anchored to. `--fs-space-4` is two `--fs-space-2` at every
|
||||
* density, so this and the row height above are the same number written twice — which is what
|
||||
* lets the cell keep its own size while the four table flavours go on padding their ordinary
|
||||
* cells differently (`--fs-space-2` here, `--fs-space-2-5` on a framed data table, and so on).
|
||||
* Measured on Wireless, whose associated-stations table pads 10px a side: the plate came out
|
||||
* 67px inside a 63px row and printed 4px BELOW the frame's bottom border. */
|
||||
padding-block: var(--fs-space-2);
|
||||
}
|
||||
|
||||
.tr.drag-over-above,
|
||||
|
||||
@@ -153,15 +153,20 @@
|
||||
#view .table[id] .tr:last-child .td:last-child,
|
||||
table.fs-dt .tr:last-child .td:last-child { border-bottom-right-radius: var(--fs-radius-lg); }
|
||||
/* Empty table: L.ui.Table adds a single-cell .tr.placeholder ("There are no active leases").
|
||||
* In a display:table it spans only the first column, so the bottom-corner rounding drew a tiny
|
||||
* rounded box on the left. Block it to full width, carrying the frame's bottom corners. */
|
||||
.cbi-section .table[id] .tr.placeholder,
|
||||
#view .table[id] .tr.placeholder,
|
||||
table.fs-dt .tr.placeholder { display: block; }
|
||||
* Spanning it across the table is base's job (base/40-tables.css: absolute, `inset-inline: 0`),
|
||||
* and the `display: block; width: 100%` that used to be restated here was a second way of saying
|
||||
* the same thing — it read as the fix while the real one was base's `inset-inline`. What is left
|
||||
* is the FRAMED table's own flavour: the plate reads as prose, left-aligned like the rows it
|
||||
* stands in for, and carries the frame's bottom corners so the rounding does not cut across it. */
|
||||
.cbi-section .table[id] .tr.placeholder .td,
|
||||
#view .table[id] .tr.placeholder .td,
|
||||
table.fs-dt .tr.placeholder .td {
|
||||
display: block; width: 100%; text-align: start;
|
||||
/* the framed flavour pads its cells `--fs-space-2-5` a side (above), and that shorthand is in
|
||||
* THIS layer, so it overrides base's placeholder padding and made the plate 4px taller than
|
||||
* the row reserved for it — measured on Wireless as 67px inside 63px, printing below the
|
||||
* frame's bottom border. Put base's number back for this one cell. */
|
||||
padding-block: var(--fs-space-2);
|
||||
text-align: start;
|
||||
color: var(--fs-dim); font-family: var(--fs-font-sans);
|
||||
border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg);
|
||||
}
|
||||
@@ -459,7 +464,11 @@
|
||||
* only thing overflow:visible protected. `#view .table.fs-dt.fs-stacked` (1,3,0) outranks the
|
||||
* frame's `#view .table[id]` (1,2,0) on specificity, not order. */
|
||||
#view .table.fs-dt.fs-stacked { overflow: hidden; }
|
||||
.table.fs-dt.fs-stacked .tr { padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-3); }
|
||||
/* `:not(.placeholder)` — the plate is out of flow (base/40-tables.css) and its row exists only to
|
||||
* reserve the height it needs. Padding is subtracted from that reservation (border-box), so the
|
||||
* card's row inset left 41px of the 63px the plate is: measured on Wireless as 10px of text
|
||||
* hanging below the frame once the table stacked. A row with no rows in it has nothing to inset. */
|
||||
.table.fs-dt.fs-stacked .tr:not(.placeholder) { padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-3); }
|
||||
/* `.fs-stacked .td`, not `.table.fs-dt.fs-stacked .td` — the same elements (the class is only
|
||||
* ever written onto a .table.fs-dt), but at (0,2,0) instead of (0,4,0): low enough that the
|
||||
* unguarded `.table .td.col-N` below, at (0,3,0), beats it on SPECIFICITY, not source order. */
|
||||
@@ -490,4 +499,34 @@
|
||||
|
||||
/* general table cell padding — compact but readable (Startup/Processes/etc) */
|
||||
.table .td { padding-top: var(--fs-space-2); padding-bottom: var(--fs-space-2); }
|
||||
|
||||
/* ---- THE LAST RESORT: a foreign <table> the card stack cannot claim ----
|
||||
*
|
||||
* fs-select.js now tags any `#view table` that HAS a header row — `.tr.table-titles`,
|
||||
* `.tr.cbi-section-table-titles`, a `<thead>`, or a first row made entirely of `<th>` — as
|
||||
* `.table.fs-dt`, so it is measured and cards like every other data table. What is left here is
|
||||
* the table with no header at all: a log, a statistics matrix, a layout table. Those must NOT be
|
||||
* carded even if they overflow — a card prints `data-title` above each value, and a matrix has no
|
||||
* titles to print, so stacking one produces a column of numbers with nothing saying what they are.
|
||||
* Comparison is the whole point of that shape; it scrolls instead.
|
||||
*
|
||||
* `display: block` + `overflow-x: auto` on the table ITSELF, not on a wrapper: the theme cannot
|
||||
* add markup around someone else's element, and the block box is what gives it a scroll container.
|
||||
* The rows still lay out as a table — a `display: block` <table> wraps its children in an
|
||||
* anonymous table box — so the columns keep lining up.
|
||||
*
|
||||
* UNGUARDED, and that is the change: this rule spent its life inside `@media (max-width: 767px)`
|
||||
* (theme/90-responsive.css), where it was written after a phone report — a wifi-clients dashboard
|
||||
* whose last column was silently CUT by `.fs-main`'s `overflow-x: clip`, measured at 390px as a
|
||||
* 388px table in a 356px card. But the viewport was never the fact that mattered: a narrow panel,
|
||||
* a split window or a 960px tablet reaches the same wall with the query switched off, and the
|
||||
* clip is just as silent there. Cost at a width where the table fits: nothing at all — `overflow`
|
||||
* only paints a scrollbar once there is something to scroll. */
|
||||
#view table:not(.table):not(.cbi-section-table):not(.fs-dt) {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
/* the scroll gesture stops at this table instead of dragging the page sideways */
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,32 +6,11 @@
|
||||
* or table shrink or wrap. Tablets (>=768px) were already clean.
|
||||
* ================================================================== */
|
||||
@media (max-width: 767px) {
|
||||
/* A FOREIGN <table> — one with none of LuCI's classes — scrolls instead of being cut off.
|
||||
*
|
||||
* The theme cards a data table when it stops fitting (.fs-stacked, fs-select.js), but that
|
||||
* only reaches `.table` markup carrying a header row: `.tr.table-titles` or
|
||||
* `.tr.cbi-section-table-titles`. A third-party dashboard that emits a plain
|
||||
* `<table><tr><th>` matches none of it, so nothing measured it, nothing carded it, and
|
||||
* `.fs-main`'s `overflow-x: clip` silently CUT the right-hand columns — reported on a phone
|
||||
* against a wifi-clients dashboard whose last column was simply not there. Measured here at
|
||||
* 390px: the table wanted 388px inside a 356px card and overflowed by 31px, with the card at
|
||||
* `overflow-x: visible` so there was nothing to scroll.
|
||||
*
|
||||
* `display: block` + `overflow-x: auto` on the table itself, not a wrapper: the theme cannot
|
||||
* add markup around someone else's element, and the block box is what gives it a scroll
|
||||
* container. The rows still lay out as a table — a `display: block` <table> wraps its own
|
||||
* children in an anonymous table box — so the columns keep lining up.
|
||||
*
|
||||
* Scoped away from every table the theme DOES understand: `.table` (LuCI's div-tables and
|
||||
* the ones fs-select tags), `.cbi-section-table` (config forms, which card by container)
|
||||
* and `.fs-dt`. Those have their own answer and must not get a scrollbar instead of it. */
|
||||
#view table:not(.table):not(.cbi-section-table):not(.fs-dt) {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
/* the scroll gesture stops at this table instead of dragging the page sideways */
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
/* The foreign-<table> scroll fallback used to live here, guarded by this query. It does not
|
||||
* any more: whether a table fits is a property of its CONTENT and its COLUMN, never of the
|
||||
* viewport — the same argument the fit engine is built on — and a 400px-wide panel on a
|
||||
* 1600px desktop needs it exactly as much as a phone does. It is unguarded now, in
|
||||
* theme/30-tables.css, next to the card stack it is the fallback FOR. */
|
||||
|
||||
/* Must stay INSIDE this @media, and the reason is specificity within ONE layer: the 400px
|
||||
* cap lives in theme/65-dropdown.css (absorbed there from base) on
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mptcp
|
||||
PKG_VERSION:=6.1
|
||||
PKG_RELEASE:=9
|
||||
PKG_RELEASE:=10
|
||||
|
||||
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
|
||||
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
@@ -52,7 +52,11 @@ global_multipath_settings() {
|
||||
config_get mptcp_rr_num_segments globals mptcp_rr_num_segments
|
||||
config_get mptcp_version globals mptcp_version "0"
|
||||
config_get mptcp_stale_loss_cnt globals mptcp_stale_loss_cnt "4"
|
||||
config_get mptcp_pm_type globals mptcp_pm_type "1"
|
||||
# Fallback matches the uci-defaults / LuCI form default (in-kernel PM):
|
||||
# an absent value must NOT resolve to userspace PM, or a config save
|
||||
# that drops this option (see issue #4349) silently disables in-kernel
|
||||
# fullmesh subflow creation and breaks multi-WAN bonding.
|
||||
config_get mptcp_pm_type globals mptcp_pm_type "0"
|
||||
config_get mptcp_allow_join_initial_addr_port globals mptcp_allow_join_initial_addr_port "1"
|
||||
config_get mptcp_close_timeout globals mptcp_close_timeout "60"
|
||||
config_get mptcp_blackhole_timeout globals mptcp_blackhole_timeout "3600"
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=netdata
|
||||
PKG_VERSION:=2.10.4
|
||||
PKG_RELEASE:=14
|
||||
PKG_VERSION:=2.11.0
|
||||
PKG_RELEASE:=15
|
||||
|
||||
PKG_MAINTAINER:=Josef Schlehofer <pepe.schlehofer@gmail.com>, Daniel Engberg <daniel.engberg.lists@pyret.net>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
Reference in New Issue
Block a user