1
0
mirror of https://github.com/bestnite/sub2sing-box.git synced 2026-05-19 22:47:32 +00:00
Files
sub2sing-box/api/static/index.html
T
legiz-ru 273856968a Update index.html
fix nodes input
2025-02-08 23:59:58 +03:00

422 lines
16 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="en" class="mdui-theme-auto">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>sub2sing-box</title>
<!-- Include Material Design UI CSS -->
<link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css" />
<!-- Include Google Fonts with Chinese support -->
<link href="https://fonts.googleapis.com/css?family=Roboto|Noto+Sans+SC&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet" />
<script src="https://unpkg.com/mdui@2/mdui.global.js"></script>
<style>
/* Apply a uniform font across Chinese, Russian, and English */
body {
font-family: "Roboto", "Noto Sans SC", sans-serif;
}
h3 {
margin-top: 0;
}
.section {
margin: 20px 0;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
background-color: rgb(var(--mdui-color-surface-container));
}
.section > * {
margin-bottom: 10px;
}
.mdui-container {
border-radius: 10px;
max-width: 1200px;
margin: 30px auto;
}
.header-controls {
display: flex;
gap: 10px;
align-items: center;
}
/* Rename fields: now stacked in a wide layout */
.rename-group {
display: flex;
flex-direction: column;
gap: 10px;
}
</style>
</head>
<body>
<div class="mdui-container">
<div style="display: flex; align-items: center; justify-content: space-between;">
<h2>
<a href="https://github.com/nitezs/sub2sing-box" target="_blank">sub2sing-box</a>
</h2>
<div class="header-controls">
<!-- Language switcher -->
<mdui-dropdown trigger="hover">
<mdui-icon slot="trigger" name="language"></mdui-icon>
<mdui-menu id="language" selects="single">
<mdui-menu-item value="zh-CN">中文</mdui-menu-item>
<mdui-menu-item value="en">English</mdui-menu-item>
<mdui-menu-item value="ru">Русский</mdui-menu-item>
</mdui-menu>
</mdui-dropdown>
<!-- Theme switcher -->
<mdui-dropdown trigger="hover">
<mdui-icon slot="trigger" name="dark_mode"></mdui-icon>
<mdui-menu id="theme" selects="single" value="dark">
<mdui-menu-item value="light" data-i18n="theme-light">Light</mdui-menu-item>
<mdui-menu-item value="dark" data-i18n="theme-dark">Dark</mdui-menu-item>
<mdui-divider></mdui-divider>
<mdui-menu-item value="auto" data-i18n="theme-auto">Auto</mdui-menu-item>
</mdui-menu>
</mdui-dropdown>
</div>
</div>
<!-- Template Section -->
<div class="section">
<h3>
<a href="https://github.com/nitezs/sub2sing-box/tree/master/templates" data-i18n="template">Template</a>
</h3>
<mdui-text-field data-i18n="template" label="Template" type="text" id="template" name="template"
value="https://raw.githubusercontent.com/nitezs/sub2sing-box/refs/heads/master/templates/example-windows.json">
</mdui-text-field>
</div>
<div id="form">
<!-- Nodes Section -->
<div class="section">
<h3 data-i18n="nodes">Nodes</h3>
<div>
<mdui-text-field autosize min-rows="3" max-rows="5" data-i18n="subscription-link"
data-i18n-placeholder="one-per-line"
label="Subscription Link" type="text" id="subscription" name="subscription"
placeholder="One per line">
</mdui-text-field>
</div>
<div>
<mdui-text-field autosize min-rows="3" max-rows="5" data-i18n="proxy-link"
data-i18n-placeholder="one-per-line"
label="Node Share Link" type="text" id="proxy" name="proxy"
placeholder="One per line">
</mdui-text-field>
</div>
<div>
<mdui-text-field data-i18n="delete-node"
data-i18n-placeholder="supports-regex"
label="Delete Node" type="text" id="delete" name="delete"
placeholder="Supports regex">
</mdui-text-field>
</div>
<div>
<mdui-list id="list">
<mdui-list-item>
<span data-i18n="rename-node">Rename Node</span>
<mdui-button slot="end-icon" type="button" onclick="addRenameField()">+</mdui-button>
</mdui-list-item>
</mdui-list>
<div id="renameContainer"></div>
</div>
</div>
<!-- Policy Group Section -->
<div class="section">
<h3 data-i18n="policy-group">Policy Group</h3>
<div>
<mdui-text-field data-i18n="type" label="Type" type="text" id="group-type" name="group-type"
value="selector">
</mdui-text-field>
</div>
<div>
<mdui-select data-i18n="sort-by" label="Sort By" name="sort" id="sort" value="tag">
<mdui-menu-item value="tag" selected data-i18n="node-name">Node Name</mdui-menu-item>
<mdui-menu-item value="num" data-i18n="node-count">Node Count</mdui-menu-item>
</mdui-select>
</div>
<div>
<mdui-select data-i18n="sort-order" label="Sort Order" name="sort-type" id="sort-type" value="asc">
<mdui-menu-item value="asc" selected data-i18n="ascending">Ascending</mdui-menu-item>
<mdui-menu-item value="desc" data-i18n="descending">Descending</mdui-menu-item>
</mdui-select>
</div>
</div>
<!-- Result Section -->
<div class="section">
<mdui-text-field data-i18n="generate-link" label="Generate Link" type="text" id="output" name="output">
<!-- Integrated copy icon inside the Generate Link text field -->
<mdui-icon slot="end-icon" onclick="copyLink()" style="cursor:pointer;">content_copy</mdui-icon>
</mdui-text-field>
<mdui-button color="primary" onclick="openProfileLink()">
<span data-i18n="import-profile">Import Profile</span>
</mdui-button>
</div>
</div>
</div>
<script>
// Translation object (v11 parameters)
const translations = {
"zh-CN": {
"template": "模板",
"nodes": "节点",
"subscription-link": "订阅链接",
"proxy-link": "节点分享链接",
"delete-node": "删除节点",
"rename-node": "重命名节点",
"policy-group": "策略组",
"type": "类型",
"sort-by": "排序依据",
"node-name": "节点名",
"node-count": "节点数量",
"sort-order": "排序方式",
"ascending": "升序",
"descending": "降序",
"generate-link": "生成链接",
"theme-light": "亮色",
"theme-dark": "暗色",
"theme-auto": "跟随系统",
"import-profile": "导入配置",
"one-per-line": "一行一个",
"supports-regex": "支持正则表达式"
},
"en": {
"template": "Template",
"nodes": "Nodes",
"subscription-link": "Subscription Link",
"proxy-link": "Node Share Link",
"delete-node": "Delete Node",
"rename-node": "Rename Node",
"policy-group": "Policy Group",
"type": "Type",
"sort-by": "Sort By",
"node-name": "Node Name",
"node-count": "Node Count",
"sort-order": "Sort Order",
"ascending": "Ascending",
"descending": "Descending",
"generate-link": "Generate Link",
"theme-light": "Light",
"theme-dark": "Dark",
"theme-auto": "Auto",
"import-profile": "Import Profile",
"one-per-line": "One per line",
"supports-regex": "Supports regex"
},
"ru": {
"template": "Шаблон",
"nodes": "Узлы",
"subscription-link": "Ссылка подписки",
"proxy-link": "Ссылка на узел",
"delete-node": "Удалить узел",
"rename-node": "Переименовать узел",
"policy-group": "Группа политик",
"type": "Тип",
"sort-by": "Сортировать по",
"node-name": "Имя узла",
"node-count": "Количество узлов",
"sort-order": "Порядок сортировки",
"ascending": "По возрастанию",
"descending": "По убыванию",
"generate-link": "Создать ссылку",
"theme-light": "Светлая",
"theme-dark": "Темная",
"theme-auto": "Системная",
"import-profile": "Импортировать профиль",
"one-per-line": "Одна на строку",
"supports-regex": "Поддерживает рег. выражения"
}
};
document.addEventListener('DOMContentLoaded', function() {
setupLanguage();
setupTheme();
listenInput();
// Do not auto-generate link on page load to keep Generate Link empty
});
function setupLanguage() {
const languageMenu = document.getElementById('language');
if (languageMenu) {
const userLang = navigator.language || navigator.userLanguage;
const savedLang = localStorage.getItem('language') ||
(userLang.includes('zh') ? 'zh-CN' : userLang.includes('ru') ? 'ru' : 'en');
languageMenu.value = savedLang;
changeLanguage(savedLang);
languageMenu.addEventListener('change', function() {
changeLanguage(languageMenu.value);
});
}
}
function changeLanguage(lang) {
localStorage.setItem('language', lang);
document.documentElement.setAttribute('lang', lang);
document.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n');
const translation = translations[lang][key];
if (translation) {
if (element.tagName.toLowerCase() === 'mdui-text-field' ||
element.tagName.toLowerCase() === 'mdui-select') {
element.setAttribute('label', translation);
} else {
element.textContent = translation;
}
}
});
document.querySelectorAll('mdui-text-field').forEach(element => {
const phKey = element.getAttribute('data-i18n-placeholder');
if (phKey && translations[lang][phKey]) {
element.setAttribute('placeholder', translations[lang][phKey]);
}
});
}
function setupTheme() {
const themeMenu = document.getElementById('theme');
const savedTheme = localStorage.getItem("theme") || "auto";
document.documentElement.className = "mdui-theme-" + savedTheme;
themeMenu.value = savedTheme;
themeMenu.addEventListener("change", function() {
const newTheme = themeMenu.value;
document.documentElement.className = "mdui-theme-" + newTheme;
localStorage.setItem("theme", newTheme);
});
}
function listenInput() {
const inputs = document.querySelectorAll("mdui-text-field, mdui-select, mdui-checkbox");
inputs.forEach(input => {
input.addEventListener("input", generateLink);
input.addEventListener("change", generateLink);
});
}
function cleanListeners() {
const inputs = document.querySelectorAll("#form input, #form textarea");
inputs.forEach(input => {
input.removeEventListener("input", generateLink);
});
const selects = document.querySelectorAll("#form select");
selects.forEach(select => {
select.removeEventListener("change", generateLink);
});
}
// Rename field parameters from v11 - fields are now wide (stacked)
function addRenameField() {
cleanListeners();
const container = document.getElementById("list");
const currentLang = localStorage.getItem('language') || 'en';
const fieldHTML = `
<mdui-list-item class="rename-group">
<mdui-text-field type="text" name="rename_from[]" data-i18n-placeholder="delete-node"
label="${translations[currentLang]['delete-node']}" data-i18n="rename-node">
</mdui-text-field>
<mdui-text-field type="text" name="rename_to[]"
label="${translations[currentLang]['rename-node']}" data-i18n="rename-node">
</mdui-text-field>
<mdui-button slot="end-icon" type="button" onclick="removeThisField(this)">-</mdui-button>
</mdui-list-item>`;
container.insertAdjacentHTML("beforeend", fieldHTML);
listenInput();
}
function removeThisField(button) {
cleanListeners();
button.parentElement.remove();
generateLink();
listenInput();
}
function generateLink() {
const subscription = document.getElementById("subscription").value
.split("\n").filter(i => i.trim());
const proxy = document.getElementById("proxy").value
.split("\n").filter(i => i.trim());
const deleteRule = document.getElementById("delete").value;
const template = document.getElementById("template").value;
const renameFrom = Array.from(document.getElementsByName("rename_from[]")).map(input => input.value);
const renameTo = Array.from(document.getElementsByName("rename_to[]")).map(input => input.value);
const output = document.getElementById("output");
const groupType = document.getElementById("group-type").value;
const sort = document.getElementById("sort").value;
const sortType = document.getElementById("sort-type").value;
let rename = {};
for (let i = 0; i < renameFrom.length; i++) {
if (renameFrom[i] && renameTo[i]) {
rename[renameFrom[i]] = renameTo[i];
}
}
// Only generate link if there is some user input (subscription, proxy, delete, or rename)
if (
subscription.length === 0 &&
proxy.length === 0 &&
deleteRule.trim() === "" &&
Object.keys(rename).length === 0
) {
output.value = "";
return;
}
// Determine base URL using window.location.origin and window.location.pathname
let basePath = window.location.pathname;
// Remove trailing slash if exists
if(basePath.endsWith('/')){
basePath = basePath.slice(0, -1);
}
const baseURL = window.location.origin + basePath;
const data = {
subscription,
proxy,
delete: deleteRule,
template,
rename,
"group-type": groupType,
sort,
"sort-type": sortType
};
output.value = baseURL + "/convert?data=" + encodeBase64(JSON.stringify(data));
}
function encodeBase64(str) {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode("0x" + p1);
})
).replace(/\+/g, "-").replace(/\//g, "_");
}
function openProfileLink() {
const output = document.getElementById("output").value;
if (output) {
window.location.href = "sing-box://import-remote-profile?url=" + encodeURIComponent(output);
} else {
alert("Output link is empty");
}
}
// Integrated copy function for the Generate Link field
function copyLink() {
const output = document.getElementById("output").value;
if (output) {
navigator.clipboard.writeText(output)
.then(() => {
mdui.snackbar({ message: "Link copied!", position: "top" });
})
.catch(err => {
mdui.snackbar({ message: "Failed to copy link!", position: "top" });
});
} else {
mdui.snackbar({ message: "Output link is empty", position: "top" });
}
}
</script>
</body>
</html>