Добавил загрузку конфигов WG
This commit is contained in:
parent
39a8308f16
commit
c53554ba4e
497
mihomo_editor.py
497
mihomo_editor.py
|
|
@ -100,9 +100,7 @@ def parse_wireguard(config_text, custom_name=None):
|
|||
conf = {"interface": {}, "peer": {}}
|
||||
section = None
|
||||
|
||||
# 1. Читаем построчно, чистим комментарии и собираем секции
|
||||
for line in config_text.splitlines():
|
||||
# Удаляем inline комментарии (# или ;)
|
||||
line = line.split('#')[0].split(';')[0].strip()
|
||||
if not line: continue
|
||||
|
||||
|
|
@ -124,11 +122,9 @@ def parse_wireguard(config_text, custom_name=None):
|
|||
if not iface or not peer:
|
||||
return None, "Invalid WireGuard config: missing Interface or Peer"
|
||||
|
||||
# 2. Endpoint (Server + Port)
|
||||
endpoint = peer.get('endpoint', '')
|
||||
if not endpoint: return None, "No Endpoint found"
|
||||
|
||||
# Обработка IPv6 в Endpoint [::1]:port
|
||||
if ']:' in endpoint:
|
||||
server = endpoint.split(']:')[0][1:]
|
||||
port = endpoint.split(']:')[1]
|
||||
|
|
@ -137,19 +133,16 @@ def parse_wireguard(config_text, custom_name=None):
|
|||
else:
|
||||
return None, "Invalid Endpoint format"
|
||||
|
||||
# 3. Name Logic
|
||||
name = "WireGuard"
|
||||
if custom_name:
|
||||
name = custom_name
|
||||
else:
|
||||
# Пытаемся взять имя из первой строки оригинального текста, если там комментарий
|
||||
first_line = config_text.splitlines()[0].strip()
|
||||
if first_line.startswith('#') and len(first_line) > 2:
|
||||
name = first_line[1:].strip()
|
||||
else:
|
||||
name = f"WG_{server}"
|
||||
|
||||
# 4. Address (IP + IPv6)
|
||||
address_raw = iface.get('address', '')
|
||||
if not address_raw: return None, "No Address found"
|
||||
|
||||
|
|
@ -158,7 +151,6 @@ def parse_wireguard(config_text, custom_name=None):
|
|||
ip_v6 = None
|
||||
|
||||
for ip in ips:
|
||||
# Убираем маску /32, /24 и т.д.
|
||||
clean_ip = ip.split('/')[0]
|
||||
if ':' in clean_ip:
|
||||
if not ip_v6: ip_v6 = clean_ip
|
||||
|
|
@ -168,7 +160,6 @@ def parse_wireguard(config_text, custom_name=None):
|
|||
if not ip_v4 and not ip_v6:
|
||||
return None, "No valid IP address found"
|
||||
|
||||
# 5. Сборка YAML
|
||||
y = []
|
||||
y.append(f'- name: "{name}"')
|
||||
y.append(f' type: wireguard')
|
||||
|
|
@ -184,11 +175,9 @@ def parse_wireguard(config_text, custom_name=None):
|
|||
pubk = peer.get('publickey')
|
||||
if pubk: y.append(f' public-key: {pubk}')
|
||||
|
||||
# Исправлено: pre-shared-key (с дефисом)
|
||||
psk = peer.get('presharedkey')
|
||||
if psk: y.append(f' pre-shared-key: {psk}')
|
||||
|
||||
# DNS
|
||||
dns_raw = iface.get('dns')
|
||||
if dns_raw:
|
||||
dns_list = [d.strip() for d in dns_raw.split(',')]
|
||||
|
|
@ -199,27 +188,24 @@ def parse_wireguard(config_text, custom_name=None):
|
|||
|
||||
y.append(' udp: true')
|
||||
|
||||
# 6. AmneziaWG Specific
|
||||
amnezia_keys = ['jc', 'jmin', 'jmax', 's1', 's2', 'h1', 'h2', 'h3', 'h4']
|
||||
amn_opts = {}
|
||||
for k in amnezia_keys:
|
||||
if k in iface:
|
||||
val = iface[k]
|
||||
if val.isdigit():
|
||||
amn_opts[k] = int(val) # Важно: int, не string
|
||||
amn_opts[k] = int(val)
|
||||
|
||||
if amn_opts:
|
||||
y.append(' amnezia-wg-option:')
|
||||
for k, v in amn_opts.items():
|
||||
y.append(f' {k}: {v}')
|
||||
|
||||
# 7. AllowedIPs (добавлено)
|
||||
allowed = peer.get('allowedips')
|
||||
if allowed:
|
||||
al_list = [x.strip() for x in allowed.split(',')]
|
||||
y.append(f' allowed-ips: {json.dumps(al_list)}')
|
||||
|
||||
# Исправлено: persistent-keepalive
|
||||
ka = peer.get('persistentkeepalive')
|
||||
if ka:
|
||||
y.append(f' persistent-keepalive: {ka}')
|
||||
|
|
@ -250,7 +236,6 @@ def insert_proxy_logic(content, proxy_name, target_groups):
|
|||
|
||||
if is_new_group:
|
||||
if in_proxies_list and current_group_name in target_groups and current_group_name not in inserted_in_group:
|
||||
# End of list section
|
||||
prefix = " " * (proxies_list_indent + 2)
|
||||
new_lines.append(prefix + '- "' + proxy_name + '"')
|
||||
inserted_in_group.add(current_group_name)
|
||||
|
|
@ -269,14 +254,11 @@ def insert_proxy_logic(content, proxy_name, target_groups):
|
|||
current_group_name = raw_name.strip("'").strip('"')
|
||||
|
||||
if current_group_name in target_groups and stripped.startswith('proxies:'):
|
||||
# Check for inline list style: proxies: [a, b]
|
||||
if '[' in stripped and stripped.rstrip().endswith(']'):
|
||||
start = line.find('[')
|
||||
end = line.rfind(']')
|
||||
if start != -1 and end != -1:
|
||||
content_inner = line[start + 1:end]
|
||||
# Check if proxy already exists in the list
|
||||
# Simple check: name in text (robust enough for simple cases)
|
||||
if proxy_name not in content_inner:
|
||||
sep = ", " if content_inner.strip() else ""
|
||||
new_content = content_inner + sep + f'"{proxy_name}"'
|
||||
|
|
@ -327,8 +309,6 @@ def replace_proxy_block(content, target_name, new_yaml_lines):
|
|||
found_target = False
|
||||
replaced = False
|
||||
|
||||
# Регулярка для поиска начала блока прокси с указанным именем
|
||||
# Учитывает кавычки и пробелы: - name: "target_name"
|
||||
name_pattern = re.compile(r'^\s*-\s+name:\s*(["\'])?' + re.escape(target_name) + r'(\1)?\s*$')
|
||||
|
||||
i = 0
|
||||
|
|
@ -336,33 +316,19 @@ def replace_proxy_block(content, target_name, new_yaml_lines):
|
|||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
|
||||
# Определяем секцию proxies
|
||||
if stripped.startswith('proxies:'):
|
||||
in_proxies = True
|
||||
new_content_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Если вышли из proxies (новая секция начинается без отступа)
|
||||
if in_proxies and line and not line.startswith(' ') and not line.startswith('\t') and not line.startswith('#'):
|
||||
in_proxies = False
|
||||
|
||||
if in_proxies and not replaced:
|
||||
if name_pattern.match(stripped):
|
||||
# Нашли целевой прокси.
|
||||
# 1. Определяем отступ этого блока (обычно 2 пробела)
|
||||
indent_len = len(line) - len(line.lstrip())
|
||||
|
||||
# 2. Добавляем новый YAML.
|
||||
# new_yaml_lines приходят без отступов (или с базовыми).
|
||||
# Нам нужно убедиться, что первая строка имеет дефис, а остальные - отступ.
|
||||
# Обычно new_yaml_lines[0] уже "- name: ...".
|
||||
# Просто добавим отступ всем строкам.
|
||||
|
||||
# Принудительно меняем имя в новом YAML на целевое, чтобы сохранить структуру
|
||||
# (хотя парсеры уже могли вернуть новое имя, но лучше перестраховаться)
|
||||
if new_yaml_lines and "name:" in new_yaml_lines[0]:
|
||||
# Заменяем имя в первой строке нового конфига на старое (target_name)
|
||||
new_yaml_lines[0] = re.sub(r'name:\s*".*"', f'name: "{target_name}"', new_yaml_lines[0])
|
||||
|
||||
for n_line in new_yaml_lines:
|
||||
|
|
@ -371,28 +337,16 @@ def replace_proxy_block(content, target_name, new_yaml_lines):
|
|||
replaced = True
|
||||
found_target = True
|
||||
|
||||
# 3. Пропускаем старый блок
|
||||
# Читаем дальше, пока не найдем строку с ТАКИМ ЖЕ отступом, начинающуюся с '-' (следующий элемент списка)
|
||||
# или строку с МЕНЬШИМ отступом (конец секции)
|
||||
i += 1
|
||||
while i < len(lines):
|
||||
next_line = lines[i]
|
||||
next_stripped = next_line.strip()
|
||||
next_indent = len(next_line) - len(next_line.lstrip())
|
||||
|
||||
if not next_stripped: # Пустые строки пропускаем/удаляем внутри блока
|
||||
if not next_stripped:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if next_indent < indent_len:
|
||||
# Конец секции proxies
|
||||
break
|
||||
|
||||
if next_indent == indent_len and next_stripped.startswith('-'):
|
||||
# Следующий элемент списка
|
||||
break
|
||||
|
||||
# Это всё еще часть старого блока, пропускаем
|
||||
if next_indent < indent_len: break
|
||||
if next_indent == indent_len and next_stripped.startswith('-'): break
|
||||
i += 1
|
||||
continue
|
||||
|
||||
|
|
@ -403,11 +357,11 @@ def replace_proxy_block(content, target_name, new_yaml_lines):
|
|||
|
||||
|
||||
HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<title>Mihomo Editor v18.8</title>
|
||||
<title>Mihomo Editor v18.9</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.7/ace.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
|
|
@ -550,155 +504,417 @@ button:hover{filter:brightness(1.1)}
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast" id="toast">✅ Успешно сохранено</div>
|
||||
<div class="toast" id="toast" data-i18n="toast_saved">✅ Успешно сохранено</div>
|
||||
<div class="hdr">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<h2 style="margin:0;color:#4caf50">Mihomo Studio</h2>
|
||||
<span style="color:var(--txt-sec);font-size:12px">v18.8 Auto-Panel</span>
|
||||
<h2 style="margin:0;color:#4caf50" data-i18n="title">Mihomo Studio</h2>
|
||||
<span style="color:var(--txt-sec);font-size:12px">v18.9 Auto-Panel</span>
|
||||
</div>
|
||||
<div id="last-load">Loaded: __TIME__</div>
|
||||
</div>
|
||||
<div class="bar">
|
||||
<button onclick="save('save')" class="btn-s">💾 Сохранить</button>
|
||||
<button onclick="save('restart')" class="btn-r">🚀 Рестарт</button>
|
||||
<button onclick="openPanel()" class="btn-g" title="Открыть встроенную панель Mihomo">🌐 Панель</button>
|
||||
<select id="theme-sel" onchange="setTheme(this.value)" style="max-width:120px; padding:0 10px; margin:0;">
|
||||
<option value="dark">🌑 Dark</option>
|
||||
<option value="light">☀️ Light</option>
|
||||
<option value="midnight">🌃 Midnight</option>
|
||||
<option value="cyber">👾 Cyber</option>
|
||||
</select>
|
||||
<button onclick="save('save')" class="btn-s" data-i18n="save">💾 Сохранить</button>
|
||||
<button onclick="save('restart')" class="btn-r" data-i18n="restart">🚀 Рестарт</button>
|
||||
<button onclick="openPanel()" class="btn-g" title="Открыть встроенную панель Mihomo" data-i18n="panel">🌐 Панель</button>
|
||||
|
||||
<div style="display:flex; gap:5px; margin-left:auto;">
|
||||
<select id="lang-sel" onchange="setLang(this.value)" style="width:100px; padding:0 5px;">
|
||||
<option value="ru">🇷🇺 RU</option>
|
||||
<option value="en">🇺🇸 EN</option>
|
||||
<option value="uk">🇺🇦 UA</option>
|
||||
</select>
|
||||
<select id="theme-sel" onchange="setTheme(this.value)" style="width:120px; padding:0 5px;">
|
||||
<option value="dark" data-i18n="theme_dark">🌑 Dark</option>
|
||||
<option value="light" data-i18n="theme_light">☀️ Light</option>
|
||||
<option value="midnight" data-i18n="theme_midnight">🌃 Midnight</option>
|
||||
<option value="cyber" data-i18n="theme_cyber">👾 Cyber</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div id="ed"></div>
|
||||
<div class="sb">
|
||||
<div class="sec">
|
||||
<h3>Профили</h3>
|
||||
<h3><span data-i18n="profiles">Профили</span></h3>
|
||||
<div class="prof-row">
|
||||
<select id="prof-sel">__PROFILES__</select>
|
||||
<button onclick="switchProf()" class="btn-s" style="padding:0; width:36px; justify-content:center;" title="Выбрать">✔</button>
|
||||
<button onclick="downloadProf()" class="btn-g" style="padding:0; width:36px; justify-content:center;" title="Скачать">💾</button>
|
||||
<button onclick="switchProf()" class="btn-s" style="padding:0; width:36px; justify-content:center;" title="Выбрать" data-i18n="select">✔</button>
|
||||
<button onclick="downloadProf()" class="btn-g" style="padding:0; width:36px; justify-content:center;" title="Скачать" data-i18n="download">💾</button>
|
||||
</div>
|
||||
<div class="prof-btns">
|
||||
<button onclick="openAddProf()" class="btn-u">➕ Создать</button>
|
||||
<button onclick="delProf()" class="btn-d">🗑 Удалить</button>
|
||||
<button onclick="openAddProf()" class="btn-u" data-i18n="create">➕ Создать</button>
|
||||
<button onclick="delProf()" class="btn-d" data-i18n="delete">🗑 Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sec">
|
||||
<h3>Управление прокси</h3>
|
||||
<h3><span data-i18n="proxy_mgmt">Управление</span></h3>
|
||||
<div class="proxy-grid">
|
||||
<button onclick="openAddProxyModal()" class="btn-s">➕ Добавить</button>
|
||||
<button onclick="openEditProxyModal()" class="btn-u">✏️ Изменить</button>
|
||||
<button onclick="showRename()" class="btn-g">Aa Переимен.</button>
|
||||
<button onclick="showDel()" class="btn-d">🗑 Удалить</button>
|
||||
<button onclick="openAddProxyModal()" class="btn-s" data-i18n="add">➕ Добавить</button>
|
||||
<button onclick="openEditProxyModal()" class="btn-u" data-i18n="edit">✏️ Заменить</button>
|
||||
<button onclick="showRename()" class="btn-g" data-i18n="rename">Переименовать</button>
|
||||
<button onclick="showDel()" class="btn-d" data-i18n="delete">🗑 Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sec">
|
||||
<h3>Бэкапы</h3>
|
||||
<h3><span data-i18n="backups">Бэкапы</span></h3>
|
||||
<div class="bk-controls">
|
||||
<span>Оставить:</span>
|
||||
<span data-i18n="keep">Оставить:</span>
|
||||
<input type="number" id="bk-lim" value="5" min="1" max="50">
|
||||
<button onclick="cleanBackups()" class="btn-g">Очистить</button>
|
||||
<button onclick="cleanBackups()" class="btn-g" data-i18n="clean">Очистить</button>
|
||||
</div>
|
||||
<div id="bk-list">__BACKUPS__</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="m-grp" class="ovl"><div class="mod"><h3>Добавить в группы:</h3>
|
||||
<div style="display:flex; gap:10px; margin-bottom:10px"><button onclick="tgGrp(true)" class="btn-g" style="flex:1; justify-content:center">☑ Выбрать все</button><button onclick="tgGrp(false)" class="btn-g" style="flex:1; justify-content:center">☐ Снять все</button></div>
|
||||
<div id="m-grp" class="ovl"><div class="mod"><h3 data-i18n="modal_groups">Добавить в группы:</h3>
|
||||
<div style="display:flex; gap:10px; margin-bottom:10px"><button onclick="tgGrp(true)" class="btn-g" style="flex:1; justify-content:center" data-i18n="btn_sel_all">☑ Выбрать все</button><button onclick="tgGrp(false)" class="btn-g" style="flex:1; justify-content:center" data-i18n="btn_sel_none">☐ Снять все</button></div>
|
||||
<div id="g-cnt" class="g-list"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px;padding-top:10px;border-top:1px solid var(--bd)"><button onclick="applyVless()" class="btn-s" style="flex:1;justify-content:center">Добавить</button><button onclick="closeM('m-grp')" class="btn-g" style="flex:1;justify-content:center">Отмена</button></div></div></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px;padding-top:10px;border-top:1px solid var(--bd)"><button onclick="applyVless()" class="btn-s" style="flex:1;justify-content:center" data-i18n="btn_add">Добавить</button><button onclick="closeM('m-grp')" class="btn-g" style="flex:1;justify-content:center" data-i18n="btn_cancel">Отмена</button></div></div></div>
|
||||
|
||||
<div id="m-del" class="ovl"><div class="mod"><h3>Удалить прокси</h3><select id="sel-del"></select><div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px"><button onclick="doDel()" class="btn-d">Удалить</button><button onclick="closeM('m-del')" class="btn-g">Отмена</button></div></div></div>
|
||||
<div id="m-con" class="ovl"><div class="mod"><h3>Консоль</h3><div id="cons">...</div><div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px"><button onclick="location.reload()" class="btn-s">Обновить</button><button onclick="closeM('m-con')" class="btn-g">Закрыть</button></div></div></div>
|
||||
<div id="m-del" class="ovl"><div class="mod"><h3 data-i18n="modal_del_proxy">Удалить прокси</h3><select id="sel-del"></select><div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px"><button onclick="doDel()" class="btn-d" data-i18n="delete">Удалить</button><button onclick="closeM('m-del')" class="btn-g" data-i18n="btn_cancel">Отмена</button></div></div></div>
|
||||
<div id="m-con" class="ovl"><div class="mod"><h3 data-i18n="modal_console">Консоль</h3><div id="cons">...</div><div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px"><button onclick="location.reload()" class="btn-s" data-i18n="btn_update">Обновить</button><button onclick="closeM('m-con')" class="btn-g" data-i18n="btn_close">Закрыть</button></div></div></div>
|
||||
|
||||
<div id="m-ren" class="ovl"><div class="mod">
|
||||
<h3>Переименовать прокси</h3>
|
||||
<p style="margin-top:0;font-size:13px;color:var(--txt-sec)">Выберите прокси для переименования:</p>
|
||||
<h3 data-i18n="modal_ren_proxy">Переименовать прокси</h3>
|
||||
<p style="margin-top:0;font-size:13px;color:var(--txt-sec)" data-i18n="lbl_sel_ren">Выберите прокси для переименования:</p>
|
||||
<select id="sel-ren-proxy"></select>
|
||||
<p style="margin-top:15px;font-size:13px;color:var(--txt-sec)">Новое имя:</p>
|
||||
<input id="inp-ren-newname" placeholder="Введите новое имя">
|
||||
<p style="margin-top:15px;font-size:13px;color:var(--txt-sec)" data-i18n="lbl_new_name">Новое имя:</p>
|
||||
<input id="inp-ren-newname" placeholder="Введите новое имя" data-i18n-ph="ph_new_name">
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px">
|
||||
<button onclick="doRename()" class="btn-s">Переименовать</button>
|
||||
<button onclick="closeM('m-ren')" class="btn-g">Отмена</button>
|
||||
<button onclick="doRename()" class="btn-s" data-i18n="btn_rename">Переименовать</button>
|
||||
<button onclick="closeM('m-ren')" class="btn-g" data-i18n="btn_cancel">Отмена</button>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div id="m-add-prof" class="ovl"><div class="mod">
|
||||
<h3>Новый профиль</h3>
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Имя (англ, без пробелов):</label>
|
||||
<h3 data-i18n="modal_new_prof">Новый профиль</h3>
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_prof_name">Имя (англ, без пробелов):</label>
|
||||
<input id="np-name" placeholder="my_config" style="margin-bottom:10px">
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Содержимое:</label>
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_content">Содержимое:</label>
|
||||
<div style="display:flex; gap:5px; margin-bottom:5px">
|
||||
<button onclick="document.getElementById('np-file').click()" class="btn-u" style="flex:1;justify-content:center">📂 Загрузить файл</button>
|
||||
<button onclick="document.getElementById('np-file').click()" class="btn-u" style="flex:1;justify-content:center" data-i18n="btn_load_file">📂 Загрузить файл</button>
|
||||
</div>
|
||||
<input type="file" id="np-file" style="display:none" onchange="loadProfFile(this)">
|
||||
<textarea id="np-content" rows="10" placeholder="Вставьте YAML конфиг сюда..."></textarea>
|
||||
<textarea id="np-content" rows="10" placeholder="Вставьте YAML конфиг сюда..." data-i18n-ph="ph_paste_yaml"></textarea>
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px">
|
||||
<button onclick="saveNewProf()" class="btn-s">Сохранить</button>
|
||||
<button onclick="closeM('m-add-prof')" class="btn-g">Отмена</button>
|
||||
<button onclick="saveNewProf()" class="btn-s" data-i18n="btn_save">Сохранить</button>
|
||||
<button onclick="closeM('m-add-prof')" class="btn-g" data-i18n="btn_cancel">Отмена</button>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div id="addProxyModal" class="ovl"><div class="mod">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--bd); padding-bottom:10px; margin-bottom:0;">
|
||||
<h3 id="proxyModalTitle" style="margin:0; padding:0; border:0;">Добавить прокси</h3>
|
||||
<h3 id="proxyModalTitle" style="margin:0; padding:0; border:0;" data-i18n="modal_add_proxy">Добавить прокси</h3>
|
||||
<button onclick="closeM('addProxyModal')" style="width:32px; height:32px; padding:0; background:var(--bg-ter); color:var(--txt); font-size:18px;">✕</button>
|
||||
</div>
|
||||
|
||||
<div id="edit-proxy-container" style="display:none; margin-bottom:10px;">
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Выберите прокси для изменения:</label>
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_select_edit">Выберите прокси для изменения:</label>
|
||||
<select id="edit-proxy-sel"></select>
|
||||
<div style="font-size:11px; color:var(--btn-u); margin-top:5px;">⚠️ Данные этого прокси будут полностью заменены новыми!</div>
|
||||
<div style="font-size:11px; color:var(--btn-u); margin-top:5px;" data-i18n="warn_edit">⚠️ Данные этого прокси будут полностью заменены новыми!</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-tabs">
|
||||
<button class="active" onclick="switchTab(event, 'vlessTab')">VLESS</button>
|
||||
<button onclick="switchTab(event, 'wgTab')">WireGuard</button>
|
||||
<button class="active" onclick="switchTab(event, 'vlessTab')" data-i18n="tab_vless">VLESS</button>
|
||||
<button onclick="switchTab(event, 'wgTab')" data-i18n="tab_wg">WireGuard</button>
|
||||
</div>
|
||||
|
||||
<div id="vlessTab" class="tab-content active">
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Ссылка VLESS:</label>
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_vless_link">Ссылка VLESS:</label>
|
||||
<input id="vlessLink" placeholder="vless://..." style="margin-bottom:10px;">
|
||||
|
||||
<div id="vless-name-block">
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Имя прокси (необязательно):</label>
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_proxy_name">Имя прокси (необязательно):</label>
|
||||
<input id="vlessProxyName" placeholder="Автоматически из ссылки" style="margin-bottom:10px;">
|
||||
</div>
|
||||
|
||||
<button onclick="parseVless()" class="btn-s" style="width:100%; justify-content:center;">Сохранить</button>
|
||||
<button onclick="parseVless()" class="btn-s" style="width:100%; justify-content:center;" data-i18n="btn_save">Сохранить</button>
|
||||
</div>
|
||||
|
||||
<div id="wgTab" class="tab-content">
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Конфигурация WireGuard:</label>
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_wg_conf">Конфигурация WireGuard:</label>
|
||||
<textarea id="wgConfig" rows="8" placeholder="Вставьте содержимое .conf файла сюда..." style="width:100%; margin-bottom:10px;"></textarea>
|
||||
|
||||
<div id="wg-name-block">
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Имя прокси (необязательно):</label>
|
||||
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_proxy_name">Имя прокси (необязательно):</label>
|
||||
<input id="wgProxyName" placeholder="Автоматически из Endpoint" style="margin-bottom:10px;">
|
||||
</div>
|
||||
|
||||
<input type="file" id="wgFile" accept=".conf" style="display:none" onchange="loadWgFile(this)">
|
||||
<button onclick="document.getElementById('wgFile').click()" class="btn-u" style="width:100%; justify-content:center; margin-bottom:10px;">📂 Или загрузить .conf файл</button>
|
||||
<button onclick="addWireguard()" class="btn-s" style="width:100%; justify-content:center;">Сохранить</button>
|
||||
<button onclick="document.getElementById('wgFile').click()" class="btn-u" style="width:100%; justify-content:center; margin-bottom:10px;" data-i18n="btn_load_file">📂 Или загрузить .conf файл</button>
|
||||
<button onclick="addWireguard()" class="btn-s" style="width:100%; justify-content:center;" data-i18n="btn_save">Сохранить</button>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div id="m-view-bk" class="ovl"><div class="mod">
|
||||
<h3>Просмотр бэкапа</h3>
|
||||
<h3 data-i18n="modal_view_bk">Просмотр бэкапа</h3>
|
||||
<pre id="bk-content" style="flex-grow:1; overflow-y:auto; min-height: 200px; max-height:60vh;"></pre>
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px;padding-top:10px;border-top:1px solid var(--bd)">
|
||||
<button id="bk-restore-btn" class="btn-r">Восстановить</button>
|
||||
<button onclick="closeM('m-view-bk')" class="btn-g">Закрыть</button>
|
||||
<button id="bk-restore-btn" class="btn-r" data-i18n="btn_restore">Восстановить</button>
|
||||
<button onclick="closeM('m-view-bk')" class="btn-g" data-i18n="btn_close">Закрыть</button>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<script>
|
||||
var ed=ace.edit("ed");ed.setTheme("ace/theme/monokai");ed.session.setMode("ace/mode/yaml");ed.setOptions({fontSize:14,tabSize:2,useSoftTabs:true});
|
||||
var pData=null, GRP_KEY="mihomo_grp_sel", LIM_KEY="mihomo_bk_lim", THM_KEY="mihomo_theme";
|
||||
var pData=null, GRP_KEY="mihomo_grp_sel", LIM_KEY="mihomo_bk_lim", THM_KEY="mihomo_theme", LANG_KEY="mihomo_lang";
|
||||
var initialConfig = __JSON_CONTENT__;
|
||||
var isEditMode = false;
|
||||
var currLang = 'ru';
|
||||
|
||||
const TR = {
|
||||
ru: {
|
||||
title: "Mihomo Studio",
|
||||
save: "💾 Сохранить",
|
||||
restart: "🚀 Рестарт",
|
||||
panel: "🌐 Панель",
|
||||
profiles: "Профили",
|
||||
create: "➕ Создать",
|
||||
delete: "🗑 Удалить",
|
||||
select: "✔",
|
||||
download: "💾",
|
||||
proxy_mgmt: "Управление",
|
||||
add: "➕ Добавить",
|
||||
edit: "✏️ Заменить",
|
||||
rename: "Переименовать",
|
||||
backups: "Бэкапы",
|
||||
clean: "Очистить",
|
||||
keep: "Оставить:",
|
||||
theme_dark: "🌑 Dark",
|
||||
theme_light: "☀️ Light",
|
||||
theme_midnight: "🌃 Midnight",
|
||||
theme_cyber: "👾 Cyber",
|
||||
toast_saved: "✅ Успешно сохранено",
|
||||
toast_cleaned: "🧹 Очищено",
|
||||
toast_deleted: "🗑 Удалено",
|
||||
toast_restored: "♻️ Восстановлено",
|
||||
toast_added: "✅ Добавлено",
|
||||
toast_renamed: "✏️ Прокси переименован",
|
||||
toast_updated: "✏️ Данные прокси обновлены",
|
||||
confirm_switch: "Переключиться на профиль {0}?",
|
||||
confirm_del_prof: "Удалить профиль {0}? Это действие необратимо.",
|
||||
confirm_del_bk: "Удалить бэкап {0}?",
|
||||
confirm_clean: "Оставить только {0} последних бэкапов?",
|
||||
confirm_restore: "Восстановить {0}? Текущий конфиг будет перезаписан.",
|
||||
confirm_del_proxy: "Удалить?",
|
||||
confirm_replace: "Заменить данные прокси '{0}'?",
|
||||
prompt_enter_name: "Введите имя!",
|
||||
error_invalid_name: "Недопустимое имя!",
|
||||
error_exists: "Профиль с таким именем уже существует",
|
||||
error_no_proxy_edit: "Выберите прокси для редактирования",
|
||||
error_empty_wg: "Конфигурація WireGuard не може бути порожньою.",
|
||||
modal_add_proxy: "Добавить прокси",
|
||||
modal_edit_proxy: "Изменить прокси",
|
||||
lbl_vless_link: "Ссылка VLESS:",
|
||||
lbl_proxy_name: "Имя прокси (необязательно):",
|
||||
lbl_wg_conf: "Конфигурация WireGuard:",
|
||||
btn_add: "Добавить",
|
||||
btn_save: "Сохранить",
|
||||
btn_cancel: "Отмена",
|
||||
btn_restore: "Восстановить",
|
||||
btn_close: "Закрыть",
|
||||
btn_update: "Обновить",
|
||||
tab_vless: "VLESS",
|
||||
tab_wg: "WireGuard",
|
||||
lbl_select_edit: "Выберите прокси для изменения:",
|
||||
warn_edit: "⚠️ Данные этого прокси будут полностью заменены новыми!",
|
||||
modal_new_prof: "Новый профиль",
|
||||
lbl_prof_name: "Имя (англ, без пробелов):",
|
||||
lbl_content: "Содержимое:",
|
||||
btn_load_file: "📂 Загрузить файл",
|
||||
ph_paste_yaml: "Вставьте YAML конфиг сюда...",
|
||||
modal_groups: "Добавить в группы:",
|
||||
btn_sel_all: "☑ Выбрать все",
|
||||
btn_sel_none: "☐ Снять все",
|
||||
modal_del_proxy: "Удалить прокси",
|
||||
modal_ren_proxy: "Переименовать прокси",
|
||||
lbl_sel_ren: "Выберите прокси для переименования:",
|
||||
lbl_new_name: "Новое имя:",
|
||||
ph_new_name: "Введите новое имя",
|
||||
btn_rename: "Переименовать",
|
||||
modal_console: "Консоль",
|
||||
modal_view_bk: "Просмотр бэкапа",
|
||||
log_loading: "⏳ Выполнение xkeen -restart...",
|
||||
last_load: "Загружено:",
|
||||
last_saved: "Сохранено:"
|
||||
},
|
||||
uk: {
|
||||
title: "Mihomo Studio",
|
||||
save: "💾 Зберегти",
|
||||
restart: "🚀 Рестарт",
|
||||
panel: "🌐 Панель",
|
||||
profiles: "Профілі",
|
||||
create: "➕ Створити",
|
||||
delete: "🗑 Видалити",
|
||||
select: "✔",
|
||||
download: "💾",
|
||||
proxy_mgmt: "Керування",
|
||||
add: "➕ Додати",
|
||||
edit: "✏️ Замінити",
|
||||
rename: "Перейменувати",
|
||||
backups: "Бекапи",
|
||||
clean: "Очистити",
|
||||
keep: "Залишити:",
|
||||
theme_dark: "🌑 Темна",
|
||||
theme_light: "☀️ Світла",
|
||||
theme_midnight: "🌃 Північ",
|
||||
theme_cyber: "👾 Кібер",
|
||||
toast_saved: "✅ Успішно збережено",
|
||||
toast_cleaned: "🧹 Очищено",
|
||||
toast_deleted: "🗑 Видалено",
|
||||
toast_restored: "♻️ Відновлено",
|
||||
toast_added: "✅ Додано",
|
||||
toast_renamed: "✏️ Проксі перейменовано",
|
||||
toast_updated: "✏️ Дані проксі оновлено",
|
||||
confirm_switch: "Переключитися на профіль {0}?",
|
||||
confirm_del_prof: "Видалити профіль {0}? Ця дія незворотна.",
|
||||
confirm_del_bk: "Видалити бекап {0}?",
|
||||
confirm_clean: "Залишити тільки {0} останніх бекапів?",
|
||||
confirm_restore: "Відновити {0}? Поточний конфіг буде перезаписано.",
|
||||
confirm_del_proxy: "Видалити?",
|
||||
confirm_replace: "Замінити дані проксі '{0}'?",
|
||||
prompt_enter_name: "Введіть ім'я!",
|
||||
error_invalid_name: "Неприпустиме ім'я!",
|
||||
error_exists: "Профіль з таким ім'ям вже існує",
|
||||
error_no_proxy_edit: "Виберіть проксі для редагування",
|
||||
error_empty_wg: "Конфігурація WireGuard не може бути порожньою.",
|
||||
modal_add_proxy: "Додати проксі",
|
||||
modal_edit_proxy: "Змінити проксі",
|
||||
lbl_vless_link: "Посилання VLESS:",
|
||||
lbl_proxy_name: "Ім'я проксі (необов'язково):",
|
||||
lbl_wg_conf: "Конфігурація WireGuard:",
|
||||
btn_add: "Додати",
|
||||
btn_save: "Зберегти",
|
||||
btn_cancel: "Скасувати",
|
||||
btn_restore: "Відновити",
|
||||
btn_close: "Закрити",
|
||||
btn_update: "Оновити",
|
||||
tab_vless: "VLESS",
|
||||
tab_wg: "WireGuard",
|
||||
lbl_select_edit: "Виберіть проксі для зміни:",
|
||||
warn_edit: "⚠️ Дані цього проксі будуть повністю замінені новими!",
|
||||
modal_new_prof: "Новий профіль",
|
||||
lbl_prof_name: "Ім'я (англ, без пробілів):",
|
||||
lbl_content: "Вміст:",
|
||||
btn_load_file: "📂 Завантажити файл",
|
||||
ph_paste_yaml: "Вставте YAML конфіг сюди...",
|
||||
modal_groups: "Додати в групи:",
|
||||
btn_sel_all: "☑ Обрати всі",
|
||||
btn_sel_none: "☐ Зняти всі",
|
||||
modal_del_proxy: "Видалити проксі",
|
||||
modal_ren_proxy: "Перейменувати проксі",
|
||||
lbl_sel_ren: "Виберіть проксі для перейменування:",
|
||||
lbl_new_name: "Нове ім'я:",
|
||||
ph_new_name: "Введіть нове ім'я",
|
||||
btn_rename: "Перейменувати",
|
||||
modal_console: "Консоль",
|
||||
modal_view_bk: "Перегляд бекапу",
|
||||
log_loading: "⏳ Виконання xkeen -restart...",
|
||||
last_load: "Завантажено:",
|
||||
last_saved: "Збережено:"
|
||||
},
|
||||
en: {
|
||||
title: "Mihomo Studio",
|
||||
save: "💾 Save",
|
||||
restart: "🚀 Restart",
|
||||
panel: "🌐 Panel",
|
||||
profiles: "Profiles",
|
||||
create: "➕ Create",
|
||||
delete: "🗑 Delete",
|
||||
select: "✔",
|
||||
download: "💾",
|
||||
proxy_mgmt: "Management",
|
||||
add: "➕ Add",
|
||||
edit: "✏️ Replace",
|
||||
rename: "Rename",
|
||||
backups: "Backups",
|
||||
clean: "Clean",
|
||||
keep: "Keep:",
|
||||
theme_dark: "🌑 Dark",
|
||||
theme_light: "☀️ Light",
|
||||
theme_midnight: "🌃 Midnight",
|
||||
theme_cyber: "👾 Cyber",
|
||||
toast_saved: "✅ Saved successfully",
|
||||
toast_cleaned: "🧹 Cleaned",
|
||||
toast_deleted: "🗑 Deleted",
|
||||
toast_restored: "♻️ Restored",
|
||||
toast_added: "✅ Added",
|
||||
toast_renamed: "✏️ Proxy renamed",
|
||||
toast_updated: "✏️ Proxy data updated",
|
||||
confirm_switch: "Switch to profile {0}?",
|
||||
confirm_del_prof: "Delete profile {0}? This action is irreversible.",
|
||||
confirm_del_bk: "Delete backup {0}?",
|
||||
confirm_clean: "Keep only the last {0} backups?",
|
||||
confirm_restore: "Restore {0}? Current config will be overwritten.",
|
||||
confirm_del_proxy: "Delete?",
|
||||
confirm_replace: "Replace data for proxy '{0}'?",
|
||||
prompt_enter_name: "Enter name!",
|
||||
error_invalid_name: "Invalid name!",
|
||||
error_exists: "Profile with this name already exists",
|
||||
error_no_proxy_edit: "Select a proxy to edit",
|
||||
error_empty_wg: "WireGuard configuration cannot be empty.",
|
||||
modal_add_proxy: "Add Proxy",
|
||||
modal_edit_proxy: "Edit Proxy",
|
||||
lbl_vless_link: "VLESS Link:",
|
||||
lbl_proxy_name: "Proxy Name (optional):",
|
||||
lbl_wg_conf: "WireGuard Config:",
|
||||
btn_add: "Add",
|
||||
btn_save: "Save",
|
||||
btn_cancel: "Cancel",
|
||||
btn_restore: "Restore",
|
||||
btn_close: "Close",
|
||||
btn_update: "Update",
|
||||
tab_vless: "VLESS",
|
||||
tab_wg: "WireGuard",
|
||||
lbl_select_edit: "Select proxy to replace:",
|
||||
warn_edit: "⚠️ This proxy's data will be fully replaced!",
|
||||
modal_new_prof: "New Profile",
|
||||
lbl_prof_name: "Name (English, no spaces):",
|
||||
lbl_content: "Content:",
|
||||
btn_load_file: "📂 Upload File",
|
||||
ph_paste_yaml: "Paste YAML config here...",
|
||||
modal_groups: "Add to groups:",
|
||||
btn_sel_all: "☑ Select All",
|
||||
btn_sel_none: "☐ Select None",
|
||||
modal_del_proxy: "Delete Proxy",
|
||||
modal_ren_proxy: "Rename Proxy",
|
||||
lbl_sel_ren: "Select proxy to rename:",
|
||||
lbl_new_name: "New Name:",
|
||||
ph_new_name: "Enter new name",
|
||||
btn_rename: "Rename",
|
||||
modal_console: "Console",
|
||||
modal_view_bk: "View Backup",
|
||||
log_loading: "⏳ Running xkeen -restart...",
|
||||
last_load: "Loaded:",
|
||||
last_saved: "Saved:"
|
||||
}
|
||||
};
|
||||
|
||||
function t(k, ...args) {
|
||||
let s = TR[currLang][k] || k;
|
||||
args.forEach((a, i) => s = s.replace('{'+i+'}', a));
|
||||
return s;
|
||||
}
|
||||
|
||||
function setLang(l) {
|
||||
currLang = l;
|
||||
localStorage.setItem(LANG_KEY, l);
|
||||
document.getElementById('lang-sel').value = l;
|
||||
|
||||
document.querySelectorAll('[data-i18n]').forEach(e => {
|
||||
let k = e.getAttribute('data-i18n');
|
||||
if(TR[l][k]) e.innerText = TR[l][k];
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-ph]').forEach(e => {
|
||||
let k = e.getAttribute('data-i18n-ph');
|
||||
if(TR[l][k]) e.placeholder = TR[l][k];
|
||||
});
|
||||
|
||||
// Update dynamic parts
|
||||
if(isEditMode) document.getElementById('proxyModalTitle').innerText = TR[l].modal_edit_proxy;
|
||||
else document.getElementById('proxyModalTitle').innerText = TR[l].modal_add_proxy;
|
||||
}
|
||||
|
||||
// Открываем панель через наш локальный прокси (безопасно для PNA/CORS)
|
||||
function openPanel() {
|
||||
|
|
@ -708,7 +924,7 @@ function openPanel() {
|
|||
ed.setValue(initialConfig); ed.clearSelection();
|
||||
|
||||
document.getElementById('vlessLink').addEventListener('input', function() {
|
||||
if(isEditMode) return; // В режиме редактирования имя берется из селекта
|
||||
if(isEditMode) return;
|
||||
var link = this.value;
|
||||
if (link.startsWith("vless://") && link.includes("#")) {
|
||||
var name = link.split('#')[1];
|
||||
|
|
@ -728,11 +944,11 @@ document.getElementById('wgConfig').addEventListener('input', function() {
|
|||
});
|
||||
|
||||
function closeM(i){document.getElementById(i).style.display='none'}
|
||||
function showToast(msg){ var t=document.getElementById('toast'); t.innerText=msg||'✅ Выполнено'; t.style.display='block'; setTimeout(()=>{t.style.display='none'}, 2000); }
|
||||
function showToast(msg){ var tBox=document.getElementById('toast'); tBox.innerText=msg||t('toast_saved'); tBox.style.display='block'; setTimeout(()=>{tBox.style.display='none'}, 2000); }
|
||||
|
||||
function switchProf() {
|
||||
var p = document.getElementById('prof-sel').value;
|
||||
if(!confirm("Переключиться на профиль " + p + "?")) return;
|
||||
if(!confirm(t('confirm_switch', p))) return;
|
||||
var params = new URLSearchParams(); params.append('act', 'switch_prof'); params.append('name', p);
|
||||
fetch('/',{method:'POST',body:params}).then(r=>r.json()).then(d=>{
|
||||
if(d.error) alert(d.error);
|
||||
|
|
@ -752,22 +968,22 @@ function loadProfFile(input) {
|
|||
function saveNewProf() {
|
||||
var n = document.getElementById('np-name').value.trim();
|
||||
var c = document.getElementById('np-content').value;
|
||||
if(!n) return alert("Введите имя!");
|
||||
if(!n.match(/^[a-zA-Z0-9_-]+$/)) return alert("Недопустимое имя!");
|
||||
if(!n) return alert(t('prompt_enter_name'));
|
||||
if(!n.match(/^[a-zA-Z0-9_-]+$/)) return alert(t('error_invalid_name'));
|
||||
var params = new URLSearchParams(); params.append('act', 'add_prof'); params.append('name', n); params.append('content', c);
|
||||
fetch('/',{method:'POST',body:params}).then(r=>r.json()).then(d=>{
|
||||
if(d.error) alert(d.error);
|
||||
else { showToast("✅ Профиль создан"); setTimeout(()=>{window.location.reload()}, 500); }
|
||||
else { showToast(t('toast_saved')); setTimeout(()=>{window.location.reload()}, 500); }
|
||||
});
|
||||
}
|
||||
function delProf() {
|
||||
var p = document.getElementById('prof-sel').value;
|
||||
if(!p) return;
|
||||
if(!confirm("Удалить профиль " + p + "? Это действие необратимо.")) return;
|
||||
if(!confirm(t('confirm_del_prof', p))) return;
|
||||
var params = new URLSearchParams(); params.append('act', 'del_prof'); params.append('name', p);
|
||||
fetch('/',{method:'POST',body:params}).then(r=>r.json()).then(d=>{
|
||||
if(d.error) alert(d.error);
|
||||
else { showToast("🗑 Удалено"); setTimeout(()=>{window.location.reload()}, 500); }
|
||||
else { showToast(t('toast_deleted')); setTimeout(()=>{window.location.reload()}, 500); }
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -790,7 +1006,7 @@ function downloadProf() {
|
|||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
showToast('📄 Загрузка началась');
|
||||
showToast('💾');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -829,6 +1045,9 @@ function setTheme(t) {
|
|||
var savedTheme = localStorage.getItem(THM_KEY) || 'dark';
|
||||
setTheme(savedTheme);
|
||||
|
||||
var savedLang = localStorage.getItem(LANG_KEY) || 'ru';
|
||||
setLang(savedLang);
|
||||
|
||||
var bkInp = document.getElementById('bk-lim');
|
||||
if(localStorage.getItem(LIM_KEY)) bkInp.value = localStorage.getItem(LIM_KEY);
|
||||
bkInp.addEventListener('change', function(){ localStorage.setItem(LIM_KEY, this.value); });
|
||||
|
|
@ -838,12 +1057,12 @@ function save(mode){
|
|||
var p=new URLSearchParams(); p.append('act', mode); p.append('content', c);
|
||||
if(mode==='restart') {
|
||||
document.getElementById('m-con').style.display='flex';
|
||||
document.getElementById('cons').innerHTML='<div style="padding:20px;text-align:center">⏳ Выполнение xkeen -restart...</div>';
|
||||
document.getElementById('cons').innerHTML='<div style="padding:20px;text-align:center">' + t('log_loading') + '</div>';
|
||||
}
|
||||
fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{
|
||||
if(mode==='save'){
|
||||
showToast("✅ Сохранено");
|
||||
document.getElementById('last-load').innerText = "Saved: " + d.time;
|
||||
showToast(t('toast_saved'));
|
||||
document.getElementById('last-load').innerText = t('last_saved') + " " + d.time;
|
||||
if(d.backups) document.getElementById('bk-list').innerHTML = d.backups;
|
||||
} else {
|
||||
var consoleDiv = document.getElementById('cons');
|
||||
|
|
@ -855,19 +1074,19 @@ function save(mode){
|
|||
|
||||
function cleanBackups(){
|
||||
var lim = document.getElementById('bk-lim').value;
|
||||
if(!confirm('Оставить только ' + lim + ' последних бэкапов?')) return;
|
||||
if(!confirm(t('confirm_clean', lim))) return;
|
||||
var p=new URLSearchParams(); p.append('act', 'clean_backups'); p.append('limit', lim);
|
||||
fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{
|
||||
showToast("🧹 Очищено");
|
||||
showToast(t('toast_cleaned'));
|
||||
if(d.backups) document.getElementById('bk-list').innerHTML = d.backups;
|
||||
});
|
||||
}
|
||||
|
||||
function delBackup(fname){
|
||||
if(!confirm('Удалить бэкап ' + fname + '?')) return;
|
||||
if(!confirm(t('confirm_del_bk', fname))) return;
|
||||
var p=new URLSearchParams(); p.append('act', 'del_backup'); p.append('f', fname);
|
||||
fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{
|
||||
showToast("🗑 Удалено");
|
||||
showToast(t('toast_deleted'));
|
||||
if(d.backups) document.getElementById('bk-list').innerHTML = d.backups;
|
||||
});
|
||||
}
|
||||
|
|
@ -888,7 +1107,7 @@ function viewBackup(fname) {
|
|||
}
|
||||
|
||||
function restoreBackup(fname){
|
||||
if(!confirm('Восстановить ' + fname + '? Текущий конфиг будет перезаписан.')) return;
|
||||
if(!confirm(t('confirm_restore', fname))) return;
|
||||
var p=new URLSearchParams(); p.append('act', 'rest'); p.append('f', fname);
|
||||
fetch('/',{method:'POST',body:p}).then(r=>r.text()).then(()=>{
|
||||
window.location.reload();
|
||||
|
|
@ -911,7 +1130,7 @@ function getProxiesList() {
|
|||
|
||||
function openAddProxyModal() {
|
||||
isEditMode = false;
|
||||
document.getElementById('proxyModalTitle').innerText = "Добавить прокси";
|
||||
document.getElementById('proxyModalTitle').innerText = t('modal_add_proxy');
|
||||
document.getElementById('edit-proxy-container').style.display = 'none';
|
||||
document.getElementById('vless-name-block').style.display = 'block';
|
||||
document.getElementById('wg-name-block').style.display = 'block';
|
||||
|
|
@ -927,7 +1146,7 @@ function openAddProxyModal() {
|
|||
|
||||
function openEditProxyModal() {
|
||||
isEditMode = true;
|
||||
document.getElementById('proxyModalTitle').innerText = "Изменить прокси";
|
||||
document.getElementById('proxyModalTitle').innerText = t('modal_edit_proxy');
|
||||
document.getElementById('edit-proxy-container').style.display = 'block';
|
||||
document.getElementById('vless-name-block').style.display = 'none';
|
||||
document.getElementById('wg-name-block').style.display = 'none';
|
||||
|
|
@ -938,7 +1157,7 @@ function openEditProxyModal() {
|
|||
sel.innerHTML = '';
|
||||
if(prs.length === 0) {
|
||||
var o = document.createElement('option');
|
||||
o.text = "Нет прокси для редактирования";
|
||||
o.text = "---";
|
||||
sel.add(o);
|
||||
sel.disabled = true;
|
||||
} else {
|
||||
|
|
@ -990,12 +1209,12 @@ function addWireguard() {
|
|||
|
||||
if(isEditMode) {
|
||||
name = document.getElementById('edit-proxy-sel').value;
|
||||
if(!name || document.getElementById('edit-proxy-sel').disabled) return alert("Выберите прокси для редактирования");
|
||||
if(!name || document.getElementById('edit-proxy-sel').disabled) return alert(t('error_no_proxy_edit'));
|
||||
} else {
|
||||
name = document.getElementById('wgProxyName').value.trim();
|
||||
}
|
||||
|
||||
if (!conf) return alert("Конфигурация WireGuard не может быть пустой.");
|
||||
if (!conf) return alert(t('error_empty_wg'));
|
||||
|
||||
var p = new URLSearchParams();
|
||||
p.append('act', 'add_wireguard');
|
||||
|
|
@ -1025,7 +1244,7 @@ function parseVless(){
|
|||
|
||||
if(isEditMode) {
|
||||
name = document.getElementById('edit-proxy-sel').value;
|
||||
if(!name || document.getElementById('edit-proxy-sel').disabled) return alert("Выберите прокси для редактирования");
|
||||
if(!name || document.getElementById('edit-proxy-sel').disabled) return alert(t('error_no_proxy_edit'));
|
||||
} else {
|
||||
name = document.getElementById('vlessProxyName').value.trim();
|
||||
}
|
||||
|
|
@ -1055,7 +1274,7 @@ function parseVless(){
|
|||
}
|
||||
|
||||
function replaceProxyData(targetName, newYaml) {
|
||||
if(!confirm("Заменить данные прокси '" + targetName + "'?")) return;
|
||||
if(!confirm(t('confirm_replace', targetName))) return;
|
||||
var content = ed.getValue();
|
||||
var p = new URLSearchParams();
|
||||
p.append('act', 'replace_proxy');
|
||||
|
|
@ -1072,7 +1291,7 @@ function replaceProxyData(targetName, newYaml) {
|
|||
ed.setValue(d.new_content);
|
||||
ed.clearSelection();
|
||||
closeM('addProxyModal');
|
||||
showToast("✏️ Данные прокси обновлены");
|
||||
showToast(t('toast_updated'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1094,7 +1313,7 @@ function applyVless(){
|
|||
document.querySelectorAll('#g-cnt input:checked').forEach(c=>sels.push(c.value));
|
||||
localStorage.setItem(GRP_KEY, JSON.stringify(sels));
|
||||
var p=new URLSearchParams(); p.append('act','apply_insert'); p.append('content',txt); p.append('proxy_name',pData.name); p.append('proxy_yaml',pData.yaml); p.append('targets',JSON.stringify(sels));
|
||||
fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{if(d.error)alert(d.error);else{ed.setValue(d.new_content);ed.clearSelection();showToast("✅ Добавлено")}});
|
||||
fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{if(d.error)alert(d.error);else{ed.setValue(d.new_content);ed.clearSelection();showToast(t('toast_added'))}});
|
||||
}
|
||||
function showDel(){
|
||||
var prs = getProxiesList();
|
||||
|
|
@ -1103,7 +1322,7 @@ function showDel(){
|
|||
document.getElementById('m-del').style.display='flex';
|
||||
}
|
||||
function doDel(){
|
||||
var nm=document.getElementById('sel-del').value;if(!nm)return;if(!confirm('Удалить?'))return;closeM('m-del');
|
||||
var nm=document.getElementById('sel-del').value;if(!nm)return;if(!confirm(t('confirm_del_proxy')))return;closeM('m-del');
|
||||
var ls=ed.getValue().split(/\\r?\\n/); var nls=[], inP=false, delB=false, bInd=-1;
|
||||
for(var l of ls){
|
||||
if(l.match(/^proxies:/)){inP=true;nls.push(l);continue} if(inP && l.match(/^[a-zA-Z]/) && !l.match(/^proxies:/)){inP=false;delB=false}
|
||||
|
|
@ -1179,7 +1398,7 @@ function doRename() {
|
|||
ed.setValue(d.new_content);
|
||||
ed.clearSelection();
|
||||
closeM('m-ren');
|
||||
showToast("✏️ Прокси переименован!");
|
||||
showToast(t('toast_renamed'));
|
||||
}
|
||||
})
|
||||
.catch(e => alert("Сетевая ошибка: " + e));
|
||||
|
|
|
|||
Loading…
Reference in New Issue