<%
local api = require "luci.passwall.api"
-%>
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js?v=25.11.27"></script>

<style>
table th, .table .th {
	text-align: center;
}

table td, .table .td {
	text-align: center;
	/* white-space: nowrap; */
	word-break: keep-all;
}

#set_node_div {
	display: none;
	width: 30rem;
	position: fixed;
	top:50%;
	padding-top: 30px;
	z-index: 99;
	text-align: center;
	background: white;
	box-shadow: darkgrey 10px 10px 30px 5px;
}

._now_use {
	color: red !important;
}

._now_use_bg {
	background: #5e72e445 !important;
}

.ping a:hover{
	text-decoration : underline;
}

@media (prefers-color-scheme: dark) {
	._now_use_bg {
		background: #4a90e2 !important;
	}
}

.td.cbi-section-actions {
	text-align: right !important;
}

.node-wrapper {
	align-items: center;
	display: inline-flex !important;
	gap: 4px;
}

.cbi-tabmenu > li {
	margin-right: 2px !important;
}

.cbi-tabmenu > li:last-child {
	margin-right: 0 !important;
}

.node-wrapper .drag-handle {
	cursor: grab !important;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	font-size: 20px;
	font-weight: 100;
	padding: 0 !important;
	line-height: inherit;
	user-select: none;
	color: #00000070;
	align-self: stretch;
}

#cbi-passwall-nodes .pw-checkbox, #cbi-passwall-nodes th:nth-child(1) {
	padding-right: 0px;
}

#select_all_btn {
	display: none;
}

/* enable flex for small screens*/
@media screen and (max-width: 1152px) {
	.cbi-section-table-row {
		display: flex;
		flex-direction: row;
		flex-wrap: wrap;
		justify-content: space-between;
	}

	#cbi-passwall-nodes-default-fieldset {
		margin: 0;
	}

	.cbi-section-table-titles {
		display: none !important;
	}

	/* meticulously control how each component occupies the limited space we have */
	#cbi-passwall-nodes .pw-checkbox, #cbi-passwall-nodes th:nth-child(1) {
		flex: 0 0 40px;
		min-width: 0;
	}

	#cbi-passwall-nodes .pw-remark {
		flex: 1 1 30%;
		min-width: 0;
	}

	#cbi-passwall-nodes .pw-ping, #cbi-passwall-nodes .pw-tcping, #cbi-passwall-nodes .pw-urltest {
		flex: 0 0 50px;
		white-space: nowrap;
		min-width: 0;
	}

	.pw-actions {
		padding-top: 0 !important;
		border-top-width: 0 !important;
		flex: 1 1 350px;
	}

	#select_all_btn {
		display: inline-block !important;
	}
}

/* shrink actionbar even further for mobile devices */
@media screen and (max-width: 500px) {
	.node-wrapper {
		gap: 0;
	}

	.cbi-button {
		margin-left: 0 !important;
		margin-right: 1px !important;
	}

	.pw-actions {
		padding-left: 5px!important;
		padding-right: 5px !important;
	}
}

.sortable-chosen {
	background-color: rgba(220, 235, 245, 0.4) !important;
	opacity: 0.7;
}

.sortable-ghost {
	background: #cce5ff !important;
	height: 3px !important;
}

