small-packages/luci-app-ssr-plus/luasrc/view/shadowsocksr/server_list.htm
2026-05-10 09:48:30 +08:00

415 lines
11 KiB
HTML

<%#
Copyright 2018-2019 Lienol <lawlienol@gmail.com>
Licensed to the public under the Apache License 2.0.
-%>
<%
require "luci.sys"
function is_js_luci()
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
end
-%>
<% if is_js_luci() then -%>
<script type="text/javascript">
var cbi_t = [];
function cbi_t_add(section, tab) {
var t = document.getElementById('tab.' + section + '.' + tab);
var c = document.getElementById('container.' + section + '.' + tab);
if( t && c ) {
cbi_t[section] = (cbi_t[section] || [ ]);
cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
}
}
function cbi_t_switch(section, tab) {
if( cbi_t[section] && cbi_t[section][tab] ) {
//在切换选项卡之前,先取消当前激活选项卡的全选状态
dechecked_all_node();
var o = cbi_t[section][tab];
var h = document.getElementById('tab.' + section);
for( var tid in cbi_t[section] ) {
var o2 = cbi_t[section][tid];
if( o.tab.id != o2.tab.id ) {
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
o2.container.style.display = 'none';
}
else {
if(h) h.value = tab;
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
o2.container.style.display = 'block';
}
}
}
return false
}
</script>
<%- else %>
<script type="text/javascript">
(function() {
if (typeof(cbi_t_switch) === "function") {
var old_switch = cbi_t_switch;
cbi_t_switch = function(section, tab) {
dechecked_all_node();
return old_switch(section, tab);
};
}
})();
</script>
<%- end %>
<script type="text/javascript">
//<![CDATA[
window.addEventListener('load', function () {
const doms = document.getElementsByClassName('pingtime');
const ports = document.getElementsByClassName("socket-connected");
const transports = document.getElementsByClassName("transport");
const wsPaths = document.getElementsByClassName("wsPath");
const wsHosts = document.getElementsByClassName("wsHost");
const tlsHosts = document.getElementsByClassName("tlsHost");
const tlss = document.getElementsByClassName("tls");
const types = document.getElementsByClassName("type");
const protos = document.getElementsByClassName("proto");
const realities = document.getElementsByClassName("reality");
const startText = '<%:Start Detection%>';
const stopText = '<%:Stop Detection%>';
const maxConcurrent = 10;
let activeRunId = 0;
let probing = false;
let queueIndex = 0;
let toggleBtn = null;
const normalizeHeaderLayout = () => {
const headerRow = document.querySelector("#cbi-shadowsocksr-servers table tr");
if (!headerRow) {
return null;
}
const headerCells = headerRow.querySelectorAll("th");
if (headerCells.length > 1) {
const first = headerCells[0];
if (first && !first.textContent.trim()) {
first.parentNode.removeChild(first);
}
}
return headerRow;
};
const ensureProbeButton = () => {
if (toggleBtn) {
return toggleBtn;
}
const headerRow = normalizeHeaderLayout();
if (!headerRow) {
return null;
}
const headerCells = headerRow.querySelectorAll("th");
const targetHeader = headerCells.length ? headerCells[headerCells.length - 1] : null;
if (!targetHeader) {
return null;
}
targetHeader.style.width = "1%";
targetHeader.style.whiteSpace = "nowrap";
targetHeader.style.textAlign = "left";
toggleBtn = document.createElement("input");
toggleBtn.type = "button";
toggleBtn.id = "ssr-probe-toggle";
toggleBtn.className = "btn cbi-button cbi-button-apply";
toggleBtn.style.marginLeft = "8px";
toggleBtn.style.minWidth = "150px";
toggleBtn.value = startText;
toggleBtn.title = startText;
targetHeader.appendChild(toggleBtn);
return toggleBtn;
};
const setProbeState = (running) => {
probing = running;
const btn = ensureProbeButton();
if (btn) {
toggleBtn = btn;
toggleBtn.value = running ? stopText : startText;
toggleBtn.title = toggleBtn.value;
}
};
const resetProbeDisplay = () => {
for (let i = 0; i < doms.length; i++) {
if (doms[i]) {
doms[i].innerHTML = 'N/A';
}
if (ports[i]) {
ports[i].innerHTML = 'N/A';
}
}
};
const xhr = (index, runId) => {
return new Promise((res) => {
const dom = doms[index];
const port = ports[index];
const transport = transports[index];
const wsPath = wsPaths[index];
const wsHost = wsHosts[index];
const tlsHost = tlsHosts[index];
const tls = tlss[index];
const type = types[index];
const proto = protos[index];
const reality = realities[index];
if (!dom || !port || runId !== activeRunId) {
res();
return;
}
if (type && type.getAttribute("hint") === "clash") {
dom.innerHTML = '<font style="color:#999999">N/A</font>';
port.innerHTML = '<font style="color:#999999">N/A</font>';
res();
return;
}
port.innerHTML = '<font style=\"color:#0072c3\">connect</font>';
XHR.get('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/ping")%>', {
index,
sid: dom.getAttribute("data-sid"),
domain: dom.getAttribute("hint"),
port: port.getAttribute("hint"),
transport: transport.getAttribute("hint"),
wsPath: wsPath.getAttribute("hint"),
host: wsHost ? wsHost.getAttribute("hint") : "",
tlsHost: tlsHost ? tlsHost.getAttribute("hint") : "",
tls: tls.getAttribute("hint"),
type: type.getAttribute("hint"),
proto: proto.getAttribute("hint"),
reality: reality ? reality.getAttribute("hint") : ""
},
(x, result) => {
if (runId !== activeRunId) {
res();
return;
}
result = result || {};
let col = '#ff0000';
if (result.ping) {
if (result.ping < 300) col = '#ff3300';
if (result.ping < 200) col = '#ff7700';
if (result.ping < 100) col = '#249400';
}
dom.innerHTML = `<font style=\"color:${col}\">${(result.ping ? result.ping : "--") + " ms"}</font>`;
if (result.socket) {
port.innerHTML = '<font style=\"color:#249400\">ok</font>';
} else {
port.innerHTML = '<font style=\"color:#ff0000\">fail</font>';
}
res();
});
});
};
const launchNext = (runId) => {
if (runId !== activeRunId || queueIndex >= doms.length) {
return Promise.resolve();
}
const index = queueIndex++;
return xhr(index, runId).then(() => launchNext(runId));
};
const startProbe = () => {
activeRunId++;
const runId = activeRunId;
queueIndex = 0;
resetProbeDisplay();
setProbeState(true);
const workers = [];
for (let i = 0; i < Math.min(maxConcurrent, doms.length); i++) {
workers.push(launchNext(runId));
}
Promise.all(workers).then(() => {
if (runId === activeRunId) {
setProbeState(false);
}
});
};
const stopProbe = () => {
activeRunId++;
setProbeState(false);
};
setProbeState(false);
ensureProbeButton();
if (toggleBtn) {
toggleBtn.onclick = function () {
if (probing) {
stopProbe();
} else {
startProbe();
}
};
}
});
function cbi_row_drop(fromId, toId, store, isToBottom) {
var fromNode = document.getElementById(fromId);
var toNode = document.getElementById(toId);
if (!fromNode || !toNode) return false;
var table = fromNode.parentNode;
while (table && table.nodeName.toLowerCase() != "table")
table = table.parentNode;
if (!table) return false;
var ids = [];
if (isToBottom) {
toNode.parentNode.appendChild(fromNode);
} else {
fromNode.parentNode.insertBefore(fromNode, toNode);
}
for (var idx = 1; idx < table.rows.length; idx++) {
table.rows[idx].className = table.rows[idx].className.replace(
/cbi-rowstyle-[12]/,
"cbi-rowstyle-" + (1 + (idx % 2))
);
if (table.rows[idx].id && table.rows[idx].id.match(/-([^\-]+)$/))
ids.push(RegExp.$1);
}
var input = document.getElementById(store);
var order = ids.join(" ");
if (input) input.value = order;
XHR.post('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/save_order")%>', {
order: order,
page: '<%=self.server_page or 1%>',
page_size: '<%=self.server_page_size or 0%>'
}, function(x, result) {
result = result || {};
if (result.ret !== 1 && result.ret !== "1") {
window.location.reload();
return;
}
if (input) {
input.value = "";
}
});
return false;
}
// set tr draggable
function enableDragForTable(table_selector, store) {
var storeInput = document.getElementById(store);
if (!storeInput) {
return;
}
// 添加 CSS 样式
const style = document.createElement("style");
style.textContent = `
tr[draggable="true"] {
cursor: move;
user-select: none;
}
.cbi-button-up,
.cbi-button-down,
.cbi-button-top,
.cbi-button-bottom {
display: none !important;
}
`;
document.head.appendChild(style);
var trs = document.querySelectorAll(table_selector + " tr");
if (!trs || trs.length < 2) {
return;
}
function getDataRows() {
return Array.prototype.filter.call(
document.querySelectorAll(table_selector + " tr"),
function(row) {
return row.id && row.id.indexOf("cbi-shadowsocksr-") === 0;
}
);
}
function ondragstart(ev) {
ev.dataTransfer.setData("Text", ev.target.id);
}
function ondrop(ev) {
var from = ev.dataTransfer.getData("Text");
cbi_row_drop(from, this.id, store);
}
function ondragover(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
}
// 上移一行
function moveUp(id) {
var trList = getDataRows();
var currentIndex = -1;
for (var i = 0; i < trList.length; i++) {
if (trList[i].id === id) {
currentIndex = i;
break;
}
}
if (currentIndex > 0) {
var prevRow = trList[currentIndex - 1];
cbi_row_drop(id, prevRow.id, store);
}
}
// 下移一行
function moveDown(id) {
var trList = getDataRows();
var currentIndex = -1;
for (var i = 0; i < trList.length; i++) {
if (trList[i].id === id) {
currentIndex = i;
break;
}
}
if (currentIndex < trList.length - 1) {
var nextRow = trList[currentIndex + 1];
var nextNextRow = trList[currentIndex + 2];
if (nextNextRow) {
cbi_row_drop(id, nextNextRow.id, store, false);
} else {
cbi_row_drop(id, nextRow.id, store, true);
}
}
}
function moveToTop(id) {
var trList = getDataRows();
if (trList.length > 0) {
cbi_row_drop(id, trList[0].id, store);
}
}
function moveToBottom(id) {
var trList = getDataRows();
if (trList.length > 0) {
var bottom = trList[trList.length - 1];
cbi_row_drop(id, bottom.id, store, true);
}
}
var dataRows = getDataRows();
for (let index = 0; index < dataRows.length; index++) {
const el = dataRows[index];
el.setAttribute("draggable", true);
el.ondragstart = ondragstart;
el.ondrop = ondrop;
el.ondragover = ondragover;
}
}
// enable
enableDragForTable(
"#cbi-shadowsocksr-servers table",
"cbi.sts.shadowsocksr.servers"
);
//]]>
</script>