🔥 Sync 2026-10-24 23:19:23

This commit is contained in:
github-actions[bot] committed 2026-10-24 23:19:23 +08:00
1 parent 9886197604
commit 37e1475e87
59 files changed
+5167 -1483

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.22
PKG_RELEASE:=347
PKG_RELEASE:=348
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -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
};
})();
@@ -78,7 +78,7 @@ if has_xray or has_singbox then
o.default = 0
o.rmempty = false
o.description = translate("Generate rule lists such as GFW, China domains, and China IP ranges based on Geo files.") .. "<br><font color='red'>" ..
translate("When manually updating with this option enabled, rules will be regenerated from existing Geo files even if no new version is available.") .. "</font>"
translate("GeoSite files without GFW rules cannot generate a GFW list (e.g. the original v2fly/Xray files).") .. "</font>"
o = s:option(Flag, "enable_geoview", translate("Enable Geo Data Parsing"))
o.default = 0
@@ -1692,8 +1692,21 @@ function gen_config(var)
local bin = api.finded_com("geoview")
if bin then
local geo_file = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat"
if luci.sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" | grep -q "^GFW$"') == 0 then
domain_list = (domain_list == "") and "geosite:gfw" or domain_list .. "\ngeosite:gfw"
local gfw_rule
if sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" 2>/dev/null | grep -q "^GFW$"') == 0 then
gfw_rule = "geosite:gfw"
elseif api.compare_versions(api.get_app_version("geoview"), ">=", "0.2.8") then
local output = GEO_VAR.TO_SRS_PATH .. "geosite-gfw.srs"
if not fs.access(output) then
fs.mkdirr(GEO_VAR.TO_SRS_PATH)
local cmd = string.format("%s -type geosite -action convert -input /usr/share/passwall/rules/gfwlist -list '*' -output %s -format srs",
api.util.shellquote(bin), api.util.shellquote(output))
if api.exec_call(cmd) ~= 0 then fs.unlink(output) end
end
gfw_rule = fs.access(output) and ("rule-set:local:" .. output) or nil
end
if gfw_rule then
domain_list = (domain_list == "") and gfw_rule or domain_list .. "\n" .. gfw_rule
end
end
+24 -10
View File
@@ -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
@@ -1479,8 +1483,21 @@ function gen_config(var)
local bin = api.finded_com("geoview")
if bin then
local geo_file = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat"
if luci.sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" | grep -q "^GFW$"') == 0 then
domain_list = (domain_list == "") and "geosite:gfw" or domain_list .. "\ngeosite:gfw"
local gfw_rule
if sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" 2>/dev/null | grep -q "^GFW$"') == 0 then
gfw_rule = "geosite:gfw"
elseif api.compare_versions(api.get_app_version("geoview"), ">=", "0.2.8") then
local output = api.CACHE_PATH .. "/geodata/gfwlist.dat"
if not fs.access(output) then
fs.mkdirr(api.CACHE_PATH .. "/geodata")
local cmd = string.format("%s -type geosite -action convert -input /usr/share/passwall/rules/gfwlist -list '*' -output %s -format geosite",
api.util.shellquote(bin), api.util.shellquote(output))
if api.exec_call(cmd) ~= 0 then fs.unlink(output) end
end
gfw_rule = fs.access(output) and "ext:gfwlist.dat:all" or nil
end
if gfw_rule then
domain_list = (domain_list == "") and gfw_rule or domain_list .. "\n" .. gfw_rule
end
end
@@ -1539,7 +1556,7 @@ function gen_config(var)
w = api.trim(w)
if w == "" or w:find("#") == 1 then return end
if w:find("rule-set:", 1, true) == 1 or w:find("rs:") == 1 then return end
if w:find("ext:", 1, true) == 1 then return end -- Rule currently not support ext:
if w:find("ext:", 1, true) == 1 and not (e[".name"] == "GFW_Mode_List" and w == "ext:gfwlist.dat:all") then return end
table.insert(domains, w)
table.insert(domain_table.domain, w)
end)
@@ -2051,10 +2068,7 @@ function gen_config(var)
if inbounds or outbounds then
local config = {
env = (function()
local asset_location = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"
return { XRAY_LOCATION_ASSET = asset_location }
end)(),
env = { XRAY_LOCATION_ASSET = api.CACHE_PATH .. "/geodata" },
log = {
-- error = string.format("/tmp/etc/passwall/%s.log", node[".name"]),
loglevel = get_log_level(loglevel)
@@ -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();
});
}
+56 -2
View File
@@ -974,8 +974,8 @@ msgstr "从 Geo 文件生成规则"
msgid "Generate rule lists such as GFW, China domains, and China IP ranges based on Geo files."
msgstr "根据 Geo 文件生成规则列表,包括 GFW、中国域名和中国 IP 段等。"
msgid "When manually updating with this option enabled, rules will be regenerated from existing Geo files even if no new version is available."
msgstr "启用此选项后手动更新时,即使没有新版本也会从现有 Geo 文件重新生成规则。"
msgid "GeoSite files without GFW rules cannot generate a GFW list (e.g. the original v2fly/Xray files)."
msgstr "不含 GFW 规则的 GeoSite 文件无法生成 GFW 列表(如 v2fly/Xray 原版)。"
msgid "Enable Geo Data Parsing"
msgstr "开启 Geo 数据解析"
@@ -2048,3 +2048,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 "筛选只在本页隐藏行,不会改动路由器上的设置。全选会跳过隐藏行。"
@@ -640,8 +640,8 @@ start_global() {
}
[ "$protocol" = "_shunt" ] && {
local geoip_path="${V2RAY_LOCATION_ASSET%*/}/geoip.dat"
local geosite_path="${V2RAY_LOCATION_ASSET%*/}/geosite.dat"
local geoip_path="${GEO_DIR%/}/geoip.dat"
local geosite_path="${GEO_DIR%/}/geosite.dat"
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
echolog "* 缺少Geo规则文件,TCP Sing-Box分流节点无法正常使用!"
fi
@@ -724,8 +724,8 @@ start_global() {
}
[ "$protocol" = "_shunt" ] && {
local geoip_path="${V2RAY_LOCATION_ASSET%*/}/geoip.dat"
local geosite_path="${V2RAY_LOCATION_ASSET%*/}/geosite.dat"
local geoip_path="${GEO_DIR%/}/geoip.dat"
local geosite_path="${GEO_DIR%/}/geosite.dat"
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
echolog "* 缺少Geo规则文件,TCP Xray分流节点无法正常使用!"
fi
@@ -1718,8 +1718,14 @@ start() {
mkdir -p /tmp/etc /tmp/log $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2
set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
get_config
export V2RAY_LOCATION_ASSET=$(config_n_get @global_rules[0] v2ray_location_asset "/usr/share/v2ray/")
export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET
GEO_DIR=$(config_n_get @global_rules[0] v2ray_location_asset "/usr/share/v2ray/")
export XRAY_LOCATION_ASSET="$TMP_PATH2/geodata"
mkdir -p "$XRAY_LOCATION_ASSET" 2>/dev/null
local geo_file
for geo_file in "${GEO_DIR%/}"/*.dat; do
[ -f "$geo_file" ] || continue
ln -sf "$geo_file" "$XRAY_LOCATION_ASSET/${geo_file##*/}" 2>/dev/null
done
export ENABLE_DEPRECATED_GEOSITE=true
export ENABLE_DEPRECATED_GEOIP=true
export SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN=1
@@ -1800,7 +1806,6 @@ stop() {
busybox pgrep -af "${CONFIG}/monitor\.sh" | xargs -r kill -9 >/dev/null 2>&1
busybox pgrep -f "sleep.*(6s|9s|58s)" | xargs -r kill -9 >/dev/null 2>&1
busybox pgrep -af "${CONFIG}/" | awk '! /app\.sh|subscribe\.lua|rule_update\.lua|tasks\.sh|server_app\.lua|ujail/{print $1}' | xargs -r kill -9 >/dev/null 2>&1
unset V2RAY_LOCATION_ASSET
unset XRAY_LOCATION_ASSET
unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN
stop_crontab "$1"
@@ -1597,6 +1597,7 @@ stop() {
rm -rf $TMP_PATH2/singbox*
rm -rf $TMP_PATH2/dnsmasq*
rm -rf $TMP_PATH2/geo_output
rm -rf $TMP_PATH2/geodata
rm -rf $TMP_PATH2/user_rules
}
flush_include
@@ -1610,6 +1610,7 @@ stop() {
rm -rf $TMP_PATH2/singbox*
rm -rf $TMP_PATH2/dnsmasq*
rm -rf $TMP_PATH2/geo_output
rm -rf $TMP_PATH2/geodata
rm -rf $TMP_PATH2/user_rules
}
flush_include