.dragging-row {
	background-color: rgba(131, 191, 255, 0.7) !important;
	box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>

<% if api.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[
	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 ajax = {
		post: function(url, data, fn_success, timeout, fn_timeout) {
			var xhr = new XMLHttpRequest();
			var code = ajax.encode(data);
			xhr.open("POST", url, true);
			xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

			if (timeout && timeout > 1000) {
				xhr.timeout = timeout;
			}
			if (fn_timeout) {
				xhr.ontimeout = function() {
					fn_timeout(xhr);
				}
			}
			xhr.onreadystatechange = function() {
				if(xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) {
					var json = null;
					if (xhr.getResponseHeader("Content-Type") == "application/json") {
						try {
							json = eval('(' + xhr.responseText + ')');
						}
						catch(e) {
							json = null;
						}
					}
					fn_success(xhr, json);
				}
			};
			xhr.send(code);
		},
		encode: function(obj) {
			obj = obj ? obj : { };
			obj['_'] = Math.random();

			if (typeof obj == 'object')
			{
				var code = '';
				var self = this;

				for (var k in obj)
					code += (code ? '&' : '') +
						k + '=' + encodeURIComponent(obj[k]);

				return code;
			}

			return obj;
		}
	}

	function to_add_node() {
		const dom = document.getElementsByClassName("cbi-tab")[0];
		const current_group = dom.getAttribute("group_name")
		window.location.href='<%=api.url("add_node")%>?redirect=1&group=' + current_group;
	}

	function copy_node(cbi_id) {
		window.location.href = '<%=api.url("copy_node")%>' + "?section=" + cbi_id;
	}

	function del_node(cbi_id) {
		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.passwall." + 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 set_select_all_state(sectionChecked) {
		var visibleContainer = document.querySelector('#cbi-passwall-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall-nodes > .cbi-tabcontainer[style*="display: block"]');
		if (!visibleContainer) return;
		var nodes = visibleContainer.getElementsByClassName("nodes_select");
		var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
		var selectAllBtn = document.getElementById("select_all_btn");
		for (var i = 0; i < nodes.length; i++) {
			nodes[i].checked = sectionChecked;
		}
		if (selectAllChk) {
			selectAllChk.checked = sectionChecked;
			selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
			selectAllChk.setAttribute("onclick", sectionChecked ? "dechecked_all_node(this)" : "checked_all_node(this)");
		}
		if (selectAllBtn) {
			selectAllBtn.value = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
			selectAllBtn.setAttribute("onclick", sectionChecked ? "dechecked_all_node(this)" : "checked_all_node(this)");
		}
	}

	function checked_all_node(btn) {
		set_select_all_state(true);
	}

	function dechecked_all_node(btn) {
		set_select_all_state(false);
	}

	function update_select_state() {
		var visibleContainer = document.querySelector('#cbi-passwall-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall-nodes > .cbi-tabcontainer[style*="display: block"]');
		if (!visibleContainer) return;
		var nodes = visibleContainer.getElementsByClassName("nodes_select");
		if (!nodes.length) return;
		var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
		var selectAllBtn = document.getElementById("select_all_btn");
		var checkedCount = 0;
		for (var i = 0; i < nodes.length; i++) {
			if (nodes[i].checked) checkedCount++;
		}
		var allChecked = checkedCount === nodes.length;
		var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
		var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)";

		function updateElement(el) {
			if (!el) return;
			if ("checked" in el) el.checked = allChecked;
			if ("title" in el) el.title = title;
			if ("value" in el) el.value = title;
			el.setAttribute("onclick", onclickFunc);
		}

		updateElement(selectAllChk);
		updateElement(selectAllBtn);
	}

	function delete_select_nodes() {
		var ids = [];
		var visibleContainer = document.querySelector('#cbi-passwall-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall-nodes > .cbi-tabcontainer[style*="display: block"]');
		if (!visibleContainer) return;
		var doms = visibleContainer.getElementsByClassName("nodes_select");
		if (doms && doms.length > 0) {
			for (var i = 0 ; i < doms.length; i++) {
				if (doms[i].checked) {
					ids.push(doms[i].getAttribute("cbid"))
				}
			}
			if (ids.length > 0) {
				if (confirm('<%:Are you sure to delete select nodes?%>') == true){
					XHR.get('<%=api.url("delete_select_nodes")%>', {
						ids: ids.join()
					},
					function(x, data) {
						if (x && x.status == 200) {
							/*
							for (var i = 0 ; i < ids.length; i++) {
								var box = document.getElementById("cbi-passwall-" + ids[i]);
								box.remove();
							}
							*/
							window.location.href = '<%=api.url("node_list")%>';
						}
						else {
							alert("<%:Error%>");
						}
					});
				}
			}
		}
		if (ids.length <= 0) {
			alert("<%:You no select nodes !%>");
		}
	}

	function set_node(protocol) {
		if (confirm('<%:Are you sure set to%> ' + protocol.toUpperCase() + '<%:the server?%>')==true){
			window.location.href = '<%=api.url("set_node")%>?protocol=' + protocol + '&section=' + section;
		}
	}

	function get_address_full(id) {
		var address = (document.getElementById("cbid.passwall." + id + ".address") || {}).value || "";
		var port = (document.getElementById("cbid.passwall." + id + ".port") || {}).value || "";
		//判断是否含有汉字
		var reg = /[\u4E00-\u9FFF]+/;
		address = !reg.test(address) ? address : "";
		return { address: address, port: port };
	}

	function get_node_order(group) {
		let table = document.getElementById("cbi-passwall-nodes-" + group + "-table");
		if (!table) {
			return;
		}
		let rows = table.querySelectorAll("tr.cbi-section-table-row");
		if (!rows || rows.length === 0) {
			return;
		}
		var ids = [];
		rows.forEach(function(row) {
			var id = row.id.replace("cbi-passwall-", "");
			ids.push(id);
		});
		return ids;
	}

	function save_current_page_order(group) {
		var table = document.getElementById("cbi-passwall-nodes-" + group + "-table");
		if (!table) {
			alert("<%:No table!%>");
			return;
		}
		var rows = table.querySelectorAll("tr.cbi-section-table-row");
		if (!rows || rows.length === 0) {
			alert("<%:No nodes!%>");
			return;
		}
		var btn = document.getElementById("save_order_btn_" + group);
		if (btn) {
			btn.style.display = "none";
			btn.disabled = true;
		}
		var ids = [];
		rows.forEach(function(row) {
			var id = row.id.replace("cbi-passwall-", "");
			ids.push(id);
		});
		XHR.get('<%=api.url("save_node_order")%>', {
				group: group,
				ids: ids.join(",")
			},
			function(x, result) {
				if (btn) {
					btn.style.display = null;
					btn.disabled = false;
				}
				if (x && x.status === 200) {
					origin_group_node_order[group] = get_node_order(group);
					alert("<%:Saved current page order successfully.%>");
					if (btn) {
						btn.style.display = "none";
					}
				} else {
					alert("<%:Save failed!%>");
				}
			}
		);
	}

	//获取当前使用的节点
	function get_now_use_node() {
		XHR.get('<%=api.url("get_now_use_node")%>', null,
			function(x, result) {
				var id = result["TCP"];
				if (id) {
					var dom = document.getElementById("cbi-passwall-" + id);
					if (dom) {
						dom.title = '<%=api.i18n.translatef("Currently using %s node", "TCP")%>';
						dom.classList.add("_now_use_bg");
						//var v = "<a style='color: red'>当前TCP节点：</a>" + document.getElementById("cbid.passwall." + id + ".remarks").value;
						//document.getElementById("cbi-passwall-" + id + "-remarks").innerHTML = v;
						var dom_remarks = document.getElementById("cbi-passwall-" + id + "-remarks");
						if (dom_remarks) {
							dom_remarks.classList.add("_now_use");
						}
					}
				}
				id = result["UDP"];
				if (id) {
					var dom = document.getElementById("cbi-passwall-" + id);
					if (dom) {
						if (result["TCP"] == result["UDP"]) {
							dom.title = '<%=api.i18n.translatef("Currently using %s node", "TCP/UDP")%>';
						} else {
							dom.title = '<%=api.i18n.translatef("Currently using %s node", "UDP")%>';
						}
						dom.classList.add("_now_use_bg");
						var dom_remarks = document.getElementById("cbi-passwall-" + id + "-remarks");
						if (dom_remarks) {
							dom_remarks.classList.add("_now_use");
						}
					}
				}
			}
		);
	}

	function urltest_node(cbi_id, dom) {
		if (cbi_id != null) {
			dom.onclick = null
			dom.innerText = "<%:Check...%>";
			XHR.get('<%=api.url("urltest_node")%>', {
					id: cbi_id
				},
				function(x, result) {
					if(x && x.status == 200) {
						if (result.use_time == null || result.use_time.trim() == "") {
							dom.outerHTML = "<font style='color:red'><%:Timeout%></font>";
						} else {
							var color = "red";
							var use_time = result.use_time;
							use_time = parseInt(use_time) + 1;
							if (use_time < 1000) {
								color = "green";
							} else if (use_time < 2000) {
								color = "#fb9a05";
							} else {
								color = "red";
							}
							dom.outerHTML = "<font style='color:" + color + "'>" + use_time + " ms" + "</font>";
						}
					} else {
						dom.outerHTML = "<font style='color:red'><%:Error%></font>";
					}
				}
			);
		}
	}

	function ping_node(cbi_id, dom, type) {
		var full = get_address_full(cbi_id);
		if ((type == "icmp" && full.address != "" ) || (type == "tcping" && full.address != "" && full.port != "")) {
			dom.onclick = null
			dom.innerText = "<%:Check...%>";
			XHR.get('<%=api.url("ping_node")%>', {
					address: full.address,
					port: full.port,
					type: type
				},
				function(x, result) {
					if(x && x.status == 200) {
						if (result.ping == null || result.ping.trim() == "") {
							dom.outerHTML = "<font style='color:red'><%:Timeout%></font>";
						} else {
							var ping = parseInt(result.ping);
							if (ping < 100)
								dom.outerHTML = "<font style='color:green'>" + result.ping + " ms" + "</font>";
							else if (ping < 200)
								dom.outerHTML = "<font style='color:#fb9a05'>" + result.ping + " ms" + "</font>";
							else if (ping >= 200)
								dom.outerHTML = "<font style='color:red'>" + result.ping + " ms" + "</font>";
						}
					}
				}
			);
		}
	}

	/* 自动Ping */
	function pingAllNodes() {
		if (auto_detection_time == "icmp" || auto_detection_time == "tcping") {
			const now = Date.now();
			var nodes = [];
			const ping_value = document.getElementsByClassName(auto_detection_time == "tcping" ? 'tcping_value' : 'ping_value');
			for (var i = 0; i < ping_value.length; i++) {
				var cbi_id = ping_value[i].getAttribute("cbiid");
				var full = get_address_full(cbi_id);
				if ((auto_detection_time == "icmp" && full.address != "" ) || (auto_detection_time == "tcping" && full.address != "" && full.port != "")) {
					var flag = false;
					// Merge duplicates
					for (var j = 0; j < nodes.length; j++) {
						if (nodes[j].address == full.address && nodes[j].port == full.port) {
							nodes[j].indexs = nodes[j].indexs + "," + i;
							flag = true;
							break;
						}
					}
					if (flag)
						continue;

					const cacheData = JSON.parse(localStorage.getItem(auto_detection_time + ":" + full.address + ":" + full.port));
					if (cacheData && cacheData.savetime && (now - cacheData.timestamp) < cacheData.savetime) {
						if (cacheData.value < 100)
							ping_value[i].innerHTML = "<font style='color:green'>" + cacheData.value + " ms" + "</font>";
						else if (cacheData.value < 200)
							ping_value[i].innerHTML = "<font style='color:#fb9a05'>" + cacheData.value + " ms" + "</font>";
						else if (cacheData.value >= 200)
							ping_value[i].innerHTML = "<font style='color:red'>" + cacheData.value + " ms" + "</font>";
					} else {
						localStorage.removeItem(auto_detection_time + ":" + full.address + ":" + full.port);
						nodes.push({
							indexs: i + "",
							address: full.address,
							port: full.port
						});
					}
				}
			}

			const _xhr = (index) => {
				return new Promise((res) => {
					const dom = nodes[index];
					if (!dom) res()
					ajax.post('<%=api.url("ping_node")%>', {
						index: dom.indexs,
						address: dom.address,
						port: dom.port,
						type: auto_detection_time
					},
					function(x, result) {
						if (x && x.status == 200) {
							var strs = dom.indexs.split(",");
							for (var i = 0; i < strs.length; i++) {
								if (result.ping == null || result.ping.trim() == "") {
									ping_value[strs[i]].innerHTML = "<font style='color:red'><%:Timeout%></font>";
								} else {
									var ping = parseInt(result.ping);
									//save to cache
									const cache_data = {
										dom_id: strs[i],
										timestamp: Date.now(),
										savetime: 60 * 1000,
										value: ping
									};
									localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
									if (ping < 100)
										ping_value[strs[i]].innerHTML = "<font style='color:green'>" + result.ping + " ms" + "</font>";
									else if (ping < 200)
										ping_value[strs[i]].innerHTML = "<font style='color:#fb9a05'>" + result.ping + " ms" + "</font>";
									else if (ping >= 200)
										ping_value[strs[i]].innerHTML = "<font style='color:red'>" + result.ping + " ms" + "</font>";
								}
							}
						}
						res();
					},
					5000,
					function(x) {
						var strs = dom.indexs.split(",");
						for (var i = 0; i < strs.length; i++) {
							ping_value[strs[i]].innerHTML = "<font style='color:red'><%:Timeout%></font>";
						}
						res();
					}
				);
				})
			}

			let task = -1;
			const thread = () => {
				task = task + 1
				if (nodes[task]) {
					_xhr(task).then(thread);
				}
			}
			for (let i = 0; i < 20; i++) {
				thread()
			}
		}
	}

	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;
	}

	//列表拖动重排
	function initSortableForTable(table) {
		if (!table) return null;
		let group = table.id.replace("cbi-passwall-nodes-", "").replace("-table", "")
		var root = table.querySelector('tbody') || table;
		if (root._sortable_initialized) return root._sortable_instance;
		root._sortable_initialized = true;
		var opts = {
			handle: ".drag-handle",
			draggable: "tr.cbi-section-table-row",
			animation: 150,
			ghostClass: "dragging-row",
			fallbackOnBody: true,
			forceFallback: false,
			swapThreshold: 0.65,
			onEnd: function (evt) {
				//save_current_page_order(group); // 自动提交保存
				let save_order_btn = document.getElementById("save_order_btn_" + group);
				if (save_order_btn) {
					const new_order = get_node_order(group);
					if (!arraysEqual(new_order, origin_group_node_order[group])) {
						save_order_btn.style.display = null;
					} else {
						save_order_btn.style.display = "none";
					}
				}
			}
		};
		try {
			var instance = Sortable.create(root, opts);
			root._sortable_instance = instance;
			return instance;
		} catch (err) {
			root._sortable_initialized = false;
			console.error("Sortable init failed:", err);
			return null;
		}
	}

	function initAllSortable(group_nodes) {
		if (typeof Sortable === 'undefined') {
			var retries = 0;
			var maxRetries = 25;
			var t = setInterval(function () {
				retries++;
				if (typeof Sortable !== 'undefined') {
					clearInterval(t);
					for (var group in group_nodes) {
						var table = document.getElementById("cbi-passwall-nodes-" + group + "-table");
						initSortableForTable(table);
					}
				} else if (retries >= maxRetries) {
					clearInterval(t);
				}
			}, 200);
		} else {
			for (var group in group_nodes) {
				var table = document.getElementById("cbi-passwall-nodes-" + group + "-table");
				initSortableForTable(table);
			}
		}
	}

	function escape_html(s) {
		return s.replace(/[&<>"']/g, c => ({
			"&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;"
		}[c]));
	}
