From a4c01fa552002c44438d0e15948f5c7a6b6bc3dc Mon Sep 17 00:00:00 2001 From: kenzok8 Date: Tue, 2 Dec 2025 04:25:44 +0800 Subject: [PATCH] update 2025-12-02 04:25:44 --- luci-app-passwall2/Makefile | 2 +- .../resources/view/passwall2/Sortable.min.js | 2 + .../luasrc/controller/passwall2.lua | 34 +- .../cbi/passwall2/client/node_subscribe.lua | 2 +- .../model/cbi/passwall2/client/type/ray.lua | 13 +- luci-app-passwall2/luasrc/passwall2/api.lua | 61 +- .../luasrc/passwall2/util_xray.lua | 3 +- .../luasrc/view/passwall2/log/log.htm | 22 +- .../passwall2/node_list/link_add_node.htm | 2 +- .../view/passwall2/node_list/node_list.htm | 375 +++++-- .../luasrc/view/passwall2/server/log.htm | 23 +- luci-app-passwall2/po/zh-cn/passwall2.po | 20 +- luci-app-passwall2/po/zh-tw/passwall2.po | 918 +++++++++--------- .../root/usr/share/passwall2/haproxy.lua | 14 +- .../usr/share/passwall2/helper_dnsmasq.lua | 4 +- .../root/usr/share/passwall2/rule_update.lua | 40 +- .../root/usr/share/passwall2/subscribe.lua | 111 ++- xray-core/Makefile | 4 +- 18 files changed, 1008 insertions(+), 642 deletions(-) create mode 100644 luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/Sortable.min.js diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 76bbb2270..76dd6f82a 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 -PKG_VERSION:=25.11.18 +PKG_VERSION:=25.12.2 PKG_RELEASE:=1 PKG_PO_VERSION:=$(PKG_VERSION) diff --git a/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/Sortable.min.js b/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/Sortable.min.js new file mode 100644 index 000000000..95423a649 --- /dev/null +++ b/luci-app-passwall2/htdocs/luci-static/resources/view/passwall2/Sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Ft(t){Z&&Z.parentNode[K]._isOutsideThisEl(t.target)}function jt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return kt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==jt.supportPointer&&"PointerEvent"in window&&(!u||c),emptyInsertThreshold:5};for(n in z.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Rt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&It,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),St.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,A())}function Ht(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Lt(t){t.draggable=!1}function Kt(){xt=!1}function Wt(t){return setTimeout(t,0)}function zt(t){return clearTimeout(t)}jt.prototype={constructor:jt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(vt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,Z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Ot.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Ot.push(o)}}(o),!Z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||et===l)){if(it=j(l),at=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return V({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),U("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return V({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),U("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!Z&&n.parentNode===r&&(o=X(n),J=r,$=(Z=n).parentNode,tt=Z.nextSibling,et=n,st=a.group,ut={target:jt.dragged=Z,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ut.clientX-o.left,gt=ut.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Z.style["will-change"]="all",o=function(){U("delayEnded",i,{evt:t}),jt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(Z.draggable=!0),i._triggerDragStart(t,e),V({sortable:i,name:"choose",originalEvent:t}),k(Z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){D(Z,t.trim(),Lt)}),h(l,"dragover",Bt),h(l,"mousemove",Bt),h(l,"touchmove",Bt),a.supportPointer?(h(l,"pointerup",i._onDrop),this.nativeDraggable||h(l,"pointercancel",i._onDrop)):(h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop)),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Z.draggable=!0),U("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():jt.eventCanceled?this._onDrop():(a.supportPointer?(h(l,"pointerup",i._disableDelayedDrag),h(l,"pointercancel",i._disableDelayedDrag)):(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag)),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){Z&&Lt(Z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(Z,"dragend",this),h(J,"dragstart",this._onDragStart));try{document.selection?Wt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Dt=!1,J&&Z?(U("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Ft),n=this.options,t||k(Z,n.dragClass,!1),k(Z,n.ghostClass,!0),jt.active=this,t&&this._appendGhost(),V({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(dt){this._lastX=dt.clientX,this._lastY=dt.clientY,Xt();for(var t=document.elementFromPoint(dt.clientX,dt.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(dt.clientX,dt.clientY))!==e;)e=t;if(Z.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:dt.clientX,clientY:dt.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=g(t=e));Yt()}},_onTouchMove:function(t){if(ut){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Q&&b(Q,!0),a=Q&&r&&r.a,l=Q&&r&&r.d,e=At&&wt&&E(wt),a=(i.clientX-ut.clientX+o.x)/(a||1)+(e?e[0]-Tt[0]:0)/(a||1),l=(i.clientY-ut.clientY+o.y)/(l||1)+(e?e[1]-Tt[1]:0)/(l||1);if(!jt.active&&!Dt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,Q),e?t.clientX<_.left-10||t.clientY /dev/null 2>&1 &") @@ -631,7 +653,7 @@ function restore_backup() fp:close() if chunk_index + 1 == total_chunks then api.sys.call("echo '' > /tmp/log/passwall2.log") - api.log(string.format(" * PassWall2 %s", i18n.translate("Configuration file uploaded successfully…"))) + api.log(0, string.format(" * PassWall2 %s", i18n.translate("Configuration file uploaded successfully…"))) local temp_dir = '/tmp/passwall2_bak' api.sys.call("mkdir -p " .. temp_dir) if api.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then @@ -641,13 +663,13 @@ function restore_backup() api.sys.call("cp -f " .. temp_file .. " " .. backup_file) end end - api.log(string.format(" * PassWall2 %s", i18n.translate("Configuration restored successfully…"))) - api.log(string.format(" * PassWall2 %s", i18n.translate("Service restarting…"))) + api.log(0, string.format(" * PassWall2 %s", i18n.translate("Configuration restored successfully…"))) + api.log(0, string.format(" * PassWall2 %s", i18n.translate("Service restarting…"))) luci.sys.call('/etc/init.d/passwall2 restart > /dev/null 2>&1 &') luci.sys.call('/etc/init.d/passwall2_server restart > /dev/null 2>&1 &') result = { status = "success", message = "Upload completed", path = file_path } else - api.log(string.format(" * PassWall2 %s", i18n.translate("Configuration file decompression failed, please try again!"))) + api.log(0, string.format(" * PassWall2 %s", i18n.translate("Configuration file decompression failed, please try again!"))) result = { status = "error", message = "Decompression failed" } end api.sys.call("rm -rf " .. temp_dir) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua index cd7cc8118..327b488cf 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua @@ -209,7 +209,7 @@ o.cfgvalue = function(t, n) str = str ~= "" and "
" .. str or "" local num = 0 m.uci:foreach(appname, "nodes", function(s) - if s["group"] ~= "" and s["group"] == remark then + if s["group"] and s["group"]:lower() == remark:lower() then num = num + 1 end end) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua index fbd6aabdf..231965e29 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua @@ -390,6 +390,9 @@ o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translat o.default = "0" o:depends({ [_n("tls")] = true, [_n("reality")] = false }) +o = s:option(Value, _n("tls_chain_fingerprint"), translate("TLS Chain Fingerprint (SHA256)"), translate("Once set, connects only when the server’s chain fingerprint matches.")) +o:depends({ [_n("tls")] = true, [_n("reality")] = false }) + o = s:option(Flag, _n("ech"), translate("ECH")) o.default = "0" o:depends({ [_n("tls")] = true, [_n("flow")] = "", [_n("reality")] = false }) @@ -616,8 +619,14 @@ o = s:option(TextValue, _n("xhttp_extra"), " ", translate("An XHttpObject in JSO o:depends({ [_n("use_xhttp_extra")] = true }) o.rows = 15 o.wrap = "off" +o.custom_cfgvalue = function(self, section, value) + local raw = m:get(section, "xhttp_extra") + if raw then + return api.base64Decode(raw) + end +end o.custom_write = function(self, section, value) - m:set(section, self.option:sub(1 + #option_prefix), value) + m:set(section, "xhttp_extra", api.base64Encode(value)) local success, data = pcall(jsonc.parse, value) if success and data then local address = (data.extra and data.extra.downloadSettings and data.extra.downloadSettings.address) @@ -640,7 +649,7 @@ o.validate = function(self, value) return value end o.custom_remove = function(self, section, value) - m:del(section, self.option:sub(1 + #option_prefix)) + m:del(section, "xhttp_extra") m:del(section, "download_address") end diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua index d17f59145..b56192610 100644 --- a/luci-app-passwall2/luasrc/passwall2/api.lua +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -31,8 +31,8 @@ if lang == "auto" then end i18n.setlanguage(lang) -function log(...) - local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ") +function echolog(...) + local result = table.concat({...}, " ") local f, err = io.open(LOG_FILE, "a") if f and err == nil then f:write(result .. "\n") @@ -40,6 +40,23 @@ function log(...) end end +function echolog_date(...) + local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ") + echolog(result) +end + +function log(level, ...) + local indent = "" + if level >= 1 then + for i = 1, level, 1 do + indent = indent .. " " + end + echolog_date(indent .. "- " .. table.concat({...}, " ")) + else + echolog_date(table.concat({...}, " ")) + end +end + function is_old_uci() return sys.call("grep -E 'require[ \t]*\"uci\"' /usr/lib/lua/luci/model/uci.lua >/dev/null 2>&1") == 0 end @@ -118,19 +135,15 @@ function exec_call(cmd) end function base64Decode(text) - local raw = text if not text then return '' end - text = text:gsub("%z", "") - text = text:gsub("%c", "") - text = text:gsub("_", "/") - text = text:gsub("-", "+") - local mod4 = #text % 4 - text = text .. string.sub('====', mod4 + 1) - local result = nixio.bin.b64decode(text) + local encoded = text:gsub("%z", ""):gsub("%c", ""):gsub("_", "/"):gsub("-", "+") + local mod4 = #encoded % 4 + encoded = encoded .. string.sub('====', mod4 + 1) + local result = nixio.bin.b64decode(encoded) if result then return result:gsub("%z", "") else - return raw + return text end end @@ -229,9 +242,13 @@ function url(...) return require "luci.dispatcher".build_url(url) end -function trim(text) - if not text or text == "" then return "" end - return text:match("^%s*(.-)%s*$") +function trim(s) + local len = #s + local i, j = 1, len + while i <= len and s:byte(i) <= 32 do i = i + 1 end + while j >= i and s:byte(j) <= 32 do j = j - 1 end + if i > j then return "" end + return s:sub(i, j) end function split(full, sep) @@ -450,6 +467,8 @@ end function get_valid_nodes() local show_node_info = uci_get_type("global_other", "show_node_info") or "0" local nodes = {} + local default_nodes = {} + local other_nodes = {} uci:foreach(appname, "nodes", function(e) e.id = e[".name"] if e.type and e.remarks then @@ -458,7 +477,11 @@ function get_valid_nodes() if type == "sing-box" then type = "Sing-Box" end e["remark"] = "%s:[%s] " % {type .. " " .. i18n.translatef(e.protocol), e.remarks} e["node_type"] = "special" - nodes[#nodes + 1] = e + if not e.group or e.group == "" then + default_nodes[#default_nodes + 1] = e + else + other_nodes[#other_nodes + 1] = e + end end local port = e.port or e.hysteria_hop or e.hysteria2_hop if port and e.address then @@ -498,11 +521,17 @@ function get_valid_nodes() e["remark"] = "%s:[%s] %s:%s" % {type, e.remarks, address, port} end e.node_type = "normal" - nodes[#nodes + 1] = e + if not e.group or e.group == "" then + default_nodes[#default_nodes + 1] = e + else + other_nodes[#other_nodes + 1] = e + end end end end end) + for i = 1, #default_nodes do nodes[#nodes + 1] = default_nodes[i] end + for i = 1, #other_nodes do nodes[#nodes + 1] = other_nodes[i] end return nodes end diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 503a8dbc8..02cf298a3 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -151,6 +151,7 @@ function gen_outbound(flag, node, tag, proxy_table) serverName = node.tls_serverName, allowInsecure = (node.tls_allowInsecure == "1") and true or false, fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil, + pinnedPeerCertificateChainSha256 = node.tls_chain_fingerprint and { node.tls_chain_fingerprint } or nil, echConfigList = (node.ech == "1") and node.ech_config or nil, echForceQuery = (node.ech == "1") and (node.ech_ForceQuery or "none") or nil } or nil, @@ -218,7 +219,7 @@ function gen_outbound(flag, node, tag, proxy_table) host = node.xhttp_host, -- If the code contains an "extra" section, retrieve the contents of "extra"; otherwise, assign the value directly to "extra". extra = node.xhttp_extra and (function() - local success, parsed = pcall(jsonc.parse, node.xhttp_extra) + local success, parsed = pcall(jsonc.parse, api.base64Decode(node.xhttp_extra)) if success then return parsed.extra or parsed else diff --git a/luci-app-passwall2/luasrc/view/passwall2/log/log.htm b/luci-app-passwall2/luasrc/view/passwall2/log/log.htm index 0df1e39ce..4e6714ed7 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/log/log.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/log/log.htm @@ -3,13 +3,19 @@ local api = require "luci.passwall2.api" -%> <% if api.is_js_luci() then -%> @@ -102,11 +195,7 @@ table td, .table .td { function cbi_t_switch(section, tab) { if( cbi_t[section] && cbi_t[section][tab] ) { // Before switching tabs, first deselect all currently active tabs. - var btn = document.getElementById("select_all_btn"); - if (btn) { - dechecked_all_node(btn); - } - + dechecked_all_node(); var o = cbi_t[section][tab]; var h = document.getElementById('tab.' + section); for( var tid in cbi_t[section] ) { @@ -131,10 +220,7 @@ table td, .table .td { if (typeof(cbi_t_switch) === "function") { var old_switch = cbi_t_switch; cbi_t_switch = function(section, tab) { - var btn = document.getElementById("select_all_btn"); - if (btn) { - dechecked_all_node(btn); - } + dechecked_all_node(); return old_switch(section, tab); }; } @@ -225,47 +311,70 @@ table td, .table .td { document.getElementById("set_node_div").style.display="none"; document.getElementById("set_node_name").innerHTML = ""; } - - function _cbi_row_top(id) { - //It has been damaged and awaits repair or other solutions. - var dom = document.getElementById("cbi-passwall2-" + id); - if (dom) { - var trs = document.getElementById("cbi-passwall2-nodes").getElementsByClassName("cbi-section-table-row"); - if (trs && trs.length > 0) { - for (var i = 0; i < trs.length; i++) { - var up = dom.getElementsByClassName("cbi-button-up"); - if (up) { - cbi_row_swap(up[0], true, 'cbi.sts.passwall2.nodes'); - } - } - } + + function row_top(btn) { + const row = btn.closest("tr"); + if (!row) return; + const parent = row.parentNode; + let firstDataRow = parent.querySelector("tr:not(.cbi-section-table-titles)"); + if (firstDataRow && firstDataRow !== row) { + parent.insertBefore(row, firstDataRow); } } - + + function set_select_all_state(sectionChecked) { + var visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]'); + if (!visibleContainer) return; + var nodes = visibleContainer.getElementsByClassName("nodes_select"); + var selectAllChk = visibleContainer.querySelector(".nodes_select_all"); + var selectAllBtn = document.getElementById("select_all_btn"); + for (var i = 0; i < nodes.length; i++) { + nodes[i].checked = sectionChecked; + } + if (selectAllChk) { + selectAllChk.checked = sectionChecked; + selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>"; + selectAllChk.setAttribute("onclick", sectionChecked ? "dechecked_all_node(this)" : "checked_all_node(this)"); + } + if (selectAllBtn) { + selectAllBtn.value = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>"; + selectAllBtn.setAttribute("onclick", sectionChecked ? "dechecked_all_node(this)" : "checked_all_node(this)"); + } + } + function checked_all_node(btn) { - var visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]'); - if (!visibleContainer) return; - var doms = visibleContainer.getElementsByClassName("nodes_select"); - if (doms && doms.length > 0) { - for (var i = 0 ; i < doms.length; i++) { - doms[i].checked = true; - } - btn.value = "<%:DeSelect all%>"; - btn.setAttribute("onclick", "dechecked_all_node(this)"); - } + set_select_all_state(true); } - + function dechecked_all_node(btn) { + set_select_all_state(false); + } + + function update_select_state() { var visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]'); if (!visibleContainer) return; - var doms = visibleContainer.getElementsByClassName("nodes_select"); - if (doms && doms.length > 0) { - for (var i = 0 ; i < doms.length; i++) { - doms[i].checked = false; - } - btn.value = "<%:Select all%>"; - btn.setAttribute("onclick", "checked_all_node(this)"); + var nodes = visibleContainer.getElementsByClassName("nodes_select"); + if (!nodes.length) return; + var selectAllChk = visibleContainer.querySelector(".nodes_select_all"); + var selectAllBtn = document.getElementById("select_all_btn"); + var checkedCount = 0; + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].checked) checkedCount++; } + var allChecked = checkedCount === nodes.length; + var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>"; + var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)"; + + function updateElement(el) { + if (!el) return; + if ("checked" in el) el.checked = allChecked; + if ("title" in el) el.title = title; + if ("value" in el) el.value = title; + el.setAttribute("onclick", onclickFunc); + } + + updateElement(selectAllChk); + updateElement(selectAllBtn); } function delete_select_nodes() { @@ -321,6 +430,66 @@ table td, .table .td { return { address: address, port: port }; } + function get_node_order(group) { + let table = document.getElementById("cbi-passwall2-nodes-" + group + "-table"); + if (!table) { + return; + } + let rows = table.querySelectorAll("tr.cbi-section-table-row"); + if (!rows || rows.length === 0) { + return; + } + var ids = []; + rows.forEach(function(row) { + var id = row.id.replace("cbi-passwall2-", ""); + ids.push(id); + }); + return ids; + } + + function save_current_page_order(group) { + var table = document.getElementById("cbi-passwall2-nodes-" + group + "-table"); + if (!table) { + alert("<%:No table!%>"); + return; + } + var 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 = []; + rows.forEach(function(row) { + var id = row.id.replace("cbi-passwall2-", ""); + ids.push(id); + }); + 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!%>"); + } + } + ); + } + function get_now_use_node() { XHR.get('<%=api.url("get_now_use_node")%>', null, function(x, result) { @@ -505,6 +674,79 @@ table td, .table .td { } } } + + function arraysEqual(a, b) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; + } + + // List drag and rearrange + function initSortableForTable(table) { + if (!table) return null; + let group = table.id.replace("cbi-passwall2-nodes-", "").replace("-table", "") + var root = table.querySelector('tbody') || table; + if (root._sortable_initialized) return root._sortable_instance; + root._sortable_initialized = true; + var opts = { + handle: ".drag-handle", + draggable: "tr.cbi-section-table-row", + animation: 150, + ghostClass: "dragging-row", + fallbackOnBody: true, + forceFallback: false, + swapThreshold: 0.65, + onEnd: function (evt) { + //save_current_page_order(group); // Auto save + let save_order_btn = document.getElementById("save_order_btn_" + group); + if (save_order_btn) { + const new_order = get_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"; + } + } + } + }; + try { + var instance = Sortable.create(root, opts); + root._sortable_instance = instance; + return instance; + } catch (err) { + root._sortable_initialized = false; + console.error("Sortable init failed:", err); + return null; + } + } + + function initAllSortable(group_nodes) { + if (typeof Sortable === 'undefined') { + var retries = 0; + var maxRetries = 25; + var t = setInterval(function () { + retries++; + if (typeof Sortable !== 'undefined') { + clearInterval(t); + for (var group in group_nodes) { + var table = document.getElementById("cbi-passwall2-nodes-" + group + "-table"); + initSortableForTable(table); + } + } else if (retries >= maxRetries) { + clearInterval(t); + } + }, 200); + } else { + for (var group in group_nodes) { + var table = document.getElementById("cbi-passwall2-nodes-" + group + "-table"); + initSortableForTable(table); + } + } + } @@ -512,6 +754,9 @@ table td, .table .td {
+ @@ -522,6 +767,7 @@ table td, .table .td {
+ + <%:Remarks%> Ping TCPing
+
@@ -531,19 +777,21 @@ table td, .table .td { - {{remarks}} - {{ping}} - {{tcping}} - {{url_test}} - + + + + {{remarks}} + {{ping}} + {{tcping}} + {{url_test}} +
- - - + /{{id}}'" alt="<%:Edit%>" title="<%:Edit%>"> +
@@ -705,6 +953,13 @@ table td, .table .td { cbi_t_switch("passwall2.nodes", default_group) } + origin_group_node_order = {}; + for (let group in group_nodes) { + origin_group_node_order[group] = get_node_order(group); + } + + initAllSortable(group_nodes); + //clear expire data if (localStorage && localStorage.length > 0) { const now = Date.now(); diff --git a/luci-app-passwall2/luasrc/view/passwall2/server/log.htm b/luci-app-passwall2/luasrc/view/passwall2/server/log.htm index 4bb5c2dad..28d9dd259 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/server/log.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/server/log.htm @@ -3,13 +3,19 @@ local api = require "luci.passwall2.api" -%>