update 2026-08-08 20:35:50

This commit is contained in:
action committed 2026-08-08 20:35:50 +08:00
1 parent 74e7dc08d2
commit 2a09d6ce42
1982 files changed
+712 -1523905

No files matched your search

@@ -8,6 +8,22 @@ function arraysEqual(a, b) {
return true;
}
function decodeIfBase64(str) {
try {
let s = str.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const decoded = decodeURIComponent(
atob(s).split('').map(c =>
'%' + c.charCodeAt(0).toString(16).padStart(2, '0')
).join('')
);
if (btoa(unescape(encodeURIComponent(decoded))).replace(/=+$/, '') === s.replace(/=+$/, '')) {
return decoded;
}
} catch (e) {}
return str;
}
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
@@ -51,14 +51,14 @@ function index()
entry({"admin", "services", appname, "socks_config"}, cbi(appname .. "/client/socks_config")).leaf = true
entry({"admin", "services", appname, "acl"}, cbi(appname .. "/client/acl"), _("Access control"), 98).leaf = true
entry({"admin", "services", appname, "acl_config"}, cbi(appname .. "/client/acl_config")).leaf = true
entry({"admin", "services", appname, "log"}, form(appname .. "/client/log"), _("Runtime Logs"), 999).leaf = true
entry({"admin", "services", appname, "log"}, template(appname .. "/log/log"), _("Runtime Logs"), 999).leaf = true
--[[ Server ]]
entry({"admin", "services", appname, "server"}, cbi(appname .. "/server/index"), _("Server-Side"), 99).leaf = true
entry({"admin", "services", appname, "server_user"}, cbi(appname .. "/server/user")).leaf = true
--[[ API ]]
entry({"admin", "services", appname, "server_user_update"}, call("server_user_update")).leaf = true
entry({"admin", "services", appname, "server_update_config"}, call("server_update_config")).leaf = true
entry({"admin", "services", appname, "server_user_status"}, call("server_user_status")).leaf = true
entry({"admin", "services", appname, "server_user_log"}, call("server_user_log")).leaf = true
entry({"admin", "services", appname, "server_get_log"}, call("server_get_log")).leaf = true
@@ -652,7 +652,7 @@ function rollback_rules()
http_write_json_ok()
end
function server_user_update()
function server_update_config()
local id = http.formvalue("id") -- Node id
local data = http.formvalue("data") -- json new Data
if id and data then
@@ -114,7 +114,7 @@ if api.is_finded("geoview") then
.. "</ul>"
end
shunt_group = add_option(ListValue, "shunt_group", translate("Group Name"))
shunt_group = add_option(ListValue, "shunt_group", translate("Shunt Rule Group"))
shunt_group:value("", translate("default"))
for k, v in pairs(groups) do
shunt_group:value(k)
@@ -1,8 +0,0 @@
local appname = "passwall2"
f = SimpleForm(appname)
f.reset = false
f.submit = false
f:append(Template(appname .. "/log/log"))
return f
@@ -30,9 +30,6 @@ o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
o.default = o.keylist[3]
-- [[ Add the node via the link ]]--
s:append(Template(appname .. "/node_list/link_add_node"))
m:append(Template(appname .. "/node_list/node_list"))
return api.return_map(m)
@@ -88,6 +88,6 @@ end
s:append(Template(appname .. "/rule/rule_version"))
s:append(Template(appname .. "/rule/shunt_rule_list"))
m:append(Template(appname .. "/rule/shunt_rule_list"))
return api.return_map(m)
@@ -76,7 +76,7 @@ remarks.validate = function(self, value, section)
return value
end
o = s:option(Value, "group", translate("Group Name"))
o = s:option(Value, "group", translate("Shunt Rule Group"))
o.default = ""
o:value("", translate("default"))
for k, v in pairs(groups) do
@@ -1,6 +1,7 @@
<%
local api = require "luci.passwall2.api"
-%>
<%+header%>
<script type="text/javascript">
//<![CDATA[
var log_textarea = null;
@@ -42,3 +43,4 @@ local api = require "luci.passwall2.api"
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clearlog()" value="<%:Clear logs%>" />
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="40" wrap="off" readonly="readonly"></textarea>
</fieldset>
<%+footer%>
@@ -1,556 +0,0 @@
<%
local api = require "luci.passwall2.api"
-%>
<script type="text/javascript">
//<![CDATA[
function ajax_add_node(link, group) {
const dom = document.getElementById('modal-mask');
dom.style.display = 'block';
dom.innerHTML = '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;"><%:Processing, please wait…%></div>';
const chunkSize = 1000; // Fragmented sending to overcome uhttpd's limitations, with each fragment containing 1000 characters.
const totalChunks = Math.ceil(link.length / chunkSize);
let currentChunk = 0;
function sendNextChunk() {
if (currentChunk < totalChunks) {
const chunk = link.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
const xhr = new XMLHttpRequest();
xhr.open('POST', '<%=api.url("link_add_node")%>', true);
xhr.onload = function () {
if (xhr.status === 200) {
currentChunk++;
sendNextChunk();
} else {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Error%>");
return;
}
};
xhr.onerror = function () {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Network Error%>");
return;
};
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("chunk_index", currentChunk);
formData.append("total_chunks", totalChunks);
formData.append("group", group);
xhr.send(formData);
} else {
window.location.href = '<%=api.url("node_list")%>';
}
}
sendNextChunk();
}
function decodeIfBase64(str) {
try {
let s = str.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const decoded = decodeURIComponent(
atob(s).split('').map(c =>
'%' + c.charCodeAt(0).toString(16).padStart(2, '0')
).join('')
);
if (btoa(unescape(encodeURIComponent(decoded))).replace(/=+$/, '') === s.replace(/=+$/, '')) {
return decoded;
}
} catch (e) {}
return str;
}
function getBg(el) {
if (!el) return null;
const style = getComputedStyle(el);
const bgImage = style.backgroundImage;
const bgColor = style.backgroundColor;
return (bgImage !== 'none' || !/rgba\([^,]+,[^,]+,[^,]+,\s*0\)/.test(bgColor) && bgColor !== 'transparent')
? style.background
: null;
};
function open_add_link_div() {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const addLinkDiv = document.getElementById("add_link_div");
addLinkDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
addLinkDiv.style.display = "block";
if (!addLinkDiv._dropdown_inited) {
addLinkDiv._dropdown_inited = true;
dropdown_list_fun("addlink_group_custom");
}
document.getElementById("nodes_link").focus();
}
function close_add_link_div() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("add_link_div").style.display = "none";
document.body.classList.remove('modal-open');
}
function add_node() {
let nodes_link = document.getElementById("nodes_link").value;
const group = (document.querySelector('#addlink_group_custom input[type="hidden"]')?.value || "default");
nodes_link = nodes_link.replace(/\t/g, "").replace(/\r\n|\r/g, "\n").replace(/<[^>]*>/g, '').trim();
if (nodes_link != "") {
nodes_link = decodeIfBase64(nodes_link);
let s = nodes_link.split('://');
if (s.length > 1) {
close_add_link_div();
ajax_add_node(nodes_link, group);
} else {
alert("<%:Please enter the correct link.%>");
}
} else {
document.getElementById("nodes_link").focus();
}
}
function clear_all_nodes() {
if (confirm('<%:Are you sure to clear all nodes?%>') == true){
XHR.get('<%=api.url("clear_all_nodes")%>', null,
function(x, data) {
if(x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
}
function open_reassign_group_div() {
const ids = [];
const visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const reassignGroupDiv = document.getElementById("reassign_group_div");
reassignGroupDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
reassignGroupDiv.style.display = "block";
if (!reassignGroupDiv._dropdown_inited) {
reassignGroupDiv._dropdown_inited = true;
dropdown_list_fun("reassign_group_custom");
}
} else {
alert("<%:You no select nodes !%>");
}
}
}
function close_reassign_group_div() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("reassign_group_div").style.display = "none";
document.body.classList.remove('modal-open');
}
function reassign_group() {
const ids = [];
const group = (document.querySelector('#reassign_group_custom input[type="hidden"]')?.value || "default");
const visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
XHR.get('<%=api.url("reassign_group")%>', {
group: group,
ids: ids.join(",")
},
function(x, data) {
if (x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
}
}
// Custom group dropdown list events
function dropdown_list_fun(div_id) {
const dropdown = document.getElementById(div_id);
if (!dropdown) return;
const display = dropdown.querySelector(".selected-display");
const displayText = display.querySelector(".text");
const list = dropdown.querySelector(".dropdown-list");
const hidden = dropdown.querySelector('input[type="hidden"]');
const input = dropdown.querySelector(".create-item-input");
display.addEventListener("click", function() {
list.style.display = list.style.display === "none" ? "block" : "none";
input.value = "";
//input.focus();
});
function selectItem(li) {
list.querySelectorAll(".dropdown-item").forEach(function(el){
el.classList.remove("selected");
});
li.classList.add("selected");
hidden.value = li.dataset.value;
displayText.textContent = li.dataset.value || "<%:default%>";
list.style.display = "none";
}
list.addEventListener("click", function(e){
const li = e.target.closest(".dropdown-item");
if (!li || li.classList.contains("custom-input")) return;
selectItem(li);
});
input.addEventListener("keydown", function(e){
const isEnter = e.key === "Enter" || e.keyCode === 13;
if (!isEnter) return;
e.stopPropagation();
e.preventDefault();
const val = input.value.trim();
if (!val) return;
if (val.toLowerCase() === "default") {
const emptyLi = Array.from(list.querySelectorAll(".dropdown-item"))
.find(function(el){ return !el.dataset.value; });
if (emptyLi) selectItem(emptyLi);
input.value = "";
return;
}
let li = Array.from(list.querySelectorAll(".dropdown-item")).find(function(el){
return el.dataset.value && el.dataset.value.toLowerCase() === val.toLowerCase();
});
if (!li) {
li = document.createElement("li");
li.className = "dropdown-item";
li.dataset.value = val;
li.textContent = val;
list.insertBefore(li, input.parentNode);
}
input.value = "";
selectItem(li);
});
// Read group names from tab
function InsertGroup(list, input) {
const tabs = document.querySelectorAll(".cbi-tabmenu li");
if (!tabs.length) {
setTimeout(() => InsertGroup(list, input), 50);
return;
}
tabs.forEach(function(li){
const group = li.id.split('.').pop();
if(group === "default") return;
if(Array.from(list.querySelectorAll(".dropdown-item")).some(el => el.dataset.value === group)) return;
const newLi = document.createElement("li");
newLi.className = "dropdown-item";
newLi.dataset.value = group;
newLi.textContent = group;
list.insertBefore(newLi, input.parentNode);
});
}
InsertGroup(list, input);
// Automatically collapses when clicking on the outside.
document.addEventListener("click", function(e) {
if (!dropdown.contains(e.target)) {
list.style.display = "none";
}
});
}
//]]>
</script>
<div id="modal-mask"></div>
<div id="add_link_div">
<div id="add_link_modal_container">
<h3><%:Add the node via the link%></h3>
<div class="value-custom">
<textarea id="nodes_link" rows="10"></textarea>
<p id="nodes_link_text"><%:Enter share links, one per line. Subscription links are not supported!%></p>
</div>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="addlink_group_custom"><%:Group Name%></label>
<div id="addlink_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="addlink_group" value="">
</div>
</div>
</div>
<div id="add_link_button_container">
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_node()" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_add_link_div()" value="<%:Close%>" />
</div>
</div>
</div>
<div id="reassign_group_div">
<div id="reassign_group_modal_container">
<h3><%:Reassign Node Group%></h3>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="reassign_group_custom"><%:Group Name%></label>
<div id="reassign_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="to_group" value="">
</div>
</div>
</div>
<div id="reassign_group_button_container">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group()" value="<%:Save%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_reassign_group_div()" value="<%:Close%>" />
</div>
</div>
</div>
<div class="pw-toolbar">
<div class="pw-toolbar-field">
<input class="btn cbi-button cbi-button-add" type="button" onclick="to_add_node()" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-add" type="button" onclick="open_add_link_div()" value="<%:Add the node via the link%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clear_all_nodes()" value="<%:Clear all nodes%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="delete_select_nodes()" value="<%:Delete select nodes%>" />
<input class="btn cbi-button cbi-button-edit" type="button" id="select_all_btn" onclick="checked_all_node(this)" value="<%:Select all%>" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="open_reassign_group_div()" value="<%:Reassign Group%>" />
<input class="btn cbi-button cbi-button-apply" type="submit" name="cbi.apply" value="<%:Save & Apply%>" />
<!--<input class="btn cbi-button cbi-button-save" type="submit" name="cbi.save" value="<%:Save%>" />-->
<!--<input class="btn cbi-button cbi-button-reset" type="button" value="<%:Reset%>" onclick="location.href='<%=REQUEST_URI%>'" />-->
</div>
</div>
<style>
.pw-toolbar {
width: 100%;
text-align: center;
}
.pw-toolbar-field {
width: 100%;
text-align: center;
display: inline-block;
padding: 5px 0 5px;
}
#modal-mask {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.4);
z-index: 999;
}
body.modal-open {
overflow: hidden;
pointer-events: none;
}
body.modal-open #add_link_div,
body.modal-open #reassign_group_div {
pointer-events: auto;
}
#add_link_div, #reassign_group_div {
display: none;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
width: 90%;
max-width: 500px;
}
#add_link_modal_container, #reassign_group_modal_container {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20px;
}
#nodes_link {
width: 100%;
height: 180px;
resize: vertical;
font-family: monospace;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
#add_link_div h3,
#reassign_group_div h3 {
background: inherit;
}
#nodes_link_text {
color: red;
font-size: 14px;
margin-top: 5px;
text-align: center;
width: 100%;
}
#add_link_button_container, #reassign_group_button_container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 300px;
margin-top: 10px;
}
.value-custom {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin: 10px 0;
padding: 0px 5px 0px 5px;
}
.value-field-custom {
display: inline-flex;
align-items: center;
gap: 10px;
}
.value-title-custom {
font-size: 13px;
line-height: 28px;
white-space: nowrap;
text-align: right;
}
.custom-dropdown {
position: relative;
border: 1px solid #d9d9d9;
border-radius: 2px;
width: 180px;
height: 28px;
font-size: 13px;
background: #fff;
cursor: pointer;
box-sizing: border-box;
display: block;
}
.selected-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
width: 100%;
height: 100%;
box-sizing: border-box;
font-size: 12px;
color: #666;
}
.selected-display:hover {
background-color: #f7f7f7;
}
.dropdown-list {
position: absolute;
top: calc(100% + 2px);
left: -1px;
width: calc(100% + 2px);
border: 1px solid #d9d9d9;
border-radius: 2px;
border-top: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
background: #fff;
list-style: none;
margin: 0;
padding: 0;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
z-index: 100;
box-sizing: border-box;
color: #666;
}
.dropdown-item {
padding: 4px 8px;
line-height: 20px;
cursor: pointer;
}
.dropdown-item.selected {
background-color: #1e90ff;
color: #fff;
}
.create-item-input::placeholder {
text-align: center;
}
.dropdown-item.custom-input input {
width: 100%;
box-sizing: border-box;
padding: 3px;
font-size: 13px;
line-height: 20px;
border: 1px solid #ccc;
text-align: left;
}
.arrow-down-small {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #555;
display: inline-block;
vertical-align: middle;
}
</style>
@@ -9,10 +9,11 @@ local node = api.uci_get_type("global", "node")
if node then
local node_type = api.uci_get_type_id(node, "type")
local node_protocol = api.uci_get_type_id(node, "protocol")
if node_type == "Xray" and node_protocol == "_shunt" then
if (node_type == "Xray" or node_type == "sing-box") and node_protocol == "_shunt" then
default_node_type = node_protocol
local node_shunt_group = api.uci_get_type_id(node, "shunt_group")
uci:foreach(appname, "shunt_rules", function(e)
if e[".name"] and e.remarks then
if e[".name"] and e.remarks and e.group == node_shunt_group then
shunt_rule_list[#shunt_rule_list + 1] = e
end
end)
@@ -36,12 +37,13 @@ table td, .table .td {
display: none;
width: 30rem;
position: fixed;
top:50%;
padding-top: 30px;
top:45%;
z-index: 99;
text-align: center;
background: white;
box-shadow: darkgrey 10px 10px 30px 5px;
text-align: center;
padding: 0.5em;
}
._now_use_bg {
@@ -183,8 +185,205 @@ table td, .table .td {
}
</style>
<% if api.is_js_luci() then -%>
<style>
.pw-toolbar {
width: 100%;
text-align: center;
}
.pw-toolbar-field {
width: 100%;
text-align: center;
display: inline-block;
padding: 5px 0 5px;
}
#modal-mask {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.4);
z-index: 999;
}
body.modal-open {
overflow: hidden;
pointer-events: none;
}
body.modal-open #add_link_div,
body.modal-open #reassign_group_div {
pointer-events: auto;
}
#add_link_div, #reassign_group_div {
display: none;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
width: 90%;
max-width: 500px;
}
#add_link_modal_container, #reassign_group_modal_container {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20px;
}
#nodes_link {
width: 100%;
height: 180px;
resize: vertical;
font-family: monospace;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
#add_link_div h3,
#reassign_group_div h3 {
background: inherit;
}
#nodes_link_text {
color: red;
font-size: 14px;
margin-top: 5px;
text-align: center;
width: 100%;
}
#add_link_button_container, #reassign_group_button_container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 300px;
margin-top: 10px;
}
.value-custom {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin: 10px 0;
padding: 0px 5px 0px 5px;
}
.value-field-custom {
display: inline-flex;
align-items: center;
gap: 10px;
}
.value-title-custom {
font-size: 13px;
line-height: 28px;
white-space: nowrap;
text-align: right;
}
.custom-dropdown {
position: relative;
border: 1px solid #d9d9d9;
border-radius: 2px;
width: 180px;
height: 28px;
font-size: 13px;
background: #fff;
cursor: pointer;
box-sizing: border-box;
display: block;
}
.selected-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
width: 100%;
height: 100%;
box-sizing: border-box;
font-size: 12px;
color: #666;
}
.selected-display:hover {
background-color: #f7f7f7;
}
.dropdown-list {
position: absolute;
top: calc(100% + 2px);
left: -1px;
width: calc(100% + 2px);
border: 1px solid #d9d9d9;
border-radius: 2px;
border-top: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
background: #fff;
list-style: none;
margin: 0;
padding: 0;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
z-index: 100;
box-sizing: border-box;
color: #666;
}
.dropdown-item {
padding: 4px 8px;
line-height: 20px;
cursor: pointer;
}
.dropdown-item.selected {
background-color: #1e90ff;
color: #fff;
}
.create-item-input::placeholder {
text-align: center;
}
.dropdown-item.custom-input input {
width: 100%;
box-sizing: border-box;
padding: 3px;
font-size: 13px;
line-height: 20px;
border: 1px solid #ccc;
text-align: left;
}
.arrow-down-small {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #555;
display: inline-block;
vertical-align: middle;
}
</style>
<script type="text/javascript">
<% if api.is_js_luci() then -%>
var cbi_t = [];
function cbi_t_add(section, tab) {
var t = document.getElementById('tab.' + section + '.' + tab);
@@ -217,9 +416,7 @@ table td, .table .td {
}
return false
}
</script>
<%- else %>
<script type="text/javascript">
(function() {
if (typeof(cbi_t_switch) === "function") {
var old_switch = cbi_t_switch;
@@ -229,16 +426,16 @@ table td, .table .td {
};
}
})();
</script>
<%- end %>
</script>
<script type="text/javascript">
//<![CDATA[
let auto_detection_time = "<%=api.uci_get_type("global_other", "auto_detection_time", "0")%>"
let show_node_info = "<%=api.uci_get_type("global_other", "show_node_info", "0")%>"
var ajaxList = [];
var ajax = {
list: [],
post: function(url, data, fn_success, timeout, fn_timeout) {
var xhr = new XMLHttpRequest();
var code = ajax.encode(data);
@@ -267,7 +464,7 @@ table td, .table .td {
fn_success(xhr, json);
}
};
ajaxList.push(xhr);
this.list.push(xhr);
xhr.send(code);
},
encode: function(obj) {
@@ -287,65 +484,63 @@ table td, .table .td {
}
return obj;
},
abortAll: function() {
this.list.forEach(xhr => xhr.abort());
this.list = [];
}
}
function ajaxAbortAll() {
ajaxList.forEach(xhr => xhr.abort());
ajaxList = [];
}
function to_add_node() {
ajaxAbortAll();
const dom = document.getElementsByClassName("cbi-tab")[0];
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
window.location.href='<%=api.url("add_node")%>?redirect=1&group=' + current_group;
}
function to_edit_node(cbi_id) {
ajaxAbortAll();
location.href='<%=api.url("node_config")%>/' + cbi_id;
}
function copy_node(cbi_id) {
ajaxAbortAll();
window.location.href = '<%=api.url("copy_node")%>' + "?section=" + cbi_id;
}
function del_node(cbi_id) {
ajaxAbortAll();
window.location.href = '<%=api.url("delete_select_nodes")%>' + "?redirect=1&ids=" + cbi_id;
}
var section = "";
function open_set_node_div(cbi_id) {
section = cbi_id;
document.getElementById("set_node_div").style.display="block";
var node_name = document.getElementById("cbid.<%=appname%>." + cbi_id + ".remarks").value;
document.getElementById("set_node_name").innerHTML = node_name;
}
function close_set_node_div() {
document.getElementById("set_node_div").style.display="none";
document.getElementById("set_node_name").innerHTML = "";
}
function row_top(btn, group) {
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);
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";
function node_action(action, cbi_id, a) {
ajax.abortAll();
switch (action) {
case "add":
const dom = document.getElementsByClassName("cbi-tab")[0];
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
window.location.href='<%=api.url("add_node")%>?redirect=1&group=' + current_group;
break;
case "copy":
window.location.href = '<%=api.url("copy_node")%>' + "?section=" + cbi_id;
break;
case "edit":
window.location.href='<%=api.url("node_config")%>/' + cbi_id;
break;
case "del":
window.location.href = '<%=api.url("delete_select_nodes")%>' + "?redirect=1&ids=" + cbi_id;
break;
case "row_top":
let btn = a.btn;
let group = a.group;
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);
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";
}
}
}
}
break;
case "del_all":
if (confirm('<%:Are you sure to clear all nodes?%>') == true){
XHR.get('<%=api.url("clear_all_nodes")%>', null,
function(x, data) {
if(x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
break;
default:
}
}
@@ -420,6 +615,7 @@ table td, .table .td {
if (confirm('<%:Are you sure to delete select nodes?%>') == true) {
dom.style.display = 'block';
dom.innerHTML = '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;"><%:Processing, please wait…%></div>';
ajax.abortAll();
XHR.get('<%=api.url("delete_select_nodes")%>', {
ids: ids.join()
},
@@ -433,7 +629,7 @@ table td, .table .td {
box.remove();
}
*/
ajaxAbortAll();
ajax.abortAll();
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
@@ -446,14 +642,7 @@ table td, .table .td {
alert("<%:You no select nodes !%>");
}
}
function set_node(type, config) {
if (confirm('<%:Are you sure set this server?%>')==true){
ajaxAbortAll();
window.location.href = '<%=api.url("set_node")%>?type=' + type + '&config=' + config + '&section=' + section;
}
}
function get_address_full(id) {
var address = (document.getElementById("cbid.<%=appname%>." + id + ".address") || {}).value || "";
var port = (document.getElementById("cbid.<%=appname%>." + id + ".port") || {}).value || "";
@@ -501,6 +690,7 @@ table td, .table .td {
var id = row.id.replace("cbi-<%=appname%>-", "");
ids.push(id);
});
ajax.abortAll();
XHR.get('<%=api.url("save_node_order")%>', {
group: group,
ids: ids.join(",")
@@ -741,16 +931,6 @@ 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;
@@ -821,6 +1001,291 @@ table td, .table .td {
}
</script>
<script type="text/javascript">
//<![CDATA[
//Pop-up logic
// Custom group dropdown list events
function dropdown_list_fun(div_id) {
const dropdown = document.getElementById(div_id);
if (!dropdown) return;
const display = dropdown.querySelector(".selected-display");
const displayText = display.querySelector(".text");
const list = dropdown.querySelector(".dropdown-list");
const hidden = dropdown.querySelector('input[type="hidden"]');
const input = dropdown.querySelector(".create-item-input");
display.addEventListener("click", function() {
list.style.display = list.style.display === "none" ? "block" : "none";
input.value = "";
//input.focus();
});
function selectItem(li) {
list.querySelectorAll(".dropdown-item").forEach(function(el){
el.classList.remove("selected");
});
li.classList.add("selected");
hidden.value = li.dataset.value;
displayText.textContent = li.dataset.value || "<%:default%>";
list.style.display = "none";
}
list.addEventListener("click", function(e){
const li = e.target.closest(".dropdown-item");
if (!li || li.classList.contains("custom-input")) return;
selectItem(li);
});
input.addEventListener("keydown", function(e){
const isEnter = e.key === "Enter" || e.keyCode === 13;
if (!isEnter) return;
e.stopPropagation();
e.preventDefault();
const val = input.value.trim();
if (!val) return;
if (val.toLowerCase() === "default") {
const emptyLi = Array.from(list.querySelectorAll(".dropdown-item"))
.find(function(el){ return !el.dataset.value; });
if (emptyLi) selectItem(emptyLi);
input.value = "";
return;
}
let li = Array.from(list.querySelectorAll(".dropdown-item")).find(function(el){
return el.dataset.value && el.dataset.value.toLowerCase() === val.toLowerCase();
});
if (!li) {
li = document.createElement("li");
li.className = "dropdown-item";
li.dataset.value = val;
li.textContent = val;
list.insertBefore(li, input.parentNode);
}
input.value = "";
selectItem(li);
});
// Read group names from tab
function InsertGroup(list, input) {
const tabs = document.querySelectorAll(".cbi-tabmenu li");
if (!tabs.length) {
setTimeout(() => InsertGroup(list, input), 50);
return;
}
tabs.forEach(function(li){
const group = li.id.split('.').pop();
if(group === "default") return;
if(Array.from(list.querySelectorAll(".dropdown-item")).some(el => el.dataset.value === group)) return;
const newLi = document.createElement("li");
newLi.className = "dropdown-item";
newLi.dataset.value = group;
newLi.textContent = group;
list.insertBefore(newLi, input.parentNode);
});
}
InsertGroup(list, input);
// Automatically collapses when clicking on the outside.
document.addEventListener("click", function(e) {
if (!dropdown.contains(e.target)) {
list.style.display = "none";
}
});
}
function getBg(el) {
if (!el) return null;
const style = getComputedStyle(el);
const bgImage = style.backgroundImage;
const bgColor = style.backgroundColor;
return (bgImage !== 'none' || !/rgba\([^,]+,[^,]+,[^,]+,\s*0\)/.test(bgColor) && bgColor !== 'transparent')
? style.background
: null;
}
const add_link_div = {
open() {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const addLinkDiv = document.getElementById("add_link_div");
addLinkDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
addLinkDiv.style.display = "block";
if (!addLinkDiv._dropdown_inited) {
addLinkDiv._dropdown_inited = true;
dropdown_list_fun("addlink_group_custom");
}
document.getElementById("nodes_link").focus();
},
close() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("add_link_div").style.display = "none";
document.body.classList.remove('modal-open');
},
add_node() {
let nodes_link = document.getElementById("nodes_link").value;
const group = (document.querySelector('#addlink_group_custom input[type="hidden"]')?.value || "default");
nodes_link = nodes_link.replace(/\t/g, "").replace(/\r\n|\r/g, "\n").replace(/<[^>]*>/g, '').trim();
if (nodes_link != "") {
nodes_link = decodeIfBase64(nodes_link);
let s = nodes_link.split('://');
if (s.length > 1) {
this.close();
this.ajax_add_node(nodes_link, group);
} else {
alert("<%:Please enter the correct link.%>");
}
} else {
document.getElementById("nodes_link").focus();
}
},
ajax_add_node(link, group) {
const dom = document.getElementById('modal-mask');
dom.style.display = 'block';
dom.innerHTML = '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;"><%:Processing, please wait…%></div>';
const chunkSize = 1000; // Fragmented sending to overcome uhttpd's limitations, with each fragment containing 1000 characters.
const totalChunks = Math.ceil(link.length / chunkSize);
let currentChunk = 0;
function sendNextChunk() {
if (currentChunk < totalChunks) {
const chunk = link.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
const xhr = new XMLHttpRequest();
xhr.open('POST', '<%=api.url("link_add_node")%>', true);
xhr.onload = function () {
if (xhr.status === 200) {
currentChunk++;
sendNextChunk();
} else {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Error%>");
return;
}
};
xhr.onerror = function () {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Network Error%>");
return;
};
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("chunk_index", currentChunk);
formData.append("total_chunks", totalChunks);
formData.append("group", group);
ajax.abortAll();
xhr.send(formData);
} else {
window.location.href = '<%=api.url("node_list")%>';
}
}
sendNextChunk();
}
}
const reassign_group_div = {
open() {
const ids = [];
const visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const reassignGroupDiv = document.getElementById("reassign_group_div");
reassignGroupDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
reassignGroupDiv.style.display = "block";
if (!reassignGroupDiv._dropdown_inited) {
reassignGroupDiv._dropdown_inited = true;
dropdown_list_fun("reassign_group_custom");
}
} else {
alert("<%:You no select nodes !%>");
}
}
},
close() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("reassign_group_div").style.display = "none";
document.body.classList.remove('modal-open');
},
action() {
const ids = [];
const group = (document.querySelector('#reassign_group_custom input[type="hidden"]')?.value || "default");
const visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
ajax.abortAll();
XHR.get('<%=api.url("reassign_group")%>', {
group: group,
ids: ids.join(",")
},
function(x, data) {
if (x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
}
}
}
const set_node_div = {
section: '',
open(cbi_id) {
this.section = cbi_id;
document.getElementById("set_node_div").style.display="block";
const node_name = document.getElementById("cbid.<%=appname%>." + cbi_id + ".remarks").value;
document.getElementById("set_node_name").innerHTML = node_name;
},
close() {
document.getElementById("set_node_div").style.display="none";
document.getElementById("set_node_name").innerHTML = "";
},
set(type, config) {
if (confirm('<%:Are you sure set this node?%>') == true){
ajax.abortAll();
window.location.href = '<%=api.url("set_node")%>?type=' + type + '&config=' + config + '&section=' + section;
}
}
}
//]]>
</script>
<div class="pw-toolbar">
<div class="pw-toolbar-field">
<input class="btn cbi-button cbi-button-add" type="button" onclick="node_action('add')" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_link_div.open()" value="<%:Add the node via the link%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="node_action('del_all')" value="<%:Clear all nodes%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="delete_select_nodes()" value="<%:Delete select nodes%>" />
<input class="btn cbi-button cbi-button-edit" type="button" id="select_all_btn" onclick="checked_all_node(this)" value="<%:Select all%>" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group_div.open()" value="<%:Reassign Group%>" />
<input class="btn cbi-button cbi-button-apply" type="submit" name="cbi.apply" value="<%:Save & Apply%>" />
<!--<input class="btn cbi-button cbi-button-save" type="submit" name="cbi.save" value="<%:Save%>" />-->
<!--<input class="btn cbi-button cbi-button-reset" type="button" value="<%:Reset%>" onclick="location.href='<%=REQUEST_URI%>'" />-->
</div>
</div>
<script type="text/template" id="nodes-table-template">
<fieldset class="cbi-section cbi-tblsection" id="cbi-<%=appname%>-nodes-{{group}}-fieldset">
@@ -838,7 +1303,7 @@ table td, .table .td {
{{node-tr}}
</table>
<div class="cbi-section-create cbi-tblsection-create">
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="to_add_node()">
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="node_action('add')">
<input class="cbi-button cbi-button-apply" style="display: none" type="button" id="save_order_btn_{{group}}" value="<%:Save Order%>" onclick="save_current_page_order('{{group}}')">
</div>
</fieldset>
@@ -858,11 +1323,11 @@ table td, .table .td {
<td class="td cbi-value-field pw-urltest">{{url_test}}</td>
<td class="td cbi-section-table-cell nowrap cbi-section-actions pw-actions">
<div class="node-wrapper">
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="row_top(this, '{{group}}')" title="<%:To Top%>"/>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Use%>" id="apply_{{id}}" onclick="open_set_node_div('{{id}}')"/>
<input class="btn cbi-button cbi-button-add" type="button" value="<%:Copy%>" onclick="copy_node('{{id}}')"/>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="to_edit_node('{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="del_node('{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="node_action('row_top', null, {btn:this, group:'{{group}}'})" title="<%:To Top%>"/>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Use%>" id="apply_{{id}}" onclick="set_node_div.open('{{id}}')"/>
<input class="btn cbi-button cbi-button-add" type="button" value="<%:Copy%>" onclick="node_action('copy', '{{id}}')"/>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="node_action('edit', '{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="node_action('del', '{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
<span class="drag-handle center" title="<%:Drag to reorder%>">⠿</span>
</div>
</td>
@@ -873,6 +1338,8 @@ table td, .table .td {
</fieldset>
<script type="text/javascript">
//init logic
function get_remarks_name(o) {
let str = "";
let remarks = o["remarks"] || "";
@@ -949,9 +1416,8 @@ table td, .table .td {
group_nodes[_node.group].push(_node)
}
var tab_ul_html = '<ul class="cbi-tabmenu">'
var tab_ul_li_html = ''
var tab_content_html = '<fieldset class="cbi-section-node cbi-section-node-tabbed" id="cbi-<%=appname%>-nodes">'
var tab_content_html = ''
var nodes_table_template = document.getElementById("nodes-table-template");
var node_template = document.getElementById("node-tr-template");
var default_group = null
@@ -1025,11 +1491,21 @@ table td, .table .td {
'</div>'
}
tab_ul_html += tab_ul_li_html + '</ul>'
tab_content_html += '</fieldset>'
var tab_html = tab_ul_html + tab_content_html
function addTabDOM(li, content) {
let tab_ul_html = '<ul class="cbi-tabmenu">';
let tab_content_html = '<fieldset class="cbi-section-shunt_rule cbi-section-shunt_rule-tabbed" id="cbi-<%=appname%>-nodes">';
if (li) {
tab_ul_html += li;
}
if (content) {
tab_content_html += content;
}
tab_ul_html += '</ul>';
tab_content_html += '</fieldset>';
return tab_ul_html + tab_content_html;
}
document.getElementById("node_list").innerHTML = tab_html
document.getElementById("node_list").innerHTML = addTabDOM(tab_ul_li_html, tab_content_html);
for (let group in group_nodes) {
cbi_t_add("<%=appname%>.nodes", group)
@@ -1038,7 +1514,7 @@ table td, .table .td {
if (default_group) {
cbi_t_switch("<%=appname%>.nodes", default_group)
}
origin_group_node_order = {};
for (let group in group_nodes) {
origin_group_node_order[group] = get_node_order(group);
@@ -1071,20 +1547,8 @@ table td, .table .td {
//Node list option saving logic
document.addEventListener("DOMContentLoaded", function () {
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function onChange(option, value) {
ajax.abortAll();
XHR.get('<%=api.url("save_node_list_opt")%>', {
option: option,
value: value
@@ -1118,31 +1582,96 @@ table td, .table .td {
links.forEach(link => {
link.addEventListener('click', (e) => {
if (link.getAttribute('target') !== '_blank') {
ajaxAbortAll();
ajax.abortAll();
}
});
});
window.addEventListener('beforeunload', (event) => {
ajax.abortAll();
});
});
//]]>
</script>
<div style="display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center;">
<div id="set_node_div" style="text-align: center;">
<div class="cbi-value" style="display: flex; justify-content: center; align-items: center; margin: 8px 0;">
<font color=#666><%:You choose node is:%></font><a style="color: red" id="set_node_name"></a>
<div id="modal-mask"></div>
<div id="add_link_div">
<div id="add_link_modal_container">
<h3><%:Add the node via the link%></h3>
<div class="value-custom">
<textarea id="nodes_link" rows="10"></textarea>
<p id="nodes_link_text"><%:Enter share links, one per line. Subscription links are not supported!%></p>
</div>
<div class="cbi-value" style="display: flex; justify-content: center; gap: 8px; margin: 8px 0;">
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="addlink_group_custom"><%:Group Name%></label>
<div id="addlink_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="addlink_group" value="">
</div>
</div>
</div>
<div id="add_link_button_container">
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_link_div.add_node()" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="add_link_div.close()" value="<%:Close%>" />
</div>
</div>
</div>
<div id="reassign_group_div">
<div id="reassign_group_modal_container">
<h3><%:Reassign Node Group%></h3>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="reassign_group_custom"><%:Group Name%></label>
<div id="reassign_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="to_group" value="">
</div>
</div>
</div>
<div id="reassign_group_button_container">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group_div.action()" value="<%:Save%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="reassign_group_div.close()" value="<%:Close%>" />
</div>
</div>
</div>
<div style="display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center;">
<div id="set_node_div">
<div class="cbi-value" style="display: flex; justify-content: center; align-items: center; margin: 1em 0; height: 2em;">
<font color=#666><%:You choose node is:%></font><a style="color: red" id="set_node_name"></a>
<div style="position:absolute; right:0.5em; top: 0.5em;"><input class="btn cbi-button" type="button" onclick="set_node_div.close()" value="X" /></div>
</div>
<div class="cbi-value" style="justify-content: center; gap: 8px; margin: 1em 0;">
<%- if default_node_type == "_shunt" then
for i, v in ipairs(shunt_rule_list) do
-%>
<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node('<%=node%>', '<%=v[".name"]%>')" value="<%=v.remarks%>" />
<input class="btn cbi-button" type="button" onclick="set_node_div.set('<%=node%>', '<%=v[".name"]%>')" value="<%=v.remarks%>" />
<%-
end
-%>
<% else %>
<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node('@global[0]', 'node')" value="<%:Node%>" />
<input class="btn cbi-button" type="button" onclick="set_node_div.set('@global[0]', 'node')" value="<%:Node%>" />
<% end %>
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_set_node_div()" value="<%:Close%>" />
</div>
</div>
</div>
@@ -254,16 +254,6 @@ 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;
@@ -355,7 +345,7 @@ table td, .table .td {
<script type="text/template" id="shunt_rule-tr-template">
<tr class="tr cbi-section-table-row" id="cbi-<%=appname%>-{{id}}">
<td class="td cbi-value-field"><h3>{{id}}</h3></td>
<td class="td cbi-value-field"><b>{{id}}</b></td>
<td class="td cbi-value-field">{{remarks}}</td>
<td class="td cbi-section-table-cell nowrap cbi-section-actions td-actions">
<div class="shunt_rule-wrapper">
@@ -368,11 +358,11 @@ table td, .table .td {
</tr>
</script>
<fieldset class="cbi-section">
<legend>Sing-Box/Xray <%:Shunt Rule%></legend>
<div class="cbi-section-descr"><a style="color: red"><%:Please note attention to the priority, the higher the order, the higher the priority.%></a></div>
<div class="cbi-section">
<h3>Sing-Box/Xray <%:Shunt Rule%></h3>
<div class="cbi-section-descr"><a style="color: red;text-decoration: none"><%:Please note attention to the priority, the higher the order, the higher the priority.%></a></div>
<div id="shunt_rule_list"></div>
</fieldset>
</div>
<script type="text/javascript">
function loadNodeList() {
@@ -3,6 +3,9 @@ local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=api.url("server_user")%>/' + node_id;
document.addEventListener("DOMContentLoaded", function () {
waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) {
let o_val = el.value;
@@ -12,12 +15,12 @@ local api = self.api
let save = true;
if (save) {
//document.getElementsByClassName('cbi-button-save')[0].click();
update_node({
enable: getOption("enable").value,
remarks: getOption("remarks").value,
type: getOption("type").value,
server_update_config(node_id, {
enable: getOption("<%=self.config%>", node_id, "enable").value,
remarks: getOption("<%=self.config%>", node_id, "remarks").value,
type: getOption("<%=self.config%>", node_id, "type").value,
protocol: ''
});
}, node_config_url);
}
}
});
@@ -1,51 +1,15 @@
<%
local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=api.url("server_user")%>/' + node_id;
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function getOption(opt) {
var obj;
var id = 'cbid.<%=self.config%>.' + node_id + '.' + opt;
obj = document.getElementsByName(id)[0] || document.getElementById(id);
if (obj) {
var combobox = document.getElementById('cbi.combobox.' + id);
if (combobox) {
obj.combobox = combobox;
}
var div = document.getElementById(id);
if (div && div.getElementsByTagName("li").length > 0) {
obj = div;
}
return obj;
} else {
return null;
}
}
function update_node(data) {
XHR.get('<%=api.url("server_user_update")%>', {
id: node_id,
function server_update_config(cfgid, data, redirect_url) {
XHR.get('<%=self.api.url("server_update_config")%>', {
id: cfgid,
data: JSON.stringify(data)
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
window.location.href = node_config_url;
if (redirect_url) {
window.location.href = redirect_url;
}
}
else {
alert("<%:Error%>");
+5 -2
View File
@@ -337,8 +337,8 @@ msgstr "آیا از حذف گره‌های انتخاب شده اطمینان د
msgid "You no select nodes !"
msgstr "شما هیچ گره‌ای انتخاب نکرده‌اید!"
msgid "Are you sure set to"
msgstr "آیا مطمئن هستید که تنظیم شود به"
msgid "Are you sure set this node?"
msgstr "مطمئنی این گره رو تنظیم کردی؟"
msgid "the server?"
msgstr "سرور؟"
@@ -970,6 +970,9 @@ msgstr "توجه: مصرف منابع را افزایش می‌دهد."
msgid "Shunt Rule"
msgstr "قانون شنت"
msgid "Shunt Rule Group"
msgstr "گروه قوانین شنت"
msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "لطفاً به اولویت توجه کنید، هرچه ترتیب بالاتر باشد، اولویت بیشتر است."
+5 -2
View File
@@ -334,8 +334,8 @@ msgstr "Вы действительно хотите удалить выбран
msgid "You no select nodes !"
msgstr "Вы не выбрали ни одного узла!"
msgid "Are you sure set to"
msgstr "Вы действительно хотите назначить"
msgid "Are you sure set this node?"
msgstr "Вы уверены, что задали этот узел?"
msgid "the server?"
msgstr "активным сервером?"
@@ -971,6 +971,9 @@ msgstr "Примечание: увеличивает нагрузку на си
msgid "Shunt Rule"
msgstr "Правила разделения трафика"
msgid "Shunt Rule Group"
msgstr "Группа правил шунтирования"
msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "Обратите внимание: правила выше в списке имеют более высокий приоритет."
+5 -2
View File
@@ -334,8 +334,8 @@ msgstr "你确定要删除选择的节点吗?"
msgid "You no select nodes !"
msgstr "你没有选择任何节点!"
msgid "Are you sure set to"
msgstr "你确定要设为"
msgid "Are you sure set this node?"
msgstr "你确定要设置这个节点吗?"
msgid "the server?"
msgstr "服务器吗?"
@@ -962,6 +962,9 @@ msgstr "注:会增加一些系统资源的开销。"
msgid "Shunt Rule"
msgstr "分流规则"
msgid "Shunt Rule Group"
msgstr "分流规则组"
msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "请注意优先级问题,排序越上面优先级越高。"
+5 -2
View File
@@ -334,8 +334,8 @@ msgstr "你確定要刪除選择的節點吗?"
msgid "You no select nodes !"
msgstr "你沒有選择任何節點!"
msgid "Are you sure set to"
msgstr "你確定要設為"
msgid "Are you sure set this node?"
msgstr "你確定要設定這個節點嗎?"
msgid "the server?"
msgstr "服務器吗?"
@@ -962,6 +962,9 @@ msgstr "注:会增加一些系統资源的開销。"
msgid "Shunt Rule"
msgstr "分流規則"
msgid "Shunt Rule Group"
msgstr "分流規則組"
msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "請注意優先級問題,排序越上面優先級越高。"