</script>

<script type="text/template" id="nodes-table-template">
	<fieldset class="cbi-section cbi-tblsection" id="cbi-passwall-nodes-{{group}}-fieldset">
		<table class="table cbi-section-table" id="cbi-passwall-nodes-{{group}}-table" style="">
			<tr class="tr cbi-section-table-titles anonymous">
				<th class="th cbi-section-table-cell" style="width:20px">
					<input class="cbi-input-checkbox nodes_select_all" type="checkbox" onclick="checked_all_node(this)" title="<%:Select all%>"/>
				</th>
				<th class="th cbi-section-table-cell" style="width:40%"><%:Remarks%></th>
				<th class="th cbi-section-table-cell" style="width:8%">Ping</th>
				<th class="th cbi-section-table-cell" style="width:8%">TCPing</th>
				<th class="th cbi-section-table-cell" style="width:8%"><%:URL Test%></th>
				<th class="th cbi-section-table-cell cbi-section-actions"></th>
			</tr>
			{{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-apply" style="display: none" type="button" id="save_order_btn_{{group}}" value="<%:Save Order%>" onclick="save_current_page_order('{{group}}')">
		</div>
	</fieldset>
</script>

<script type="text/template" id="node-tr-template">
	<tr class="tr cbi-section-table-row" id="cbi-passwall-{{id}}">
		<input class="hidden" id="cbid.passwall.{{id}}.remarks" value="{{remarks_val}}"/>
		<input class="hidden" id="cbid.passwall.{{id}}.address" value="{{address_val}}"/>
		<input class="hidden" id="cbid.passwall.{{id}}.port" value="{{port_val}}"/>
		<td class="td cbi-value-field pw-checkbox">
			<input class="cbi-input-checkbox nodes_select" type="checkbox" cbid="{{id}}" onclick="update_select_state()"/>
		</td>
		<td class="td cbi-value-field pw-remark">{{remarks}}</td>
		<td class="td cbi-value-field pw-ping">{{ping}}</td>
		<td class="td cbi-value-field pw-tcping">{{tcping}}</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="location.href='<%=api.url("node_config")%>/{{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%>">
				<span class="drag-handle center" title="<%:Drag to reorder%>">⠿</span>
			</div>
		</td>
	</tr>
</script>

<fieldset class="cbi-section" id="node_list">
</fieldset>

<script type="text/javascript">
	function get_remarks_name(o) {
		let str = "";
		let remarks = o["remarks"] || "";
		let type = o["type"] || "";
		str += "<input type='hidden' id='cbid.passwall." + o[".name"] + ".type' value='" + type + "'/>";
		if (type == "sing-box" || type == "Xray") {
			let protocol = o["protocol"]
			let p = "";
			if (protocol == "_balancing") {
				p = "<%:Balancing%>";
			} else if (protocol == "_urltest") {
				p = "URLTest";
			} else if (protocol == "_shunt") {
				p = "<%:Shunt%>";
			} else if (protocol == "vmess") {
				p = "VMess";
			} else if (protocol == "vless") {
				p = "VLESS";
			} else if (protocol == "shadowsocks") {
				p = "SS";
			} else if (protocol == "shadowsocksr") {
				p = "SSR";
			} else if (protocol == "wireguard") {
				p = "WG";
			} else if (protocol == "hysteria") {
				p = "HY";
			} else if (protocol == "hysteria2") {
				p = "HY2";
			} else if (protocol == "anytls") {
				p = "AnyTLS";
			} else if (protocol == "ssh") {
				p = "SSH";
			} else {
				p = protocol.charAt(0).toUpperCase() + protocol.slice(1);
			}
			if (type == "sing-box") {
				type = "Sing-Box";
			}
			type += " " + p;
		}
		let address = o["address"] || "";
		let port = o["port"] || "";
		let port_s = "";
		if (port != "") {
			port_s = port;
		} else {
			port_s = o["hysteria_hop"] || o["hysteria2_hop"];
		}
		str += type + "：" + remarks;
		return str;
	}

	function loadNodeList() {
		XHR.get('<%=api.url("get_node")%>', null, function(x, result) {
			var node_list = result

			var group_nodes = {}
			for (let i = 0; i < node_list.length; i++) {
				let _node = node_list[i]
				if (!_node.group || _node.group === "") {
					_node.group = "default"
				}
				if (!group_nodes[_node.group]) {
					group_nodes[_node.group] = []
				}
				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-passwall-nodes">'
			var nodes_table_template = document.getElementById("nodes-table-template");
			var node_template = document.getElementById("node-tr-template");
			var default_group = null
			for (let group in group_nodes) {
				if (default_group == null)
					default_group = group

				var table_html = "";
				if (true) {
					//Node List
					var new_nodes_table_dom = nodes_table_template.cloneNode(true);
					var _html = new_nodes_table_dom.innerHTML;
					_html = _html.split("{{group}}").join(group);
					var node_tr_html = "";
					for (var i = 0; i < group_nodes[group].length; i++) {
						let o = group_nodes[group][i]
						var newDom = node_template.cloneNode(true);
						newDom.classList.add("cbi-rowstyle-" + (i % 2 + 1));
						var innerHTML = newDom.innerHTML;
						if (auto_detection_time != "icmp" && o["address"] && o["port"]) {
							innerHTML = innerHTML.split("{{ping}}").join('<span class="ping"><a href="javascript:void(0)" onclick="javascript:ping_node(\'{{id}}\', this, \'icmp\')"><%:Test%></a></span>');
						} else {
							innerHTML = innerHTML.split("{{ping}}").join('<span class="ping_value" cbiid="{{id}}">---</span>');
						}
						if (auto_detection_time != "tcping" && o["address"] && o["port"]) {
							innerHTML = innerHTML.split("{{tcping}}").join('<span class="ping"><a href="javascript:void(0)" onclick="javascript:ping_node(\'{{id}}\', this, \'tcping\')"><%:Test%></a></span>');
						} else {
							innerHTML = innerHTML.split("{{tcping}}").join('<span class="tcping_value" cbiid="{{id}}">---</span>');
						}
						innerHTML = innerHTML.split("{{url_test}}").join('<span class="ping"><a href="javascript:void(0)" onclick="javascript:urltest_node(\'{{id}}\', this)"><%:Test%></a></span>');
						innerHTML = innerHTML.split("{{id}}").join(o[".name"]);
						innerHTML = innerHTML.split("{{group}}").join(o["group"] || "");
						let node_remarks = get_remarks_name(o);
						if (show_node_info == "1") {
							if (o["address"] && o["port"]) {
								let _address = o["address"]
								if (o["full_address"])
									_address = o["full_address"]
								node_remarks += "<br>" + _address + ":" + o["port"]
							}
						}
						innerHTML = innerHTML.split("{{remarks}}").join(node_remarks);
						innerHTML = innerHTML.split("{{remarks_val}}").join(o["remarks"]);
						innerHTML = innerHTML.split("{{address_val}}").join(o["address"] || "");
						innerHTML = innerHTML.split("{{port_val}}").join(o["port"] || "");

						node_tr_html += innerHTML
					}
					_html = _html.split("{{node-tr}}").join(node_tr_html);
					table_html = _html;
				}

				var group_name = group
				if (group === "default") {
					group_name = "<%:default%>"
				}

				tab_ul_li_html +=
					'<li group_name="' + group + '" id="tab.passwall.nodes.' + group + '" class="cbi-tab">' +
						'<a onclick="this.blur(); return cbi_t_switch(\'passwall.nodes\', \'' + group + '\')" href="<%=REQUEST_URI%>?tab.passwall.nodes=' + group + '">' + escape_html(group_name) + " | " + "<font style='color: red'>" + group_nodes[group].length + '</font></a>' +
					'</li>'
				tab_content_html +=
					'<div class="cbi-tabcontainer" id="container.passwall.nodes.' + group + '" style="display: none;">' +
					'' + table_html +
					'</div>'
			}

			tab_ul_html += tab_ul_li_html + '</ul>'
			tab_content_html += '</fieldset>'
			var tab_html = tab_ul_html + tab_content_html

			document.getElementById("node_list").innerHTML = tab_html

			for (let group in group_nodes) {
				cbi_t_add("passwall.nodes", group)
			}

			if (default_group) {
				cbi_t_switch("passwall.nodes", default_group)
			}

			origin_group_node_order = {};
			for (let group in group_nodes) {
				origin_group_node_order[group] = get_node_order(group);
			}

			initAllSortable(group_nodes);

			//clear expire data
			if (localStorage && localStorage.length > 0) {
				const now = Date.now();
				for (let i = 0; i < localStorage.length; i++) {
					let key = localStorage.key(i);
					if (key && (key.startsWith("icmp") || key.startsWith("tcping"))) {
						let value_str = localStorage.getItem(key);
						const value = JSON.parse(value_str);
						if (!(value && value.savetime && (now - value.timestamp) < value.savetime)) {
							localStorage.removeItem(key);
						}
					}
				}
			}

			get_now_use_node();

			pingAllNodes();
		});
	}

	loadNodeList();

	//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) {
			XHR.get('<%=api.url("save_node_list_opt")%>', {
				option: option,
				value: value
			}, function(x) {
				if (x && x.status == 200) {
					document.getElementById("node_list").innerHTML = "";
					loadNodeList();
				} else {
					alert("<%:Error%>");
				}
			});
		}

		waitForElement('input[type="checkbox"][name*="passwall"][name*="show_node_info"]', function(el) {
			el.addEventListener("change", () => {
				el.blur();
				show_node_info = el.checked ? "1" : "0";
				onChange("show_node_info", show_node_info);
			});
		});

		waitForElement('select[name*="passwall"][name*="auto_detection_time"]', function(el) {
			el.addEventListener("change", () => {
				el.blur();
				auto_detection_time = el.value;
				onChange("auto_detection_time", auto_detection_time);
			});
		});
	});
	//]]>
</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">
		<div class="cbi-value"><%:You choose node is:%><a style="color: red" id="set_node_name"></a></div>
		<div class="cbi-value">
			<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node('tcp')" value="TCP" />
			<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node('udp')" value="UDP" />
			<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_set_node_div()" value="<%:Close%>" />
		</div>
	</div>
</div>
