update 2026-10-24 16:34:19

This commit is contained in:
action committed 2026-10-24 16:34:19 +08:00
1 parent b2346e5950
commit 6d61e8bf50
36 files changed
+3356 -23

No files matched your search

@@ -0,0 +1,773 @@
/*
* Click-to-sort headers and a per-group filter bar for the node list.
*
* Sort reorders rows inside one group table and calls set_order(), the same
* path as drag-and-drop and To Top. Nothing is written until Save & Apply.
*
* A filter only toggles the pw-row-hidden class. Rows stay in the form, so
* Save & Apply still posts every node and the full group.order value.
*
* Translated strings are read from #pw-nodesort-i18n (the node list template).
* The template calls init() once, then enhanceAll() after loadNodeList()
* has built the group tables.
*
* auto_detection_time is a top-level let in the template, not a window
* property. It is read by name so rows are not moved while automatic Ping
* or TCPing is still writing results by row position.
*/
(function () {
'use strict';
var COLUMNS = {
'pw-remark': 'name',
'pw-ping': 'ping',
'pw-tcping': 'tcping',
'pw-urltest': 'urltest'
};
/* sub controls drawn inside the Remarks header: [sort key, label key or symbol, title key] */
var NAME_EXTRA = [
['type', 'type', 'typeHint'],
['addr', 'addr', 'addrHint'],
['orig', '\u21BA', 'origHint']
];
var state = {
app: 'passwall',
root: null,
text: {},
bound: false
};
var searchTimer = null;
function T(key) {
var value = state.text[key];
return (value == null || value === '') ? key : value;
}
function readText(box) {
var text = {};
var spans = box.querySelectorAll('[data-k]');
for (var i = 0; i < spans.length; i++)
text[spans[i].getAttribute('data-k')] = spans[i].textContent;
return text;
}
var collator = null;
function prepareCollator() {
var lang = document.documentElement.lang || (navigator.language || '');
try {
collator = (window.Intl && Intl.Collator) ? new Intl.Collator(lang || undefined, { numeric: true, sensitivity: 'base' }) : null;
} catch (e) {
collator = null;
}
}
function cmpText(a, b) {
if (collator)
return collator.compare(a, b);
return a < b ? -1 : (a > b ? 1 : 0);
}
/* "123 ms", "12.5 ms", "1.2 s" -> milliseconds. Timeout, Error, ---, Test stay null. */
function parseLatency(text) {
var t = String(text || '').replace(/\s+/g, ' ').trim();
var m = /^(\d+(?:[.,]\d+)?)\s*(ms|s)?$/i.exec(t);
if (!m)
return null;
var v = parseFloat(m[1].replace(',', '.'));
if (isNaN(v))
return null;
if (m[2] && /^s$/i.test(m[2]))
v *= 1000;
return v;
}
function hiddenVal(id, opt) {
var el = document.getElementById('cbid.' + state.app + '.' + id + '.' + opt);
return el && el.value != null ? String(el.value).trim() : '';
}
function cellsOf(tr) {
var out = [];
for (var i = 0; i < tr.children.length; i++) {
var tag = tr.children[i].tagName;
if (tag === 'TD' || tag === 'TH')
out.push(tr.children[i]);
}
return out;
}
var wordSplit;
try {
wordSplit = new RegExp('[^\\p{L}\\p{N}]+', 'u');
} catch (e) {
wordSplit = /[\s.,;:!?|\/\\()\[\]{}<>"'`~@#$%^&*=+_-]+/;
}
function wordsOf(text) {
var seen = {};
var out = [];
var parts = String(text || '').split(wordSplit);
for (var i = 0; i < parts.length; i++) {
var w = parts[i];
if (w.length < 2 || /^\d+$/.test(w))
continue;
var k = w.toLowerCase();
if (!seen[k]) {
seen[k] = 1;
out.push(w);
}
}
return out;
}
function infoOf(table) {
var prefix = 'cbi-' + state.app + '-nodes-';
var id = table && table.id ? table.id : '';
if (id.indexOf(prefix) !== 0 || id.slice(-6) !== '-table')
return null;
return { group: id.slice(prefix.length, -6) };
}
function rowId(tr) {
var prefix = 'cbi-' + state.app + '-';
return tr.id.indexOf(prefix) === 0 ? tr.id.substring(prefix.length) : tr.id;
}
function bodyOf(table) {
return table.querySelector('tbody') || table;
}
function dataRows(table) {
var body = bodyOf(table);
var out = [];
for (var i = 0; i < body.children.length; i++) {
var tr = body.children[i];
if (tr.tagName === 'TR' && tr.classList.contains('cbi-section-table-row') && !tr.classList.contains('placeholder'))
out.push(tr);
}
return out;
}
function colIndexMap(table) {
var rows = dataRows(table);
var map = {};
if (!rows.length)
return map;
var cells = cellsOf(rows[0]);
for (var i = 0; i < cells.length; i++) {
for (var cls in COLUMNS) {
if (cells[i].classList.contains(cls))
map[COLUMNS[cls]] = i;
}
}
return map;
}
function typeOf(tr, cols) {
var cell = cellsOf(tr)[cols.name];
var text = cell ? cell.textContent.replace(/\s+/g, ' ').trim() : '';
var i = text.indexOf('\uFF1A');
if (i < 0)
i = text.indexOf(':');
return (i < 0 ? '' : text.slice(0, i)).replace(/\s+Realm$/, '').trim();
}
function sortKey(tr, key, cols) {
var id = rowId(tr);
var cell;
if (key === 'name')
return hiddenVal(id, 'remarks') || ((cellsOf(tr)[cols.name] || {}).textContent || '').trim();
if (key === 'type')
return typeOf(tr, cols) + '\u0000' + hiddenVal(id, 'remarks');
if (key === 'addr') {
var address = hiddenVal(id, 'address');
if (!address)
return null;
return address + '\u0000' + ('00000' + hiddenVal(id, 'port')).slice(-5);
}
cell = cellsOf(tr)[cols[key]];
return cell ? parseLatency(cell.textContent) : null;
}
function detectionMode() {
try {
if (typeof auto_detection_time === 'undefined')
return null;
return auto_detection_time;
} catch (e) {
return null;
}
}
/* { inflight: requests still running, left: cells still waiting for a result } */
function pingBusy() {
var mode = detectionMode();
var inflight = 0;
var left = 0;
if (mode !== 'icmp' && mode !== 'tcping')
return { inflight: 0, left: 0 };
var list = window.ajax && window.ajax.list;
if (list && list.length) {
for (var i = 0; i < list.length; i++) {
if (list[i].readyState > 0 && list[i].readyState < 4)
inflight++;
}
}
if (!state.root)
return { inflight: inflight, left: 0 };
var spans = state.root.getElementsByClassName(mode === 'tcping' ? 'tcping_value' : 'ping_value');
for (var s = 0; s < spans.length; s++) {
var id = spans[s].getAttribute('cbiid');
if (!id || spans[s].textContent.trim() !== '---')
continue;
var full = null;
if (typeof window.get_address_full === 'function')
full = window.get_address_full(id);
if (!full)
full = { address: hiddenVal(id, 'address'), port: hiddenVal(id, 'port') };
if (full.address && (mode === 'icmp' || full.port))
left++;
}
return { inflight: inflight, left: left };
}
function note(table, text) {
var id = 'pw-note-' + table.id;
var el = document.getElementById(id);
if (!text) {
if (el && el.parentNode)
el.parentNode.removeChild(el);
return;
}
if (!el) {
el = document.createElement('div');
el.id = id;
el.className = 'pw-sort-note';
table.parentNode.insertBefore(el, table);
}
el.textContent = text;
}
function setIndicator(table, th, sub, key, dir) {
var arrows = table.querySelectorAll('.pw-sort-arrow');
var subs = table.querySelectorAll('.pw-sort-sub');
var headers = table.querySelectorAll('th[data-pw-sort]');
var i;
for (i = 0; i < arrows.length; i++)
arrows[i].textContent = '';
for (i = 0; i < subs.length; i++)
subs[i].classList.remove('pw-sort-active');
for (i = 0; i < headers.length; i++) {
if (headers[i] !== th) {
delete headers[i].dataset.pwDir;
delete headers[i].dataset.pwKey;
}
}
var arrow = th.querySelector('.pw-sort-arrow');
if (arrow && key !== 'orig' && !sub)
arrow.textContent = dir === 'desc' ? '\u25BC' : '\u25B2';
if (sub && key !== 'orig')
sub.classList.add('pw-sort-active');
}
function applyOrder(table, info, rows) {
var body = bodyOf(table);
var frag = document.createDocumentFragment();
for (var i = 0; i < rows.length; i++) {
rows[i].classList.remove('cbi-rowstyle-1', 'cbi-rowstyle-2');
rows[i].classList.add('cbi-rowstyle-' + (i % 2 + 1));
frag.appendChild(rows[i]);
}
body.appendChild(frag);
if (typeof window.set_order === 'function')
window.set_order(info.group);
}
function doSort(table, th, sub, key) {
var info = infoOf(table);
var rows = dataRows(table);
var dir = 'asc';
if (!info || rows.length < 2)
return;
if (key === 'orig') {
var pos = {};
var orig = table.__pwOrig || [];
for (var n = 0; n < orig.length; n++)
pos[orig[n]] = n;
rows.sort(function (a, b) {
var pa = pos[rowId(a)];
var pb = pos[rowId(b)];
return (pa == null ? 1e9 : pa) - (pb == null ? 1e9 : pb);
});
delete th.dataset.pwDir;
delete th.dataset.pwKey;
} else {
dir = (th.dataset.pwKey === key && th.dataset.pwDir === 'asc') ? 'desc' : 'asc';
th.dataset.pwKey = key;
th.dataset.pwDir = dir;
var cols = colIndexMap(table);
var sign = dir === 'desc' ? -1 : 1;
var list = [];
for (var i = 0; i < rows.length; i++)
list.push({ tr: rows[i], i: i, k: sortKey(rows[i], key, cols) });
list.sort(function (a, b) {
var emptyA = (a.k === null || a.k === '');
var emptyB = (b.k === null || b.k === '');
if (emptyA || emptyB)
return emptyA === emptyB ? a.i - b.i : (emptyA ? 1 : -1);
var cmp = (typeof a.k === 'number' && typeof b.k === 'number') ? a.k - b.k : cmpText(String(a.k), String(b.k));
return cmp !== 0 ? sign * cmp : a.i - b.i;
});
rows = [];
for (var j = 0; j < list.length; j++)
rows.push(list[j].tr);
}
applyOrder(table, info, rows);
refreshFilter(table);
setIndicator(table, th, sub, key, dir);
note(table, key === 'orig' ? T('restored') : T('changed'));
}
function requestSort(table, th, sub, key) {
if (table.__pwWait) {
clearInterval(table.__pwWait);
table.__pwWait = null;
}
var st = pingBusy();
if (!st.inflight && !st.left) {
doSort(table, th, sub, key);
return;
}
var started = Date.now();
var idle = 0;
note(table, T('wait').replace('%d', st.left || st.inflight));
table.__pwWait = setInterval(function () {
st = pingBusy();
idle = st.inflight ? 0 : idle + 1;
if ((!st.inflight && !st.left) || idle >= 6) {
clearInterval(table.__pwWait);
table.__pwWait = null;
if (document.body.contains(table))
doSort(table, th, sub, key);
return;
}
if (Date.now() - started > 120000) {
clearInterval(table.__pwWait);
table.__pwWait = null;
note(table, T('waitFail'));
return;
}
note(table, T('wait').replace('%d', st.left || st.inflight));
}, 500);
}
function enhance(table) {
var info = infoOf(table);
if (!info || table.__pwReady)
return;
var titles = table.querySelector('tr.cbi-section-table-titles');
var rows = dataRows(table);
if (!titles || !rows.length)
return;
var cols = colIndexMap(table);
var headers = cellsOf(titles);
table.__pwReady = true;
table.__pwOrig = [];
for (var r = 0; r < rows.length; r++)
table.__pwOrig.push(rowId(rows[r]));
for (var key in cols) {
if (!Object.prototype.hasOwnProperty.call(cols, key))
continue;
var th = headers[cols[key]];
if (!th || th.classList.contains('cbi-section-actions'))
continue;
th.setAttribute('data-pw-sort', key);
th.title = T('hint');
var arrow = document.createElement('span');
arrow.className = 'pw-sort-arrow';
th.appendChild(arrow);
if (key !== 'name')
continue;
for (var s = 0; s < NAME_EXTRA.length; s++) {
var spec = NAME_EXTRA[s];
var sp = document.createElement('span');
sp.className = 'pw-sort-sub';
sp.setAttribute('data-pw-sort', spec[0]);
sp.textContent = T(spec[1]);
sp.title = T(spec[2]);
th.appendChild(sp);
}
}
}
function rowVisible(el) {
var row = el && el.closest ? el.closest('tr') : null;
return !row || !row.classList.contains('pw-row-hidden');
}
function filterDefs(rows, cols) {
return [
{
key: 'type',
label: T('type'),
multi: false,
get: function (tr) { return typeOf(tr, cols); }
},
{
key: 'word',
label: T('fWord'),
multi: true,
minCount: 2,
max: 40,
get: function (tr) { return wordsOf(hiddenVal(rowId(tr), 'remarks')); }
}
];
}
function searchText(tr, cols) {
var id = rowId(tr);
return [
typeOf(tr, cols),
hiddenVal(id, 'remarks'),
hiddenVal(id, 'address') + ':' + hiddenVal(id, 'port')
].join(' ');
}
function collect(table) {
var rows = dataRows(table);
var cols = colIndexMap(table);
var defs = rows.length >= 2 ? filterDefs(rows, cols) : [];
var shown = [];
for (var d = 0; d < defs.length; d++) {
var def = defs[d];
var counts = {};
var labels = {};
var vals = [];
for (var r = 0; r < rows.length; r++) {
var raw = def.get(rows[r]);
var list = def.multi ? (raw || []) : [raw == null ? '' : String(raw)];
var keys = [];
for (var n = 0; n < list.length; n++) {
var item = list[n];
var k = def.multi ? String(item).toLowerCase() : String(item);
counts[k] = (counts[k] || 0) + 1;
if (!labels[k])
labels[k] = {};
labels[k][item] = (labels[k][item] || 0) + 1;
keys.push(k);
}
vals.push(keys);
}
var opts = [];
for (var k in counts) {
if (!Object.prototype.hasOwnProperty.call(counts, k))
continue;
if (def.multi && (counts[k] < (def.minCount || 1) || counts[k] >= rows.length))
continue;
var best = '';
var bestN = -1;
for (var label in labels[k]) {
if (labels[k][label] > bestN) {
bestN = labels[k][label];
best = label;
}
}
opts.push({ v: k, label: best === '' ? T('empty') : best, n: counts[k] });
}
opts.sort(function (a, b) {
if (def.multi && a.n !== b.n)
return b.n - a.n;
return cmpText(a.label, b.label);
});
if (def.max && opts.length > def.max)
opts = opts.slice(0, def.max);
var enough = def.multi ? opts.length >= 1 : opts.length >= 2;
if (!enough)
continue;
def.vals = vals;
def.opts = opts;
shown.push(def);
}
var search = rows.length >= 2;
var sig = String(rows.length) + (search ? 's' : '');
for (var i = 0; i < shown.length; i++) {
sig += '|' + shown[i].key + '=';
for (var o = 0; o < shown[i].opts.length; o++)
sig += shown[i].opts[o].v + ':' + shown[i].opts[o].n + ',';
}
return { rows: rows, cols: cols, defs: shown, search: search, sig: sig };
}
function barId(table) {
return 'pw-filter-' + table.id;
}
function buildBar(table, collected) {
var st = table.__pwF || (table.__pwF = { sel: {}, q: '' });
var id = barId(table);
var bar = document.getElementById(id);
if (!bar) {
bar = document.createElement('div');
bar.id = id;
bar.className = 'pw-filter';
bar.title = T('filterHint');
table.parentNode.insertBefore(bar, table);
bar.__pwTable = table;
}
while (bar.firstChild)
bar.removeChild(bar.firstChild);
var head = document.createElement('span');
head.textContent = T('filter');
bar.appendChild(head);
var keep = {};
for (var d = 0; d < collected.defs.length; d++) {
var def = collected.defs[d];
var lab = document.createElement('label');
var sel = document.createElement('select');
lab.appendChild(document.createTextNode(def.label));
sel.className = 'cbi-input-select';
sel.setAttribute('data-pw-filter', def.key);
var all = document.createElement('option');
all.value = '';
all.textContent = T('all') + ' (' + collected.rows.length + ')';
sel.appendChild(all);
for (var o = 0; o < def.opts.length; o++) {
var op = document.createElement('option');
op.value = 'v:' + def.opts[o].v;
op.textContent = def.opts[o].label + ' (' + def.opts[o].n + ')';
sel.appendChild(op);
}
var cur = st.sel[def.key] || '';
var known = false;
if (cur) {
for (var c = 0; c < def.opts.length; c++) {
if (('v:' + def.opts[c].v) === cur) {
known = true;
break;
}
}
}
if (!known)
cur = '';
sel.value = cur;
if (cur)
keep[def.key] = cur;
lab.appendChild(sel);
bar.appendChild(lab);
}
st.sel = keep;
if (collected.search) {
var input = document.createElement('input');
input.type = 'search';
input.className = 'cbi-input-text';
input.placeholder = T('search');
input.title = T('searchHint');
input.autocomplete = 'off';
input.setAttribute('data-pw-search', '1');
input.value = st.q;
bar.appendChild(input);
} else {
st.q = '';
}
var count = document.createElement('span');
count.className = 'pw-filter-count';
bar.appendChild(count);
var reset = document.createElement('button');
reset.type = 'button';
reset.className = 'btn cbi-button';
reset.setAttribute('data-pw-reset', '1');
reset.textContent = T('reset');
reset.title = T('resetHint');
bar.appendChild(reset);
return bar;
}
function listHas(list, value) {
for (var i = 0; i < list.length; i++) {
if (list[i] === value)
return true;
}
return false;
}
function applyFilter(table, collected, bar) {
var st = table.__pwF || { sel: {}, q: '' };
var q = String(st.q || '').toLowerCase().trim();
var active = !!q;
var shown = 0;
var d;
for (d = 0; d < collected.defs.length; d++) {
if (st.sel[collected.defs[d].key])
active = true;
}
for (var i = 0; i < collected.rows.length; i++) {
var tr = collected.rows[i];
var ok = true;
for (d = 0; d < collected.defs.length; d++) {
var sel = st.sel[collected.defs[d].key];
if (ok && sel && !listHas(collected.defs[d].vals[i], sel.slice(2)))
ok = false;
}
if (ok && q && searchText(tr, collected.cols).toLowerCase().indexOf(q) < 0)
ok = false;
if (ok !== !tr.classList.contains('pw-row-hidden'))
tr.classList.toggle('pw-row-hidden', !ok);
if (ok) {
shown++;
var want = 'cbi-rowstyle-' + ((shown - 1) % 2 + 1);
var other = want === 'cbi-rowstyle-1' ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1';
if (!tr.classList.contains(want))
tr.classList.add(want);
if (tr.classList.contains(other))
tr.classList.remove(other);
} else {
var cb = tr.querySelector('input.nodes_select');
if (cb && cb.checked) {
cb.checked = false;
}
}
}
if (typeof window.update_select_state === 'function')
window.update_select_state();
var count = bar.querySelector('.pw-filter-count');
var txt = T('shown').replace('%s', shown).replace('%t', collected.rows.length);
if (count && count.textContent !== txt)
count.textContent = txt;
if (bar.classList.contains('pw-filter-on') !== active)
bar.classList.toggle('pw-filter-on', active);
}
function removeBar(table) {
var bar = document.getElementById(barId(table));
if (bar && bar.parentNode)
bar.parentNode.removeChild(bar);
var rows = dataRows(table);
for (var i = 0; i < rows.length; i++) {
if (rows[i].classList.contains('pw-row-hidden'))
rows[i].classList.remove('pw-row-hidden');
}
table.__pwSig = null;
}
function refreshFilter(table) {
if (!infoOf(table))
return;
var collected = collect(table);
if (!collected.defs.length && !collected.search) {
if (table.__pwSig)
removeBar(table);
return;
}
var bar = document.getElementById(barId(table));
if (!bar || collected.sig !== table.__pwSig) {
bar = buildBar(table, collected);
table.__pwSig = collected.sig;
}
applyFilter(table, collected, bar);
}
function barTable(el) {
var bar = el && el.closest ? el.closest('.pw-filter') : null;
return bar && bar.__pwTable ? bar.__pwTable : null;
}
function eventTarget(ev) {
var t = ev.target;
if (t && t.nodeType !== 1)
t = t.parentNode;
return t;
}
function bind(root) {
root.addEventListener('click', function (ev) {
var t = eventTarget(ev);
if (t && t.getAttribute && t.getAttribute('data-pw-reset')) {
var resetTable = barTable(t);
if (!resetTable)
return;
ev.preventDefault();
resetTable.__pwF = { sel: {}, q: '' };
resetTable.__pwSig = null;
refreshFilter(resetTable);
return;
}
var el = t && t.closest ? t.closest('[data-pw-sort]') : null;
if (!el || !root.contains(el))
return;
if (t.closest && t.closest('input, a, button, select'))
return;
var th = el.closest('th');
var table = el.closest('table');
if (!th || !table || !infoOf(table))
return;
ev.preventDefault();
ev.stopPropagation();
requestSort(table, th, el === th ? null : el, el.getAttribute('data-pw-sort'));
});
root.addEventListener('change', function (ev) {
var t = eventTarget(ev);
if (!t || !t.getAttribute || !t.getAttribute('data-pw-filter'))
return;
var table = barTable(t);
if (!table)
return;
(table.__pwF = table.__pwF || { sel: {}, q: '' }).sel[t.getAttribute('data-pw-filter')] = t.value;
refreshFilter(table);
});
root.addEventListener('input', function (ev) {
var t = eventTarget(ev);
if (!t || !t.getAttribute || !t.getAttribute('data-pw-search'))
return;
var table = barTable(t);
if (!table)
return;
clearTimeout(searchTimer);
searchTimer = setTimeout(function () {
(table.__pwF = table.__pwF || { sel: {}, q: '' }).q = t.value;
refreshFilter(table);
}, 150);
});
root.addEventListener('keydown', function (ev) {
var t = eventTarget(ev);
if (ev.key === 'Enter' && t && t.getAttribute && t.getAttribute('data-pw-search'))
ev.preventDefault();
});
}
function init() {
var box = document.getElementById('pw-nodesort-i18n');
var root = document.getElementById('node_list');
if (!box || !root)
return;
state.app = box.getAttribute('data-app') || 'passwall';
state.text = readText(box);
state.root = root;
prepareCollator();
if (!state.bound) {
bind(root);
state.bound = true;
}
}
function enhanceAll() {
if (!state.root)
return;
var tables = state.root.querySelectorAll('table.cbi-section-table');
for (var i = 0; i < tables.length; i++) {
try {
enhance(tables[i]);
refreshFilter(tables[i]);
} catch (e) {
if (window.console)
console.warn('passwall node list sort:', e);
}
}
}
window.pwNodeSort = {
init: init,
enhanceAll: enhanceAll,
rowVisible: rowVisible
};
})();
@@ -579,11 +579,15 @@ function gen_config_server(node)
end
end
if node.protocol == "vmess" or node.protocol == "vless" then
if node.protocol == "vless" then
settings = {
users = users,
decryption = (node.decryption and node.decryption ~= "") and node.decryption or "none"
}
elseif node.protocol == "vmess" then
if users then
settings = {
users = users,
decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil
users = users
}
end
elseif node.protocol == "socks" then
@@ -24,6 +24,7 @@ if node then
end
-%>
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js?v=1.15.7"></script>
<script src="<%=resource%>/view/<%=appname%>/node_list_sort.js"></script>
<style>
table th, .table .th {
@@ -385,6 +386,89 @@ table td, .table .td {
}
</style>
<style>
/* node list: click-to-sort headers and the per-group filter bar */
th[data-pw-sort] {
cursor: pointer;
user-select: none;
white-space: nowrap;
}
th[data-pw-sort]:hover {
text-decoration: underline;
}
.pw-sort-arrow {
display: inline-block;
min-width: 1em;
margin-left: 0.25em;
font-size: 90%;
}
.pw-sort-sub {
font-weight: normal;
font-size: 85%;
opacity: 0.85;
margin-left: 0.6em;
cursor: pointer;
text-decoration: underline dotted;
white-space: nowrap;
}
.pw-sort-sub.pw-sort-active {
opacity: 1;
font-weight: bold;
}
.pw-sort-note {
margin: 0.3em 0;
padding: 0.3em 0.6em;
border-radius: 3px;
background: rgba(251, 154, 5, 0.15);
font-size: 90%;
}
tr.pw-row-hidden {
display: none !important;
}
.pw-filter {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.3em 0.8em;
margin: 0.4em 0;
font-size: 90%;
}
.pw-filter label {
display: inline-flex;
align-items: center;
gap: 0.3em;
white-space: nowrap;
}
.pw-filter select,
.pw-filter input[type="search"],
.pw-filter .cbi-button {
width: auto !important;
min-width: 6em;
max-width: 22em;
height: auto;
padding: 0.15em 0.3em;
margin: 0;
}
.pw-filter .pw-filter-count {
opacity: 0.8;
}
.pw-filter.pw-filter-on .pw-filter-count {
font-weight: bold;
opacity: 1;
}
</style>
<script type="text/javascript">
<% if is_js_luci then -%>
var cbi_t = [];
@@ -540,15 +624,29 @@ table td, .table .td {
}
}
function node_row_visible(el) {
if (window.pwNodeSort && typeof pwNodeSort.rowVisible === "function")
return pwNodeSort.rowVisible(el);
var row = el && el.closest ? el.closest("tr") : null;
return !row || !row.classList.contains("pw-row-hidden");
}
function set_select_all_state(sectionChecked) {
var visibleContainer = document.querySelector('#cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-<%=appname%>-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");
var visibleCount = 0;
for (var i = 0; i < nodes.length; i++) {
if (!node_row_visible(nodes[i])) {
nodes[i].checked = false;
continue;
}
nodes[i].checked = sectionChecked;
visibleCount++;
}
sectionChecked = sectionChecked && visibleCount > 0;
if (selectAllChk) {
selectAllChk.checked = sectionChecked;
selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
@@ -576,10 +674,13 @@ table td, .table .td {
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
var selectAllBtn = document.getElementById("select_all_btn");
var checkedCount = 0;
var visibleCount = 0;
for (var i = 0; i < nodes.length; i++) {
if (!node_row_visible(nodes[i])) continue;
visibleCount++;
if (nodes[i].checked) checkedCount++;
}
var allChecked = checkedCount === nodes.length;
var allChecked = visibleCount > 0 && checkedCount === visibleCount;
var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)";
@@ -602,7 +703,7 @@ table td, .table .td {
var doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (var i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
if (doms[i].checked && node_row_visible(doms[i])) {
ids.push(doms[i].getAttribute("cbid"))
}
}
@@ -1163,7 +1264,7 @@ table td, .table .td {
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
if (doms[i].checked && node_row_visible(doms[i])) {
ids.push(doms[i].getAttribute("cbid"))
}
}
@@ -1195,7 +1296,7 @@ table td, .table .td {
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
if (doms[i].checked && node_row_visible(doms[i])) {
ids.push(doms[i].getAttribute("cbid"))
}
}
@@ -1307,8 +1408,33 @@ table td, .table .td {
<<%=section_tag%> class="cbi-section" id="node_list">
</<%=section_tag%>>
<div id="pw-nodesort-i18n" data-app="<%=appname%>" style="display:none">
<span data-k="hint"><%:Click to sort; click again to reverse. Press Save & Apply to keep the order.%></span>
<span data-k="typeHint"><%:Sort by type or protocol, then by name%></span>
<span data-k="addrHint"><%:Sort by server address and port%></span>
<span data-k="origHint"><%:Restore the saved order%></span>
<span data-k="changed"><%:The order changed on this page only. Press Save & Apply to store it on the router.%></span>
<span data-k="restored"><%:Saved order restored.%></span>
<span data-k="wait"><%:Automatic ping is still running (%d left). Sorting will run when it finishes.%></span>
<span data-k="waitFail"><%:Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again.%></span>
<span data-k="filter"><%:Filter:%></span>
<span data-k="all"><%:All%></span>
<span data-k="empty"><%:(empty)%></span>
<span data-k="type"><%:Type%></span>
<span data-k="addr"><%:Address%></span>
<span data-k="fWord"><%:Word in remarks%></span>
<span data-k="search"><%:search...%></span>
<span data-k="searchHint"><%:Show only rows that contain this text%></span>
<span data-k="shown"><%:Shown %s of %t%></span>
<span data-k="reset"><%:Reset%></span>
<span data-k="resetHint"><%:Show all rows%></span>
<span data-k="filterHint"><%:The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows.%></span>
</div>
<script type="text/javascript">
//init logic
if (window.pwNodeSort)
pwNodeSort.init();
function get_remarks_name(o) {
let str = "";
@@ -1511,6 +1637,8 @@ table td, .table .td {
get_now_use_node();
pingAllNodes();
if (window.pwNodeSort)
pwNodeSort.enhanceAll();
});
}
+54
View File
@@ -2006,3 +2006,57 @@ msgstr "WARP 设备私钥"
msgid "WARP Endpoint Public Key"
msgstr "WARP 服务端公钥"
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
msgstr "点击表头排序,再次点击切换方向。按保存并应用可保留顺序。"
msgid "Sort by type or protocol, then by name"
msgstr "先按类型或协议排序,再按名称排序"
msgid "Sort by server address and port"
msgstr "按服务器地址和端口排序"
msgid "Restore the saved order"
msgstr "恢复已保存的顺序"
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
msgstr "顺序仅在本页改变。按保存并应用才会写入路由器。"
msgid "Saved order restored."
msgstr "已恢复保存的顺序。"
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
msgstr "自动探测仍在进行(剩余 %d)。结束后会立即排序。"
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
msgstr "自动探测在 2 分钟内未结束,已取消排序。请刷新页面后重试。"
msgid "Filter:"
msgstr "筛选:"
msgid "(empty)"
msgstr "(空)"
msgid "Word in remarks"
msgstr "备注关键词"
msgid "Address"
msgstr "地址"
msgid "search..."
msgstr "搜索..."
msgid "Show only rows that contain this text"
msgstr "只显示包含此文本的行"
msgid "Shown %s of %t"
msgstr "显示 %s / %t"
msgid "Reset"
msgstr "重置"
msgid "Show all rows"
msgstr "显示全部行"
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
msgstr "筛选只在本页隐藏行,不会改动路由器上的设置。全选会跳过隐藏行。"