mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-10-21 00:04:10 +03:00
UI refactoring
This commit is contained in:
@@ -24,13 +24,13 @@ from utils.tray_common import (
|
|||||||
from ui.ctk_tray_ui import (
|
from ui.ctk_tray_ui import (
|
||||||
install_tray_config_buttons, install_tray_config_form,
|
install_tray_config_buttons, install_tray_config_form,
|
||||||
populate_first_run_window, tray_settings_scroll_and_footer,
|
populate_first_run_window, tray_settings_scroll_and_footer,
|
||||||
validate_config_form,
|
|
||||||
)
|
)
|
||||||
from ui.ctk_theme import (
|
from ui.ctk_theme import (
|
||||||
CONFIG_DIALOG_FRAME_PAD, CONFIG_DIALOG_SIZE, FIRST_RUN_SIZE,
|
CONFIG_DIALOG_FRAME_PAD, CONFIG_DIALOG_SIZE, FIRST_RUN_SIZE,
|
||||||
create_ctk_toplevel, ctk_theme_for_platform, main_content_frame,
|
create_ctk_toplevel, ctk_theme_for_platform, main_content_frame,
|
||||||
)
|
)
|
||||||
from ui.i18n import set_language, t
|
from ui.i18n import t
|
||||||
|
from ui.settings_dialog import SettingsDialog
|
||||||
|
|
||||||
_tray_icon: Optional[object] = None
|
_tray_icon: Optional[object] = None
|
||||||
_config: dict = {}
|
_config: dict = {}
|
||||||
@@ -158,68 +158,24 @@ def _edit_config_dialog() -> None:
|
|||||||
if _tray_icon is not None:
|
if _tray_icon is not None:
|
||||||
_tray_icon.menu = _build_menu()
|
_tray_icon.menu = _build_menu()
|
||||||
|
|
||||||
_original_language = _config.get("language", DEFAULT_CONFIG["language"])
|
|
||||||
|
|
||||||
widgets = install_tray_config_form(
|
widgets = install_tray_config_form(
|
||||||
ctk, scroll, theme, cfg, DEFAULT_CONFIG,
|
ctk, scroll, theme, cfg, DEFAULT_CONFIG,
|
||||||
show_autostart=False,
|
show_autostart=False,
|
||||||
on_language_change=_refresh_tray_menu,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_original_appearance = ctk.get_appearance_mode()
|
|
||||||
|
|
||||||
def _restore_ui_locale() -> None:
|
|
||||||
set_language(_original_language)
|
|
||||||
_refresh_tray_menu()
|
|
||||||
|
|
||||||
def _finish() -> None:
|
def _finish() -> None:
|
||||||
root.destroy()
|
root.destroy()
|
||||||
done.set()
|
done.set()
|
||||||
|
|
||||||
def _cancel() -> None:
|
dialog = SettingsDialog(
|
||||||
ctk.set_appearance_mode(_original_appearance)
|
ctk=ctk, root=root, widgets=widgets, config=_config,
|
||||||
_restore_ui_locale()
|
defaults=DEFAULT_CONFIG, persist=save_config,
|
||||||
_finish()
|
refresh_menu=_refresh_tray_menu, finish=_finish,
|
||||||
|
restart=lambda config: restart_proxy(config, _show_error),
|
||||||
def on_save() -> None:
|
|
||||||
from tkinter import messagebox
|
|
||||||
merged = validate_config_form(widgets, DEFAULT_CONFIG, include_autostart=False)
|
|
||||||
if isinstance(merged, str):
|
|
||||||
messagebox.showerror(t("app.error_title"), merged, parent=root)
|
|
||||||
return
|
|
||||||
|
|
||||||
merged["force_test_dc"] = _config.get("force_test_dc", DEFAULT_CONFIG["force_test_dc"])
|
|
||||||
|
|
||||||
_ui_only_keys = {"appearance", "check_updates", "language"}
|
|
||||||
config_changed = any(merged.get(k) != _config.get(k) for k in merged)
|
|
||||||
proxy_changed = any(merged.get(k) != _config.get(k) for k in merged if k not in _ui_only_keys)
|
|
||||||
|
|
||||||
if not config_changed:
|
|
||||||
_restore_ui_locale()
|
|
||||||
_finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
save_config(merged)
|
|
||||||
_config.update(merged)
|
|
||||||
set_language(merged.get("language", DEFAULT_CONFIG["language"]))
|
|
||||||
log.info("Config saved: %s", merged)
|
|
||||||
_tray_icon.menu = _build_menu()
|
|
||||||
|
|
||||||
if not proxy_changed:
|
|
||||||
_finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
do_restart = messagebox.askyesno(
|
|
||||||
t("dialog.restart_title"),
|
|
||||||
t("dialog.restart_body"),
|
|
||||||
parent=root,
|
|
||||||
)
|
)
|
||||||
_finish()
|
|
||||||
if do_restart:
|
|
||||||
threading.Thread(target=lambda: restart_proxy(_config, _show_error), daemon=True).start()
|
|
||||||
|
|
||||||
root.protocol("WM_DELETE_WINDOW", _cancel)
|
root.protocol("WM_DELETE_WINDOW", dialog.cancel)
|
||||||
install_tray_config_buttons(ctk, footer, theme, on_save=on_save, on_cancel=_cancel)
|
install_tray_config_buttons(ctk, footer, theme, on_save=dialog.save, on_cancel=dialog.cancel)
|
||||||
|
|
||||||
ctk_run_dialog(_build)
|
ctk_run_dialog(_build)
|
||||||
|
|
||||||
|
|||||||
@@ -87,9 +87,9 @@ from ui.ctk_tray_ui import (
|
|||||||
install_tray_config_form,
|
install_tray_config_form,
|
||||||
populate_first_run_window,
|
populate_first_run_window,
|
||||||
tray_settings_scroll_and_footer,
|
tray_settings_scroll_and_footer,
|
||||||
validate_config_form,
|
|
||||||
)
|
)
|
||||||
from ui.i18n import set_language, t
|
from ui.i18n import t
|
||||||
|
from ui.settings_dialog import SettingsDialog
|
||||||
from utils.tray_common import (
|
from utils.tray_common import (
|
||||||
APP_DIR,
|
APP_DIR,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
@@ -362,7 +362,6 @@ def _edit_config_dialog() -> None:
|
|||||||
pady=frame_pad_y,
|
pady=frame_pad_y,
|
||||||
)
|
)
|
||||||
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
|
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
|
||||||
original_language = _config.get("language", DEFAULT_CONFIG["language"])
|
|
||||||
log.info("Building settings form")
|
log.info("Building settings form")
|
||||||
widgets = install_tray_config_form(
|
widgets = install_tray_config_form(
|
||||||
ctk,
|
ctk,
|
||||||
@@ -371,14 +370,8 @@ def _edit_config_dialog() -> None:
|
|||||||
cfg,
|
cfg,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
show_autostart=False,
|
show_autostart=False,
|
||||||
on_language_change=_refresh_tray_menu,
|
|
||||||
)
|
)
|
||||||
log.info("Settings form built")
|
log.info("Settings form built")
|
||||||
original_appearance = ctk.get_appearance_mode()
|
|
||||||
|
|
||||||
def restore_ui_locale() -> None:
|
|
||||||
set_language(original_language)
|
|
||||||
_refresh_tray_menu()
|
|
||||||
|
|
||||||
def finish() -> None:
|
def finish() -> None:
|
||||||
global _settings_window
|
global _settings_window
|
||||||
@@ -386,70 +379,20 @@ def _edit_config_dialog() -> None:
|
|||||||
_settings_window = None
|
_settings_window = None
|
||||||
_hide_ctk_root()
|
_hide_ctk_root()
|
||||||
|
|
||||||
def cancel() -> None:
|
dialog = SettingsDialog(
|
||||||
ctk.set_appearance_mode(original_appearance)
|
ctk=ctk, root=root, widgets=widgets, config=_config,
|
||||||
restore_ui_locale()
|
defaults=DEFAULT_CONFIG, persist=save_config,
|
||||||
finish()
|
refresh_menu=_refresh_tray_menu, finish=finish,
|
||||||
|
restart=lambda config: restart_proxy(config, _show_error),
|
||||||
def save() -> None:
|
|
||||||
from tkinter import messagebox
|
|
||||||
|
|
||||||
merged = validate_config_form(
|
|
||||||
widgets,
|
|
||||||
DEFAULT_CONFIG,
|
|
||||||
include_autostart=False,
|
|
||||||
)
|
|
||||||
if isinstance(merged, str):
|
|
||||||
messagebox.showerror(t("app.error_title"), merged, parent=root)
|
|
||||||
return
|
|
||||||
|
|
||||||
merged["force_test_dc"] = _config.get(
|
|
||||||
"force_test_dc",
|
|
||||||
DEFAULT_CONFIG["force_test_dc"],
|
|
||||||
)
|
|
||||||
ui_only_keys = {"appearance", "check_updates", "language"}
|
|
||||||
config_changed = any(merged.get(key) != _config.get(key) for key in merged)
|
|
||||||
proxy_changed = any(
|
|
||||||
merged.get(key) != _config.get(key)
|
|
||||||
for key in merged
|
|
||||||
if key not in ui_only_keys
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not config_changed:
|
root.protocol("WM_DELETE_WINDOW", dialog.cancel)
|
||||||
restore_ui_locale()
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
save_config(merged)
|
|
||||||
_config.update(merged)
|
|
||||||
set_language(merged.get("language", DEFAULT_CONFIG["language"]))
|
|
||||||
log.info("Config saved: %s", merged)
|
|
||||||
_refresh_tray_menu()
|
|
||||||
|
|
||||||
if not proxy_changed:
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
do_restart = messagebox.askyesno(
|
|
||||||
t("dialog.restart_title"),
|
|
||||||
t("dialog.restart_body"),
|
|
||||||
parent=root,
|
|
||||||
)
|
|
||||||
finish()
|
|
||||||
if do_restart:
|
|
||||||
threading.Thread(
|
|
||||||
target=lambda: restart_proxy(_config, _show_error),
|
|
||||||
daemon=True,
|
|
||||||
name="proxy-restart",
|
|
||||||
).start()
|
|
||||||
|
|
||||||
root.protocol("WM_DELETE_WINDOW", cancel)
|
|
||||||
install_tray_config_buttons(
|
install_tray_config_buttons(
|
||||||
ctk,
|
ctk,
|
||||||
footer,
|
footer,
|
||||||
theme,
|
theme,
|
||||||
on_save=save,
|
on_save=dialog.save,
|
||||||
on_cancel=cancel,
|
on_cancel=dialog.cancel,
|
||||||
)
|
)
|
||||||
_activate_app()
|
_activate_app()
|
||||||
log.info("Settings window ready")
|
log.info("Settings window ready")
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
|
||||||
|
log = logging.getLogger("tg-ws-tray")
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundTask:
|
||||||
|
def __init__(self, owner):
|
||||||
|
self.owner = owner
|
||||||
|
self.running = False
|
||||||
|
self.closed = False
|
||||||
|
self.pending = None
|
||||||
|
owner.bind("<Destroy>", self._on_destroy, add="+")
|
||||||
|
|
||||||
|
def _on_destroy(self, event):
|
||||||
|
if event.widget is self.owner:
|
||||||
|
self.closed = True
|
||||||
|
if self.pending is not None:
|
||||||
|
self.owner.after_cancel(self.pending)
|
||||||
|
self.pending = None
|
||||||
|
|
||||||
|
def start(self, work, complete, finish):
|
||||||
|
if self.running or self.closed:
|
||||||
|
return
|
||||||
|
self.running = True
|
||||||
|
results = queue.Queue()
|
||||||
|
|
||||||
|
def run():
|
||||||
|
try:
|
||||||
|
results.put((True, work()))
|
||||||
|
except Exception:
|
||||||
|
log.exception("Connectivity test failed")
|
||||||
|
results.put((False, None))
|
||||||
|
|
||||||
|
def poll():
|
||||||
|
self.pending = None
|
||||||
|
if self.closed:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
success, result = results.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
self.pending = self.owner.after(50, poll)
|
||||||
|
return
|
||||||
|
self.running = False
|
||||||
|
try:
|
||||||
|
if success:
|
||||||
|
complete(result)
|
||||||
|
finally:
|
||||||
|
if not self.closed:
|
||||||
|
finish()
|
||||||
|
|
||||||
|
threading.Thread(target=run, daemon=True, name="connectivity-test").start()
|
||||||
|
self.pending = self.owner.after(50, poll)
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import socket as _socket
|
||||||
|
from contextlib import nullcontext
|
||||||
|
from tkinter import messagebox
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from proxy.utils import create_ssl_context
|
||||||
|
from ui.i18n import t
|
||||||
|
|
||||||
|
_CFPROXY_TEST_DCS = [1, 2, 3, 4, 5, 203]
|
||||||
|
_CFWORKER_TEST_DST = {
|
||||||
|
1: '149.154.175.50',
|
||||||
|
2: '149.154.167.51',
|
||||||
|
3: '149.154.175.100',
|
||||||
|
4: '149.154.167.91',
|
||||||
|
5: '149.154.171.5',
|
||||||
|
203: '91.105.192.100',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _run_connectivity_test(cases: list, *, secure: bool = True) -> dict:
|
||||||
|
ctx = create_ssl_context() if secure else None
|
||||||
|
port = 443 if secure else 80
|
||||||
|
results = {}
|
||||||
|
for dc, connect_host, sni_host, req_host, path in cases:
|
||||||
|
try:
|
||||||
|
with _socket.create_connection((connect_host, port), timeout=5) as raw:
|
||||||
|
connection = (ctx.wrap_socket(raw, server_hostname=sni_host)
|
||||||
|
if secure else nullcontext(raw))
|
||||||
|
with connection as ssock:
|
||||||
|
ws_key = base64.b64encode(os.urandom(16)).decode()
|
||||||
|
req = (
|
||||||
|
f"GET {path} HTTP/1.1\r\n"
|
||||||
|
f"Host: {req_host}\r\n"
|
||||||
|
f"Upgrade: websocket\r\n"
|
||||||
|
f"Connection: Upgrade\r\n"
|
||||||
|
f"Sec-WebSocket-Key: {ws_key}\r\n"
|
||||||
|
f"Sec-WebSocket-Version: 13\r\n"
|
||||||
|
f"Sec-WebSocket-Protocol: binary\r\n"
|
||||||
|
f"\r\n"
|
||||||
|
).encode()
|
||||||
|
ssock.sendall(req)
|
||||||
|
ssock.settimeout(5)
|
||||||
|
buf = b""
|
||||||
|
while b"\r\n\r\n" not in buf:
|
||||||
|
chunk = ssock.recv(512)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
buf += chunk
|
||||||
|
first = buf.decode("utf-8", errors="replace").split("\r\n")[0]
|
||||||
|
if "101" in first:
|
||||||
|
results[dc] = True
|
||||||
|
else:
|
||||||
|
results[dc] = first or t("connectivity.no_response")
|
||||||
|
ssock.close()
|
||||||
|
raw.close()
|
||||||
|
except _socket.timeout:
|
||||||
|
results[dc] = t("connectivity.timeout")
|
||||||
|
except OSError as exc:
|
||||||
|
msg = str(exc)
|
||||||
|
results[dc] = msg[:60] if len(msg) > 60 else msg
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _run_cfproxy_connectivity_test(domain: str, *, secure: bool = True) -> dict:
|
||||||
|
cases = []
|
||||||
|
for dc in _CFPROXY_TEST_DCS:
|
||||||
|
host = f"kws{dc}.{domain}"
|
||||||
|
cases.append((dc, host, host, host, "/apiws"))
|
||||||
|
return _run_connectivity_test(cases, secure=secure)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_cfworker_connectivity_test(domain: str, *, secure: bool = True) -> dict:
|
||||||
|
cases = []
|
||||||
|
for dc in _CFPROXY_TEST_DCS:
|
||||||
|
dst = _CFWORKER_TEST_DST[dc]
|
||||||
|
path = f"/apiws?dst={dst}&dc={dc}&media=0"
|
||||||
|
cases.append((dc, domain, domain, domain, path))
|
||||||
|
return _run_connectivity_test(cases, secure=secure)
|
||||||
|
|
||||||
|
|
||||||
|
def run_cfproxy_multi_test(domains: list, *, secure: bool = True) -> dict:
|
||||||
|
return {domain: _run_cfproxy_connectivity_test(domain, secure=secure) for domain in domains}
|
||||||
|
|
||||||
|
|
||||||
|
def run_cfworker_multi_test(domains: list, *, secure: bool = True) -> dict:
|
||||||
|
return {domain: _run_cfworker_connectivity_test(domain, secure=secure) for domain in domains}
|
||||||
|
|
||||||
|
|
||||||
|
def run_cfproxy_auto_test(domains: list, *, secure: bool = True) -> tuple:
|
||||||
|
merged: dict = {}
|
||||||
|
best_domain = None
|
||||||
|
for domain in reversed(domains):
|
||||||
|
res = _run_cfproxy_connectivity_test(domain, secure=secure)
|
||||||
|
if all(v is True for v in res.values()):
|
||||||
|
return domain, res
|
||||||
|
for dc, v in res.items():
|
||||||
|
if v is True:
|
||||||
|
merged[dc] = True
|
||||||
|
best_domain = domain
|
||||||
|
elif dc not in merged:
|
||||||
|
merged[dc] = v
|
||||||
|
return best_domain, merged
|
||||||
|
|
||||||
|
|
||||||
|
def show_connectivity_results(title_base: str, results: dict,
|
||||||
|
domain: str = '', label_prefix: str = 'DC',
|
||||||
|
auto_mode: bool = False,
|
||||||
|
unavailable_message: str = '', *, parent: Any) -> None:
|
||||||
|
ok = [dc for dc, v in results.items() if v is True]
|
||||||
|
total = len(_CFPROXY_TEST_DCS)
|
||||||
|
if auto_mode:
|
||||||
|
if domain:
|
||||||
|
title = t("connectivity.available", title=title_base)
|
||||||
|
msg = t("connectivity.auto_ok", title=title_base, ok=len(ok), total=total)
|
||||||
|
else:
|
||||||
|
title = t("connectivity.unavailable", title=title_base)
|
||||||
|
msg = unavailable_message
|
||||||
|
else:
|
||||||
|
fail = [(dc, v) for dc, v in results.items() if v is not True]
|
||||||
|
if len(ok) == total:
|
||||||
|
title = t("connectivity.all_ok", title=title_base)
|
||||||
|
msg = t("connectivity.all_ok_domain", total=total, domain=domain)
|
||||||
|
elif not ok:
|
||||||
|
title = t("connectivity.unavailable", title=title_base)
|
||||||
|
errors = "\n".join(
|
||||||
|
t("connectivity.error_line", prefix=label_prefix, dc=dc, error=v)
|
||||||
|
for dc, v in fail
|
||||||
|
)
|
||||||
|
msg = t("connectivity.none_ok", domain=domain, errors=errors)
|
||||||
|
else:
|
||||||
|
title = t("connectivity.partial", title=title_base)
|
||||||
|
ok_list = ", ".join(f"{label_prefix}{dc}" for dc in ok)
|
||||||
|
fail_list = "\n".join(
|
||||||
|
t("connectivity.error_line", prefix=label_prefix, dc=dc, error=v)
|
||||||
|
for dc, v in fail
|
||||||
|
)
|
||||||
|
msg = t("connectivity.partial_detail", domain=domain, ok_list=ok_list, fail_list=fail_list)
|
||||||
|
|
||||||
|
messagebox.showinfo(title, msg, parent=parent)
|
||||||
|
|
||||||
|
|
||||||
|
def show_multi_connectivity_results(title_base: str, per_domain: dict,
|
||||||
|
label_prefix: str = 'DC', *, parent: Any) -> None:
|
||||||
|
total = len(_CFPROXY_TEST_DCS)
|
||||||
|
all_ok = True
|
||||||
|
any_ok = False
|
||||||
|
blocks = []
|
||||||
|
for domain, results in per_domain.items():
|
||||||
|
ok = [dc for dc, v in results.items() if v is True]
|
||||||
|
fail = [(dc, v) for dc, v in results.items() if v is not True]
|
||||||
|
if len(ok) == total:
|
||||||
|
any_ok = True
|
||||||
|
blocks.append(t("connectivity.multi_all_ok", domain=domain, total=total))
|
||||||
|
elif not ok:
|
||||||
|
all_ok = False
|
||||||
|
blocks.append(t("connectivity.multi_fail", domain=domain))
|
||||||
|
else:
|
||||||
|
all_ok = False
|
||||||
|
any_ok = True
|
||||||
|
ok_list = ", ".join(f"{label_prefix}{dc}" for dc in ok)
|
||||||
|
fail_list = ", ".join(f"{label_prefix}{dc}" for dc, _ in fail)
|
||||||
|
blocks.append(
|
||||||
|
t("connectivity.multi_partial", domain=domain, ok_list=ok_list, fail_list=fail_list)
|
||||||
|
)
|
||||||
|
|
||||||
|
if all_ok:
|
||||||
|
title = t("connectivity.all_ok", title=title_base)
|
||||||
|
elif any_ok:
|
||||||
|
title = t("connectivity.partial", title=title_base)
|
||||||
|
else:
|
||||||
|
title = t("connectivity.unavailable", title=title_base)
|
||||||
|
msg = "\n\n".join(blocks)
|
||||||
|
|
||||||
|
messagebox.showinfo(title, msg, parent=parent)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ui.ctk_theme import CtkTheme
|
||||||
|
from ui.ctk_tooltip import attach_tooltip_to_widgets
|
||||||
|
|
||||||
|
|
||||||
|
def create_entry(ctk, parent, theme, *, var=None, width=0, height=36, radius=10, **kw):
|
||||||
|
opts = {
|
||||||
|
"font": (theme.ui_font_family, 13), "corner_radius": radius,
|
||||||
|
"fg_color": theme.bg, "border_color": theme.field_border,
|
||||||
|
"border_width": 1, "text_color": theme.text_primary,
|
||||||
|
}
|
||||||
|
if var is not None:
|
||||||
|
opts["textvariable"] = var
|
||||||
|
if width:
|
||||||
|
opts["width"] = width
|
||||||
|
opts["height"] = height
|
||||||
|
opts.update(kw)
|
||||||
|
return ctk.CTkEntry(parent, **opts)
|
||||||
|
|
||||||
|
|
||||||
|
def create_checkbox(ctk, parent, theme, text, variable):
|
||||||
|
return ctk.CTkCheckBox(
|
||||||
|
parent, text=text, variable=variable,
|
||||||
|
font=(theme.ui_font_family, 13), text_color=theme.text_primary,
|
||||||
|
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
||||||
|
corner_radius=6, border_width=2, border_color=theme.field_border,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_label(ctk, parent, theme, text, *, size=12, bold=False, secondary=True, **kw):
|
||||||
|
weight = "bold" if bold else "normal"
|
||||||
|
return ctk.CTkLabel(
|
||||||
|
parent, text=text,
|
||||||
|
font=(theme.ui_font_family, size, weight),
|
||||||
|
text_color=theme.text_secondary if secondary else theme.text_primary,
|
||||||
|
anchor="w", **kw,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_labeled_entry(ctk, parent, theme, label_text, value, *, tip="", width=0, pack_fill=False):
|
||||||
|
col = ctk.CTkFrame(parent, fg_color="transparent")
|
||||||
|
lbl = create_label(ctk, col, theme, label_text)
|
||||||
|
lbl.pack(anchor="w", pady=(0, 2))
|
||||||
|
var = ctk.StringVar(master=parent, value=str(value))
|
||||||
|
ent = create_entry(ctk, col, theme, var=var, width=width)
|
||||||
|
if pack_fill:
|
||||||
|
ent.pack(fill="x")
|
||||||
|
else:
|
||||||
|
ent.pack(anchor="w")
|
||||||
|
if tip:
|
||||||
|
attach_tooltip_to_widgets([lbl, ent, col], tip)
|
||||||
|
return col, var
|
||||||
|
|
||||||
|
|
||||||
|
def create_config_section(
|
||||||
|
ctk: Any,
|
||||||
|
parent: Any,
|
||||||
|
theme: CtkTheme,
|
||||||
|
title: str,
|
||||||
|
*,
|
||||||
|
bottom_spacer: int = 6,
|
||||||
|
) -> Any:
|
||||||
|
wrap = ctk.CTkFrame(parent, fg_color="transparent")
|
||||||
|
wrap.pack(fill="x", pady=(0, bottom_spacer))
|
||||||
|
create_label(ctk, wrap, theme, title, secondary=False, bold=True).pack(anchor="w", pady=(0, 2))
|
||||||
|
card = ctk.CTkFrame(
|
||||||
|
wrap, fg_color=theme.field_bg, corner_radius=10,
|
||||||
|
border_width=1, border_color=theme.field_border,
|
||||||
|
)
|
||||||
|
card.pack(fill="x")
|
||||||
|
inner = ctk.CTkFrame(card, fg_color="transparent")
|
||||||
|
inner.pack(fill="x", padx=10, pady=8)
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def create_combobox(ctk, parent, theme, *, variable, values, command=None):
|
||||||
|
return ctk.CTkComboBox(
|
||||||
|
parent, values=values, variable=variable, command=command,
|
||||||
|
height=32, font=(theme.ui_font_family, 12),
|
||||||
|
text_color=theme.text_primary, fg_color=theme.bg,
|
||||||
|
border_color=theme.field_border, button_color=theme.field_border,
|
||||||
|
button_hover_color=theme.text_secondary,
|
||||||
|
dropdown_fg_color=theme.field_bg,
|
||||||
|
dropdown_text_color=theme.text_primary,
|
||||||
|
dropdown_hover_color=theme.field_border,
|
||||||
|
corner_radius=8, state="readonly",
|
||||||
|
)
|
||||||
+11
-925
@@ -1,305 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
from typing import Any, Callable, Tuple
|
||||||
import os
|
|
||||||
import webbrowser
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
||||||
|
|
||||||
from proxy import __version__, get_link_host, parse_dc_ip_list, coerce_domain_list
|
from proxy import get_link_host
|
||||||
from proxy.balancer import balancer
|
from ui.ctk_controls import create_checkbox
|
||||||
from utils.update_check import RELEASES_PAGE_URL, get_status
|
from ui.ctk_theme import FIRST_RUN_FRAME_PAD, CtkTheme, main_content_frame
|
||||||
|
from ui.ctk_tooltip import attach_ctk_tooltip
|
||||||
|
from ui.i18n import t
|
||||||
from ui.ctk_theme import (
|
from ui.settings_form import (
|
||||||
FIRST_RUN_FRAME_PAD,
|
TrayConfigFormWidgets as TrayConfigFormWidgets,
|
||||||
CtkTheme,
|
install_tray_config_form as install_tray_config_form,
|
||||||
main_content_frame,
|
validate_config_form as validate_config_form,
|
||||||
)
|
)
|
||||||
from ui.ctk_tooltip import attach_ctk_tooltip, attach_tooltip_to_widgets
|
|
||||||
from ui.i18n import (
|
|
||||||
label_from_language,
|
|
||||||
language_from_label,
|
|
||||||
language_option_labels,
|
|
||||||
set_language,
|
|
||||||
t,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger('tg-mtproto-proxy')
|
|
||||||
|
|
||||||
|
|
||||||
def _get_doc_url(doc_name: str) -> str:
|
|
||||||
from ui.i18n import get_language
|
|
||||||
lang = get_language().value
|
|
||||||
lang_folder = "EN" if lang == "en" else "RU"
|
|
||||||
return f"https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/{lang_folder}/{doc_name}.md"
|
|
||||||
_CFPROXY_TEST_DCS = [1, 2, 3, 4, 5, 203]
|
|
||||||
_CFWORKER_TEST_DST = {
|
|
||||||
1: '149.154.175.50',
|
|
||||||
2: '149.154.167.51',
|
|
||||||
3: '149.154.175.100',
|
|
||||||
4: '149.154.167.91',
|
|
||||||
5: '149.154.171.5',
|
|
||||||
203: '91.105.192.100',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _run_connectivity_test(cases: list, *, secure: bool = True) -> dict:
|
|
||||||
import base64
|
|
||||||
from contextlib import nullcontext
|
|
||||||
import socket as _socket
|
|
||||||
from proxy.utils import create_ssl_context
|
|
||||||
|
|
||||||
ctx = create_ssl_context() if secure else None
|
|
||||||
port = 443 if secure else 80
|
|
||||||
results = {}
|
|
||||||
for dc, connect_host, sni_host, req_host, path in cases:
|
|
||||||
try:
|
|
||||||
with _socket.create_connection((connect_host, port), timeout=5) as raw:
|
|
||||||
connection = (ctx.wrap_socket(raw, server_hostname=sni_host)
|
|
||||||
if secure else nullcontext(raw))
|
|
||||||
with connection as ssock:
|
|
||||||
ws_key = base64.b64encode(os.urandom(16)).decode()
|
|
||||||
req = (
|
|
||||||
f"GET {path} HTTP/1.1\r\n"
|
|
||||||
f"Host: {req_host}\r\n"
|
|
||||||
f"Upgrade: websocket\r\n"
|
|
||||||
f"Connection: Upgrade\r\n"
|
|
||||||
f"Sec-WebSocket-Key: {ws_key}\r\n"
|
|
||||||
f"Sec-WebSocket-Version: 13\r\n"
|
|
||||||
f"Sec-WebSocket-Protocol: binary\r\n"
|
|
||||||
f"\r\n"
|
|
||||||
).encode()
|
|
||||||
ssock.sendall(req)
|
|
||||||
ssock.settimeout(5)
|
|
||||||
buf = b""
|
|
||||||
while b"\r\n\r\n" not in buf:
|
|
||||||
chunk = ssock.recv(512)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
buf += chunk
|
|
||||||
first = buf.decode("utf-8", errors="replace").split("\r\n")[0]
|
|
||||||
if "101" in first:
|
|
||||||
results[dc] = True
|
|
||||||
else:
|
|
||||||
results[dc] = first or t("connectivity.no_response")
|
|
||||||
ssock.close()
|
|
||||||
raw.close()
|
|
||||||
except _socket.timeout:
|
|
||||||
results[dc] = t("connectivity.timeout")
|
|
||||||
except OSError as exc:
|
|
||||||
msg = str(exc)
|
|
||||||
results[dc] = msg[:60] if len(msg) > 60 else msg
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def _run_cfproxy_connectivity_test(domain: str, *, secure: bool = True) -> dict:
|
|
||||||
cases = []
|
|
||||||
for dc in _CFPROXY_TEST_DCS:
|
|
||||||
host = f"kws{dc}.{domain}"
|
|
||||||
cases.append((dc, host, host, host, "/apiws"))
|
|
||||||
return _run_connectivity_test(cases, secure=secure)
|
|
||||||
|
|
||||||
|
|
||||||
def _run_cfworker_connectivity_test(domain: str, *, secure: bool = True) -> dict:
|
|
||||||
cases = []
|
|
||||||
for dc in _CFPROXY_TEST_DCS:
|
|
||||||
dst = _CFWORKER_TEST_DST[dc]
|
|
||||||
path = f"/apiws?dst={dst}&dc={dc}&media=0"
|
|
||||||
cases.append((dc, domain, domain, domain, path))
|
|
||||||
return _run_connectivity_test(cases, secure=secure)
|
|
||||||
|
|
||||||
|
|
||||||
def _run_cfproxy_multi_test(domains: list, *, secure: bool = True) -> dict:
|
|
||||||
return {domain: _run_cfproxy_connectivity_test(domain, secure=secure) for domain in domains}
|
|
||||||
|
|
||||||
|
|
||||||
def _run_cfworker_multi_test(domains: list, *, secure: bool = True) -> dict:
|
|
||||||
return {domain: _run_cfworker_connectivity_test(domain, secure=secure) for domain in domains}
|
|
||||||
|
|
||||||
|
|
||||||
def _run_cfproxy_auto_test(domains: list, *, secure: bool = True) -> tuple:
|
|
||||||
merged: dict = {}
|
|
||||||
best_domain = None
|
|
||||||
for domain in reversed(domains):
|
|
||||||
res = _run_cfproxy_connectivity_test(domain, secure=secure)
|
|
||||||
if all(v is True for v in res.values()):
|
|
||||||
return domain, res
|
|
||||||
for dc, v in res.items():
|
|
||||||
if v is True:
|
|
||||||
merged[dc] = True
|
|
||||||
best_domain = domain
|
|
||||||
elif dc not in merged:
|
|
||||||
merged[dc] = v
|
|
||||||
return best_domain, merged
|
|
||||||
|
|
||||||
|
|
||||||
def _show_connectivity_results(title_base: str, results: dict,
|
|
||||||
domain: str = '', label_prefix: str = 'DC',
|
|
||||||
auto_mode: bool = False,
|
|
||||||
unavailable_message: str = '') -> None:
|
|
||||||
import tkinter as _tk
|
|
||||||
from tkinter import messagebox as _mb
|
|
||||||
|
|
||||||
ok = [dc for dc, v in results.items() if v is True]
|
|
||||||
total = len(_CFPROXY_TEST_DCS)
|
|
||||||
if auto_mode:
|
|
||||||
if domain:
|
|
||||||
title = t("connectivity.available", title=title_base)
|
|
||||||
msg = t("connectivity.auto_ok", title=title_base, ok=len(ok), total=total)
|
|
||||||
else:
|
|
||||||
title = t("connectivity.unavailable", title=title_base)
|
|
||||||
msg = unavailable_message
|
|
||||||
else:
|
|
||||||
fail = [(dc, v) for dc, v in results.items() if v is not True]
|
|
||||||
if len(ok) == total:
|
|
||||||
title = t("connectivity.all_ok", title=title_base)
|
|
||||||
msg = t("connectivity.all_ok_domain", total=total, domain=domain)
|
|
||||||
elif not ok:
|
|
||||||
title = t("connectivity.unavailable", title=title_base)
|
|
||||||
errors = "\n".join(
|
|
||||||
t("connectivity.error_line", prefix=label_prefix, dc=dc, error=v)
|
|
||||||
for dc, v in fail
|
|
||||||
)
|
|
||||||
msg = t("connectivity.none_ok", domain=domain, errors=errors)
|
|
||||||
else:
|
|
||||||
title = t("connectivity.partial", title=title_base)
|
|
||||||
ok_list = ", ".join(f"{label_prefix}{dc}" for dc in ok)
|
|
||||||
fail_list = "\n".join(
|
|
||||||
t("connectivity.error_line", prefix=label_prefix, dc=dc, error=v)
|
|
||||||
for dc, v in fail
|
|
||||||
)
|
|
||||||
msg = t("connectivity.partial_detail", domain=domain, ok_list=ok_list, fail_list=fail_list)
|
|
||||||
|
|
||||||
root = _tk.Tk()
|
|
||||||
root.withdraw()
|
|
||||||
try:
|
|
||||||
root.attributes("-topmost", True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
_mb.showinfo(title, msg, parent=root)
|
|
||||||
root.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
def _show_multi_connectivity_results(title_base: str, per_domain: dict,
|
|
||||||
label_prefix: str = 'DC') -> None:
|
|
||||||
import tkinter as _tk
|
|
||||||
from tkinter import messagebox as _mb
|
|
||||||
|
|
||||||
total = len(_CFPROXY_TEST_DCS)
|
|
||||||
all_ok = True
|
|
||||||
any_ok = False
|
|
||||||
blocks = []
|
|
||||||
for domain, results in per_domain.items():
|
|
||||||
ok = [dc for dc, v in results.items() if v is True]
|
|
||||||
fail = [(dc, v) for dc, v in results.items() if v is not True]
|
|
||||||
if len(ok) == total:
|
|
||||||
any_ok = True
|
|
||||||
blocks.append(t("connectivity.multi_all_ok", domain=domain, total=total))
|
|
||||||
elif not ok:
|
|
||||||
all_ok = False
|
|
||||||
blocks.append(t("connectivity.multi_fail", domain=domain))
|
|
||||||
else:
|
|
||||||
all_ok = False
|
|
||||||
any_ok = True
|
|
||||||
ok_list = ", ".join(f"{label_prefix}{dc}" for dc in ok)
|
|
||||||
fail_list = ", ".join(f"{label_prefix}{dc}" for dc, _ in fail)
|
|
||||||
blocks.append(
|
|
||||||
t("connectivity.multi_partial", domain=domain, ok_list=ok_list, fail_list=fail_list)
|
|
||||||
)
|
|
||||||
|
|
||||||
if all_ok:
|
|
||||||
title = t("connectivity.all_ok", title=title_base)
|
|
||||||
elif any_ok:
|
|
||||||
title = t("connectivity.partial", title=title_base)
|
|
||||||
else:
|
|
||||||
title = t("connectivity.unavailable", title=title_base)
|
|
||||||
msg = "\n\n".join(blocks)
|
|
||||||
|
|
||||||
root = _tk.Tk()
|
|
||||||
root.withdraw()
|
|
||||||
try:
|
|
||||||
root.attributes("-topmost", True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
_mb.showinfo(title, msg, parent=root)
|
|
||||||
root.destroy()
|
|
||||||
|
|
||||||
_INNER_W = 396
|
|
||||||
|
|
||||||
_APPEARANCE_KEYS = ("auto", "light", "dark")
|
|
||||||
_APPEARANCE_TO_CTK = {"auto": "system", "light": "Light", "dark": "Dark"}
|
|
||||||
|
|
||||||
|
|
||||||
def _appearance_options() -> List[str]:
|
|
||||||
return [t(f"appearance.{key}") for key in _APPEARANCE_KEYS]
|
|
||||||
|
|
||||||
|
|
||||||
def _appearance_from_cfg(value: str) -> str:
|
|
||||||
if value in _APPEARANCE_KEYS:
|
|
||||||
return t(f"appearance.{value}")
|
|
||||||
return t("appearance.auto")
|
|
||||||
|
|
||||||
|
|
||||||
def _appearance_to_cfg(label: str) -> str:
|
|
||||||
for key in _APPEARANCE_KEYS:
|
|
||||||
if t(f"appearance.{key}") == label:
|
|
||||||
return key
|
|
||||||
return "auto"
|
|
||||||
|
|
||||||
|
|
||||||
def _sync_language_combobox(combo: Any, var: Any, cfg_value: str) -> None:
|
|
||||||
combo.configure(values=[label for _, label in language_option_labels()])
|
|
||||||
var.set(label_from_language(cfg_value))
|
|
||||||
|
|
||||||
|
|
||||||
def _entry(ctk, parent, theme, *, var=None, width=0, height=36, radius=10, **kw):
|
|
||||||
opts = {
|
|
||||||
"font": (theme.ui_font_family, 13), "corner_radius": radius,
|
|
||||||
"fg_color": theme.bg, "border_color": theme.field_border,
|
|
||||||
"border_width": 1, "text_color": theme.text_primary,
|
|
||||||
}
|
|
||||||
if var is not None:
|
|
||||||
opts["textvariable"] = var
|
|
||||||
if width:
|
|
||||||
opts["width"] = width
|
|
||||||
opts["height"] = height
|
|
||||||
opts.update(kw)
|
|
||||||
return ctk.CTkEntry(parent, **opts)
|
|
||||||
|
|
||||||
|
|
||||||
def _checkbox(ctk, parent, theme, text, variable):
|
|
||||||
return ctk.CTkCheckBox(
|
|
||||||
parent, text=text, variable=variable,
|
|
||||||
font=(theme.ui_font_family, 13), text_color=theme.text_primary,
|
|
||||||
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
|
||||||
corner_radius=6, border_width=2, border_color=theme.field_border,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _label(ctk, parent, theme, text, *, size=12, bold=False, secondary=True, **kw):
|
|
||||||
weight = "bold" if bold else "normal"
|
|
||||||
return ctk.CTkLabel(
|
|
||||||
parent, text=text,
|
|
||||||
font=(theme.ui_font_family, size, weight),
|
|
||||||
text_color=theme.text_secondary if secondary else theme.text_primary,
|
|
||||||
anchor="w", **kw,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _labeled_entry(ctk, parent, theme, label_text, value, *, tip="", width=0, pack_fill=False):
|
|
||||||
col = ctk.CTkFrame(parent, fg_color="transparent")
|
|
||||||
lbl = _label(ctk, col, theme, label_text)
|
|
||||||
lbl.pack(anchor="w", pady=(0, 2))
|
|
||||||
var = ctk.StringVar(value=str(value))
|
|
||||||
ent = _entry(ctk, col, theme, var=var, width=width)
|
|
||||||
if pack_fill:
|
|
||||||
ent.pack(fill="x")
|
|
||||||
else:
|
|
||||||
ent.pack(anchor="w")
|
|
||||||
if tip:
|
|
||||||
attach_tooltip_to_widgets([lbl, ent, col], tip)
|
|
||||||
return col, var
|
|
||||||
|
|
||||||
|
|
||||||
def tray_settings_scroll_and_footer(
|
def tray_settings_scroll_and_footer(
|
||||||
@@ -324,632 +36,6 @@ def tray_settings_scroll_and_footer(
|
|||||||
return scroll, footer
|
return scroll, footer
|
||||||
|
|
||||||
|
|
||||||
def _config_section(
|
|
||||||
ctk: Any,
|
|
||||||
parent: Any,
|
|
||||||
theme: CtkTheme,
|
|
||||||
title: str,
|
|
||||||
*,
|
|
||||||
bottom_spacer: int = 6,
|
|
||||||
) -> Any:
|
|
||||||
wrap = ctk.CTkFrame(parent, fg_color="transparent")
|
|
||||||
wrap.pack(fill="x", pady=(0, bottom_spacer))
|
|
||||||
_label(ctk, wrap, theme, title, secondary=False, bold=True).pack(anchor="w", pady=(0, 2))
|
|
||||||
card = ctk.CTkFrame(
|
|
||||||
wrap, fg_color=theme.field_bg, corner_radius=10,
|
|
||||||
border_width=1, border_color=theme.field_border,
|
|
||||||
)
|
|
||||||
card.pack(fill="x")
|
|
||||||
inner = ctk.CTkFrame(card, fg_color="transparent")
|
|
||||||
inner.pack(fill="x", padx=10, pady=8)
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TrayConfigFormWidgets:
|
|
||||||
host_var: Any
|
|
||||||
port_var: Any
|
|
||||||
secret_var: Any
|
|
||||||
dc_textbox: Any
|
|
||||||
verbose_var: Any
|
|
||||||
no_secure_var: Any
|
|
||||||
adv_entries: List[Any]
|
|
||||||
adv_keys: Tuple[str, ...]
|
|
||||||
autostart_var: Optional[Any]
|
|
||||||
check_updates_var: Optional[Any]
|
|
||||||
cfproxy_var: Optional[Any] = None
|
|
||||||
cfproxy_user_domain_enabled_var: Optional[Any] = None
|
|
||||||
cfproxy_user_domain_var: Optional[Any] = None
|
|
||||||
cfproxy_worker_enabled_var: Optional[Any] = None
|
|
||||||
cfproxy_worker_domain_var: Optional[Any] = None
|
|
||||||
appearance_var: Optional[Any] = None
|
|
||||||
language_var: Optional[Any] = None
|
|
||||||
|
|
||||||
|
|
||||||
def install_tray_config_form(
|
|
||||||
ctk: Any,
|
|
||||||
frame: Any,
|
|
||||||
theme: CtkTheme,
|
|
||||||
cfg: dict,
|
|
||||||
default_config: dict,
|
|
||||||
*,
|
|
||||||
show_autostart: bool = False,
|
|
||||||
autostart_value: bool = False,
|
|
||||||
on_language_change: Optional[Callable[[], None]] = None,
|
|
||||||
on_update_click: Optional[Callable[[], None]] = None,
|
|
||||||
) -> TrayConfigFormWidgets:
|
|
||||||
lang_cfg = cfg.get("language", default_config["language"])
|
|
||||||
set_language(lang_cfg)
|
|
||||||
|
|
||||||
header = ctk.CTkFrame(frame, fg_color="transparent")
|
|
||||||
header.pack(fill="x", pady=(0, 2))
|
|
||||||
ctk.CTkLabel(
|
|
||||||
header, text=t("settings.title"),
|
|
||||||
font=(theme.ui_font_family, 17, "bold"),
|
|
||||||
text_color=theme.text_primary, anchor="w",
|
|
||||||
).pack(side="left")
|
|
||||||
ctk.CTkLabel(
|
|
||||||
header, text=f"v{__version__}",
|
|
||||||
font=(theme.ui_font_family, 12),
|
|
||||||
text_color=theme.text_secondary, anchor="e",
|
|
||||||
).pack(side="right", padx=(4, 0))
|
|
||||||
|
|
||||||
appearance_var = ctk.StringVar(
|
|
||||||
value=_appearance_from_cfg(cfg.get("appearance", "auto"))
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_appearance_change(choice: str) -> None:
|
|
||||||
cfg_val = _appearance_to_cfg(choice)
|
|
||||||
ctk.set_appearance_mode(_APPEARANCE_TO_CTK[cfg_val])
|
|
||||||
cfg["appearance"] = cfg_val
|
|
||||||
|
|
||||||
ctk.CTkButton(
|
|
||||||
header, text="Donate ♥", width=90, height=28,
|
|
||||||
font=(theme.ui_font_family, 13, "bold"), corner_radius=8,
|
|
||||||
fg_color="#22c55e", hover_color="#16a34a",
|
|
||||||
text_color="#ffffff", border_width=0,
|
|
||||||
command=lambda: (
|
|
||||||
header.winfo_toplevel().iconify(),
|
|
||||||
webbrowser.open(_get_doc_url("Funding")),
|
|
||||||
),
|
|
||||||
).pack(side="right", padx=(0, 6))
|
|
||||||
|
|
||||||
ui_inner = _config_section(ctk, frame, theme, t("section.interface"))
|
|
||||||
ui_row = ctk.CTkFrame(ui_inner, fg_color="transparent")
|
|
||||||
ui_row.pack(fill="x")
|
|
||||||
|
|
||||||
lang_col = ctk.CTkFrame(ui_row, fg_color="transparent")
|
|
||||||
lang_col.pack(side="left", fill="x", expand=True, padx=(0, 8))
|
|
||||||
|
|
||||||
theme_col = ctk.CTkFrame(ui_row, fg_color="transparent")
|
|
||||||
theme_col.pack(side="left", fill="x", expand=True, padx=(8, 0))
|
|
||||||
|
|
||||||
language_var = ctk.StringVar(value=label_from_language(lang_cfg))
|
|
||||||
_label(ctk, lang_col, theme, t("settings.language"), size=11).pack(
|
|
||||||
anchor="w", pady=(0, 2)
|
|
||||||
)
|
|
||||||
language_combo = ctk.CTkComboBox(
|
|
||||||
lang_col,
|
|
||||||
values=[label for _, label in language_option_labels()],
|
|
||||||
variable=language_var,
|
|
||||||
height=32,
|
|
||||||
font=(theme.ui_font_family, 12),
|
|
||||||
text_color=theme.text_primary,
|
|
||||||
fg_color=theme.bg,
|
|
||||||
border_color=theme.field_border,
|
|
||||||
button_color=theme.field_border,
|
|
||||||
button_hover_color=theme.text_secondary,
|
|
||||||
dropdown_fg_color=theme.field_bg,
|
|
||||||
dropdown_text_color=theme.text_primary,
|
|
||||||
dropdown_hover_color=theme.field_border,
|
|
||||||
corner_radius=8,
|
|
||||||
state="readonly",
|
|
||||||
)
|
|
||||||
language_combo.pack(fill="x")
|
|
||||||
_sync_language_combobox(language_combo, language_var, lang_cfg)
|
|
||||||
|
|
||||||
_label(ctk, theme_col, theme, t("settings.theme"), size=11).pack(
|
|
||||||
anchor="w", pady=(0, 2)
|
|
||||||
)
|
|
||||||
theme_combo = ctk.CTkComboBox(
|
|
||||||
theme_col,
|
|
||||||
values=_appearance_options(),
|
|
||||||
variable=appearance_var,
|
|
||||||
height=32,
|
|
||||||
font=(theme.ui_font_family, 12),
|
|
||||||
text_color=theme.text_primary,
|
|
||||||
fg_color=theme.bg,
|
|
||||||
border_color=theme.field_border,
|
|
||||||
button_color=theme.field_border,
|
|
||||||
button_hover_color=theme.text_secondary,
|
|
||||||
dropdown_fg_color=theme.field_bg,
|
|
||||||
dropdown_text_color=theme.text_primary,
|
|
||||||
dropdown_hover_color=theme.field_border,
|
|
||||||
corner_radius=8,
|
|
||||||
state="readonly",
|
|
||||||
command=_on_appearance_change,
|
|
||||||
)
|
|
||||||
theme_combo.pack(fill="x")
|
|
||||||
|
|
||||||
conn = _config_section(ctk, frame, theme, t("section.mtproto"))
|
|
||||||
|
|
||||||
host_row = ctk.CTkFrame(conn, fg_color="transparent")
|
|
||||||
host_row.pack(fill="x")
|
|
||||||
|
|
||||||
host_col, host_var = _labeled_entry(
|
|
||||||
ctk, host_row, theme, t("label.host"),
|
|
||||||
cfg.get("host", default_config["host"]),
|
|
||||||
tip=t("tip.host"), width=160, pack_fill=True,
|
|
||||||
)
|
|
||||||
host_col.pack(side="left", fill="x", expand=True, padx=(0, 10))
|
|
||||||
|
|
||||||
port_col, port_var = _labeled_entry(
|
|
||||||
ctk, host_row, theme, t("label.port"),
|
|
||||||
cfg.get("port", default_config["port"]),
|
|
||||||
tip=t("tip.port"), width=100,
|
|
||||||
)
|
|
||||||
port_col.pack(side="left")
|
|
||||||
|
|
||||||
secret_row = ctk.CTkFrame(conn, fg_color="transparent")
|
|
||||||
secret_row.pack(fill="x")
|
|
||||||
|
|
||||||
secret_col, secret_var = _labeled_entry(
|
|
||||||
ctk, secret_row, theme, t("label.secret"),
|
|
||||||
cfg.get("secret", default_config["secret"]),
|
|
||||||
tip=t("tip.secret"), width=160, pack_fill=True,
|
|
||||||
)
|
|
||||||
secret_col.pack(side="left", fill="x", expand=True, padx=(0, 10))
|
|
||||||
|
|
||||||
regen_col = ctk.CTkFrame(secret_row, fg_color="transparent")
|
|
||||||
regen_col.pack(side="left", anchor="s")
|
|
||||||
ctk.CTkLabel(regen_col, text="", font=(theme.ui_font_family, 12)).pack(pady=(0, 2))
|
|
||||||
ctk.CTkButton(
|
|
||||||
regen_col, text="↺", width=36, height=36,
|
|
||||||
font=(theme.ui_font_family, 18), corner_radius=10,
|
|
||||||
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
|
||||||
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
|
||||||
command=lambda: secret_var.set(os.urandom(16).hex()),
|
|
||||||
).pack()
|
|
||||||
|
|
||||||
dc_inner = _config_section(ctk, frame, theme, t("section.dc"))
|
|
||||||
dc_lbl = _label(ctk, dc_inner, theme, t("label.dc_hint"), size=11)
|
|
||||||
dc_lbl.pack(anchor="w", pady=(0, 4))
|
|
||||||
dc_textbox = ctk.CTkTextbox(
|
|
||||||
dc_inner, width=_INNER_W, height=88,
|
|
||||||
font=(theme.mono_font_family, 12), corner_radius=10,
|
|
||||||
fg_color=theme.bg, border_color=theme.field_border,
|
|
||||||
border_width=1, text_color=theme.text_primary,
|
|
||||||
)
|
|
||||||
dc_textbox.pack(fill="x")
|
|
||||||
dc_textbox.insert("1.0", "\n".join(cfg.get("dc_ip", default_config["dc_ip"])))
|
|
||||||
attach_tooltip_to_widgets([dc_lbl, dc_textbox], t("tip.dc"))
|
|
||||||
|
|
||||||
cf_inner = _config_section(ctk, frame, theme, t("section.cfproxy"))
|
|
||||||
|
|
||||||
cf_row = ctk.CTkFrame(cf_inner, fg_color="transparent")
|
|
||||||
cf_row.pack(fill="x", pady=(0, 4))
|
|
||||||
|
|
||||||
cfproxy_var = ctk.BooleanVar(
|
|
||||||
value=cfg.get("cfproxy", default_config.get("cfproxy", True))
|
|
||||||
)
|
|
||||||
cf_cb = _checkbox(ctk, cf_row, theme, t("label.cf_enable"), cfproxy_var)
|
|
||||||
cf_cb.pack(side="left", padx=(0, 16))
|
|
||||||
attach_ctk_tooltip(cf_cb, t("tip.cfproxy"))
|
|
||||||
|
|
||||||
_cf_test_btn = [None]
|
|
||||||
|
|
||||||
def _on_cf_test():
|
|
||||||
secure = not no_secure_var.get()
|
|
||||||
user_domains = (
|
|
||||||
coerce_domain_list(cfproxy_user_domain_var.get())
|
|
||||||
if cf_custom_cb_var.get() else []
|
|
||||||
)
|
|
||||||
btn = _cf_test_btn[0]
|
|
||||||
if btn:
|
|
||||||
btn.configure(text=t("button.test_loading"), state="disabled")
|
|
||||||
import threading as _threading
|
|
||||||
if user_domains:
|
|
||||||
def _worker():
|
|
||||||
try:
|
|
||||||
per = _run_cfproxy_multi_test(user_domains, secure=secure)
|
|
||||||
if btn:
|
|
||||||
btn.after(
|
|
||||||
0,
|
|
||||||
lambda: _show_multi_connectivity_results(
|
|
||||||
t("connectivity.cfproxy_title"), per, label_prefix='kws',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
log.error("CF proxy test failed: %s", exc)
|
|
||||||
finally:
|
|
||||||
if btn:
|
|
||||||
btn.after(0, lambda: btn.configure(text=t("button.test"), state="normal"))
|
|
||||||
_threading.Thread(target=_worker, daemon=True).start()
|
|
||||||
else:
|
|
||||||
def _worker_auto():
|
|
||||||
try:
|
|
||||||
ok_domain, res = _run_cfproxy_auto_test(balancer.domains, secure=secure)
|
|
||||||
if btn:
|
|
||||||
btn.after(
|
|
||||||
0,
|
|
||||||
lambda: _show_connectivity_results(
|
|
||||||
t("connectivity.cfproxy_title"), res,
|
|
||||||
domain=ok_domain or '',
|
|
||||||
auto_mode=True,
|
|
||||||
unavailable_message=t("connectivity.cf_auto_fail"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
log.error("CF proxy auto-test failed: %s", exc)
|
|
||||||
finally:
|
|
||||||
if btn:
|
|
||||||
btn.after(0, lambda: btn.configure(text=t("button.test"), state="normal"))
|
|
||||||
_threading.Thread(target=_worker_auto, daemon=True).start()
|
|
||||||
|
|
||||||
_cf_test_widget = ctk.CTkButton(
|
|
||||||
cf_row, text=t("button.test"), width=56, height=28,
|
|
||||||
font=(theme.ui_font_family, 13), corner_radius=8,
|
|
||||||
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
|
||||||
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
|
||||||
command=_on_cf_test,
|
|
||||||
)
|
|
||||||
_cf_test_widget.pack(side="right")
|
|
||||||
_cf_test_btn[0] = _cf_test_widget
|
|
||||||
|
|
||||||
cf_custom_row = ctk.CTkFrame(cf_inner, fg_color="transparent")
|
|
||||||
cf_custom_row.pack(fill="x")
|
|
||||||
|
|
||||||
saved_user_domains = coerce_domain_list(
|
|
||||||
cfg.get("cfproxy_user_domain", default_config.get("cfproxy_user_domain", ""))
|
|
||||||
)
|
|
||||||
cf_custom_cb_var = ctk.BooleanVar(
|
|
||||||
value=cfg.get("cfproxy_user_domain_enabled", bool(saved_user_domains))
|
|
||||||
)
|
|
||||||
cf_custom_cb = _checkbox(ctk, cf_custom_row, theme, t("label.cf_custom_domain"), cf_custom_cb_var)
|
|
||||||
cf_custom_cb.pack(side="left", padx=(0, 10))
|
|
||||||
attach_ctk_tooltip(cf_custom_cb, t("tip.cfproxy_user_domain_cb"))
|
|
||||||
|
|
||||||
ctk.CTkButton(
|
|
||||||
cf_custom_row, text="?", width=28, height=32,
|
|
||||||
font=(theme.ui_font_family, 14), corner_radius=8,
|
|
||||||
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
|
||||||
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
|
||||||
command=lambda: webbrowser.open(_get_doc_url("CfProxy")),
|
|
||||||
).pack(side="right")
|
|
||||||
|
|
||||||
cfproxy_user_domain_var = ctk.StringVar(value=", ".join(saved_user_domains))
|
|
||||||
cf_domain_entry = _entry(
|
|
||||||
ctk, cf_custom_row, theme, var=cfproxy_user_domain_var,
|
|
||||||
height=32, radius=8,
|
|
||||||
)
|
|
||||||
cf_domain_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))
|
|
||||||
attach_ctk_tooltip(cf_domain_entry, t("tip.cfproxy_domain"))
|
|
||||||
|
|
||||||
def _sync_domain_entry(*_):
|
|
||||||
state = "normal" if cf_custom_cb_var.get() else "disabled"
|
|
||||||
cf_domain_entry.configure(state=state)
|
|
||||||
|
|
||||||
cf_custom_cb_var.trace_add("write", _sync_domain_entry)
|
|
||||||
_sync_domain_entry()
|
|
||||||
|
|
||||||
cf_worker_inner = _config_section(ctk, frame, theme, t("section.cfworker"))
|
|
||||||
|
|
||||||
cf_worker_row = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
|
|
||||||
cf_worker_row.pack(fill="x", pady=(0, 4))
|
|
||||||
cf_worker_lbl = _label(ctk, cf_worker_row, theme, t("label.cfworker_domains"), size=11)
|
|
||||||
cf_worker_lbl.pack(side="left", anchor="w", pady=(0, 2))
|
|
||||||
|
|
||||||
cf_worker_input = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
|
|
||||||
cf_worker_input.pack(fill="x")
|
|
||||||
|
|
||||||
saved_worker_domains = coerce_domain_list(
|
|
||||||
cfg.get("cfproxy_worker_domain", default_config.get("cfproxy_worker_domain", ""))
|
|
||||||
)
|
|
||||||
cfproxy_worker_enabled_var = ctk.BooleanVar(
|
|
||||||
value=cfg.get("cfproxy_worker_enabled", bool(saved_worker_domains))
|
|
||||||
)
|
|
||||||
cf_worker_cb = _checkbox(
|
|
||||||
ctk, cf_worker_input, theme, t("label.cf_custom_domain"),
|
|
||||||
cfproxy_worker_enabled_var,
|
|
||||||
)
|
|
||||||
cf_worker_cb.pack(side="left", padx=(0, 10))
|
|
||||||
attach_ctk_tooltip(cf_worker_cb, t("tip.cfworker_domain"))
|
|
||||||
|
|
||||||
cfproxy_worker_domain_var = ctk.StringVar(value=", ".join(saved_worker_domains))
|
|
||||||
cf_worker_entry = _entry(
|
|
||||||
ctk, cf_worker_input, theme, var=cfproxy_worker_domain_var,
|
|
||||||
height=32, radius=8,
|
|
||||||
)
|
|
||||||
cf_worker_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))
|
|
||||||
attach_tooltip_to_widgets([cf_worker_lbl, cf_worker_entry], t("tip.cfworker_domain"))
|
|
||||||
|
|
||||||
_cfworker_test_btn = [None]
|
|
||||||
|
|
||||||
def _sync_cfworker_test_button(*_):
|
|
||||||
btn = _cfworker_test_btn[0]
|
|
||||||
if btn is None:
|
|
||||||
return
|
|
||||||
enabled = (
|
|
||||||
cfproxy_worker_enabled_var.get()
|
|
||||||
and bool(coerce_domain_list(cfproxy_worker_domain_var.get()))
|
|
||||||
)
|
|
||||||
btn.configure(state="normal" if enabled else "disabled")
|
|
||||||
|
|
||||||
def _on_cfworker_test():
|
|
||||||
secure = not no_secure_var.get()
|
|
||||||
domains = coerce_domain_list(cfproxy_worker_domain_var.get())
|
|
||||||
btn = _cfworker_test_btn[0]
|
|
||||||
if not cfproxy_worker_enabled_var.get() or not domains or btn is None:
|
|
||||||
return
|
|
||||||
btn.configure(text=t("button.test_loading"), state="disabled")
|
|
||||||
import threading as _threading
|
|
||||||
|
|
||||||
def _worker():
|
|
||||||
try:
|
|
||||||
per = _run_cfworker_multi_test(domains, secure=secure)
|
|
||||||
btn.after(
|
|
||||||
0,
|
|
||||||
lambda: _show_multi_connectivity_results(
|
|
||||||
t("connectivity.cfworker_title"), per, label_prefix='DC',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
log.error("CF worker test failed: %s", exc)
|
|
||||||
finally:
|
|
||||||
btn.after(0, lambda: btn.configure(text=t("button.test")))
|
|
||||||
btn.after(0, _sync_cfworker_test_button)
|
|
||||||
|
|
||||||
_threading.Thread(target=_worker, daemon=True).start()
|
|
||||||
|
|
||||||
ctk.CTkButton(
|
|
||||||
cf_worker_input, text="?", width=28, height=32,
|
|
||||||
font=(theme.ui_font_family, 14), corner_radius=8,
|
|
||||||
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
|
||||||
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
|
||||||
command=lambda: webbrowser.open(_get_doc_url("CfWorker")),
|
|
||||||
).pack(side="right")
|
|
||||||
|
|
||||||
_cfworker_test_widget = ctk.CTkButton(
|
|
||||||
cf_worker_row, text=t("button.test"), width=56, height=28,
|
|
||||||
font=(theme.ui_font_family, 13), corner_radius=8,
|
|
||||||
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
|
||||||
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
|
||||||
command=_on_cfworker_test,
|
|
||||||
)
|
|
||||||
_cfworker_test_widget.pack(side="right")
|
|
||||||
_cfworker_test_btn[0] = _cfworker_test_widget
|
|
||||||
|
|
||||||
def _sync_cfworker_entry(*_):
|
|
||||||
state = "normal" if cfproxy_worker_enabled_var.get() else "disabled"
|
|
||||||
cf_worker_entry.configure(state=state)
|
|
||||||
_sync_cfworker_test_button()
|
|
||||||
|
|
||||||
cfproxy_worker_enabled_var.trace_add("write", _sync_cfworker_entry)
|
|
||||||
cfproxy_worker_domain_var.trace_add("write", _sync_cfworker_test_button)
|
|
||||||
_sync_cfworker_entry()
|
|
||||||
|
|
||||||
log_inner = _config_section(ctk, frame, theme, t("section.logs"))
|
|
||||||
|
|
||||||
verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False))
|
|
||||||
verbose_cb = _checkbox(ctk, log_inner, theme, t("label.verbose"), verbose_var)
|
|
||||||
verbose_cb.pack(anchor="w", pady=(0, 6))
|
|
||||||
attach_ctk_tooltip(verbose_cb, t("tip.verbose"))
|
|
||||||
|
|
||||||
no_secure_var = ctk.BooleanVar(value=cfg.get("no_secure", False))
|
|
||||||
no_secure_cb = _checkbox(ctk, log_inner, theme, t("label.no_secure"), no_secure_var)
|
|
||||||
no_secure_cb.pack(anchor="w", pady=(0, 6))
|
|
||||||
attach_ctk_tooltip(no_secure_cb, t("tip.no_secure"))
|
|
||||||
|
|
||||||
adv_frame = ctk.CTkFrame(log_inner, fg_color="transparent")
|
|
||||||
adv_frame.pack(fill="x")
|
|
||||||
|
|
||||||
adv_rows = [
|
|
||||||
(t("label.buf_kb"), "buf_kb", t("tip.buf_kb")),
|
|
||||||
(t("label.pool_size"), "pool_size", t("tip.pool")),
|
|
||||||
(t("label.log_max_mb"), "log_max_mb", t("tip.log_mb")),
|
|
||||||
]
|
|
||||||
for label_text, key, tip in adv_rows:
|
|
||||||
col = ctk.CTkFrame(adv_frame, fg_color="transparent")
|
|
||||||
col.pack(fill="x", pady=(0, 0 if key == "log_max_mb" else 5))
|
|
||||||
adv_l = _label(ctk, col, theme, label_text, size=11)
|
|
||||||
adv_l.pack(anchor="w", pady=(0, 2))
|
|
||||||
adv_e = _entry(
|
|
||||||
ctk, col, theme, width=_INNER_W, height=32, radius=8,
|
|
||||||
textvariable=ctk.StringVar(value=str(cfg.get(key, default_config[key]))),
|
|
||||||
)
|
|
||||||
adv_e.pack(fill="x")
|
|
||||||
attach_tooltip_to_widgets([adv_l, adv_e, col], tip)
|
|
||||||
|
|
||||||
adv_entries = list(adv_frame.winfo_children())
|
|
||||||
adv_keys = ("buf_kb", "pool_size", "log_max_mb")
|
|
||||||
|
|
||||||
upd_inner = _config_section(ctk, frame, theme, t("section.updates"))
|
|
||||||
st = get_status()
|
|
||||||
check_updates_var = ctk.BooleanVar(
|
|
||||||
value=bool(cfg.get("check_updates", default_config.get("check_updates", True)))
|
|
||||||
)
|
|
||||||
upd_cb = _checkbox(ctk, upd_inner, theme, t("label.check_updates"), check_updates_var)
|
|
||||||
upd_cb.pack(anchor="w", pady=(0, 6))
|
|
||||||
attach_ctk_tooltip(upd_cb, t("tip.check_updates"))
|
|
||||||
|
|
||||||
if st.get("error"):
|
|
||||||
upd_status = t("updates.status_error")
|
|
||||||
elif not st.get("checked"):
|
|
||||||
upd_status = t("updates.status_pending")
|
|
||||||
elif st.get("has_update") and st.get("latest"):
|
|
||||||
upd_status = t("updates.status_available", latest=st["latest"], current=__version__)
|
|
||||||
elif st.get("ahead_of_release") and st.get("latest"):
|
|
||||||
upd_status = t("updates.status_ahead", current=__version__, latest=st["latest"])
|
|
||||||
else:
|
|
||||||
upd_status = t("updates.status_latest")
|
|
||||||
|
|
||||||
_label(ctk, upd_inner, theme, upd_status, size=11,
|
|
||||||
justify="left", wraplength=_INNER_W).pack(anchor="w", pady=(0, 8))
|
|
||||||
|
|
||||||
rel_url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
|
||||||
if st.get("has_update") and on_update_click is not None:
|
|
||||||
upd_btn_row = ctk.CTkFrame(upd_inner, fg_color="transparent")
|
|
||||||
upd_btn_row.pack(fill="x")
|
|
||||||
upd_btn_row.grid_columnconfigure(0, weight=1)
|
|
||||||
upd_btn_row.grid_columnconfigure(1, weight=1)
|
|
||||||
ctk.CTkButton(
|
|
||||||
upd_btn_row, text=t("button.open_release"), height=32,
|
|
||||||
font=(theme.ui_font_family, 13), corner_radius=8,
|
|
||||||
fg_color=theme.field_bg, hover_color=theme.field_border,
|
|
||||||
text_color=theme.text_primary, border_width=1,
|
|
||||||
border_color=theme.field_border,
|
|
||||||
command=lambda u=rel_url: webbrowser.open(u),
|
|
||||||
).grid(row=0, column=0, sticky="ew", padx=(0, 4))
|
|
||||||
ctk.CTkButton(
|
|
||||||
upd_btn_row, text=t("button.update"), height=32,
|
|
||||||
font=(theme.ui_font_family, 13, "bold"), corner_radius=8,
|
|
||||||
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
|
||||||
text_color="#ffffff",
|
|
||||||
command=on_update_click,
|
|
||||||
).grid(row=0, column=1, sticky="ew", padx=(4, 0))
|
|
||||||
else:
|
|
||||||
ctk.CTkButton(
|
|
||||||
upd_inner, text=t("button.open_release"), height=32,
|
|
||||||
font=(theme.ui_font_family, 13), corner_radius=8,
|
|
||||||
fg_color=theme.field_bg, hover_color=theme.field_border,
|
|
||||||
text_color=theme.text_primary, border_width=1,
|
|
||||||
border_color=theme.field_border,
|
|
||||||
command=lambda u=rel_url: webbrowser.open(u),
|
|
||||||
).pack(anchor="w")
|
|
||||||
|
|
||||||
autostart_var = None
|
|
||||||
if show_autostart:
|
|
||||||
sys_inner = _config_section(ctk, frame, theme, t("section.windows_startup"), bottom_spacer=4)
|
|
||||||
autostart_var = ctk.BooleanVar(value=autostart_value)
|
|
||||||
as_cb = _checkbox(ctk, sys_inner, theme, t("label.autostart"), autostart_var)
|
|
||||||
as_cb.pack(anchor="w", pady=(0, 4))
|
|
||||||
as_hint = _label(
|
|
||||||
ctk, sys_inner, theme,
|
|
||||||
t("label.autostart_hint"),
|
|
||||||
size=11, justify="left", wraplength=_INNER_W,
|
|
||||||
)
|
|
||||||
as_hint.pack(anchor="w")
|
|
||||||
attach_tooltip_to_widgets([as_cb, as_hint], t("tip.autostart"))
|
|
||||||
|
|
||||||
return TrayConfigFormWidgets(
|
|
||||||
host_var=host_var, port_var=port_var, secret_var=secret_var,
|
|
||||||
dc_textbox=dc_textbox, verbose_var=verbose_var, no_secure_var=no_secure_var,
|
|
||||||
adv_entries=adv_entries, adv_keys=adv_keys,
|
|
||||||
autostart_var=autostart_var, check_updates_var=check_updates_var,
|
|
||||||
cfproxy_var=cfproxy_var,
|
|
||||||
cfproxy_user_domain_enabled_var=cf_custom_cb_var,
|
|
||||||
cfproxy_user_domain_var=cfproxy_user_domain_var,
|
|
||||||
cfproxy_worker_enabled_var=cfproxy_worker_enabled_var,
|
|
||||||
cfproxy_worker_domain_var=cfproxy_worker_domain_var,
|
|
||||||
appearance_var=appearance_var,
|
|
||||||
language_var=language_var,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def merge_adv_from_form(
|
|
||||||
widgets: TrayConfigFormWidgets,
|
|
||||||
base: Dict[str, Any],
|
|
||||||
default_config: dict,
|
|
||||||
) -> None:
|
|
||||||
for i, key in enumerate(widgets.adv_keys):
|
|
||||||
col_frame = widgets.adv_entries[i]
|
|
||||||
entry = col_frame.winfo_children()[1]
|
|
||||||
try:
|
|
||||||
val = float(entry.get().strip())
|
|
||||||
if key in ("buf_kb", "pool_size"):
|
|
||||||
val = int(val)
|
|
||||||
base[key] = val
|
|
||||||
except ValueError:
|
|
||||||
base[key] = default_config[key]
|
|
||||||
|
|
||||||
|
|
||||||
def _dc_validation_message(error: ValueError) -> str:
|
|
||||||
exc_entry = getattr(error, "entry", None)
|
|
||||||
if exc_entry is None:
|
|
||||||
return str(error)
|
|
||||||
kind = getattr(error, "kind", "invalid")
|
|
||||||
if kind == "format":
|
|
||||||
return t("validation.dc_format", entry=exc_entry)
|
|
||||||
return t("validation.dc_invalid", entry=exc_entry)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config_form(
|
|
||||||
widgets: TrayConfigFormWidgets,
|
|
||||||
default_config: dict,
|
|
||||||
*,
|
|
||||||
include_autostart: bool,
|
|
||||||
) -> Union[dict, str]:
|
|
||||||
import socket as _sock
|
|
||||||
|
|
||||||
host_val = widgets.host_var.get().strip()
|
|
||||||
try:
|
|
||||||
_sock.inet_aton(host_val)
|
|
||||||
except OSError:
|
|
||||||
return t("validation.bad_host")
|
|
||||||
|
|
||||||
try:
|
|
||||||
port_val = int(widgets.port_var.get().strip())
|
|
||||||
if not (1 <= port_val <= 65535):
|
|
||||||
raise ValueError
|
|
||||||
except ValueError:
|
|
||||||
return t("validation.bad_port")
|
|
||||||
|
|
||||||
lines = [
|
|
||||||
line.strip()
|
|
||||||
for line in widgets.dc_textbox.get("1.0", "end").strip().splitlines()
|
|
||||||
if line.strip()
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
parse_dc_ip_list(lines)
|
|
||||||
except ValueError as e:
|
|
||||||
return _dc_validation_message(e)
|
|
||||||
|
|
||||||
secret_val = widgets.secret_var.get().strip()
|
|
||||||
if len(secret_val) != 32:
|
|
||||||
return t("validation.bad_secret_len")
|
|
||||||
try:
|
|
||||||
bytes.fromhex(secret_val)
|
|
||||||
except ValueError:
|
|
||||||
return t("validation.bad_secret_hex")
|
|
||||||
|
|
||||||
new_cfg: Dict[str, Any] = {
|
|
||||||
"host": host_val,
|
|
||||||
"port": port_val,
|
|
||||||
"secret": secret_val,
|
|
||||||
"dc_ip": lines,
|
|
||||||
"verbose": widgets.verbose_var.get(),
|
|
||||||
}
|
|
||||||
if include_autostart:
|
|
||||||
new_cfg["autostart"] = (
|
|
||||||
widgets.autostart_var.get()
|
|
||||||
if widgets.autostart_var is not None
|
|
||||||
else False
|
|
||||||
)
|
|
||||||
|
|
||||||
merge_adv_from_form(widgets, new_cfg, default_config)
|
|
||||||
if widgets.check_updates_var is not None:
|
|
||||||
new_cfg["check_updates"] = bool(widgets.check_updates_var.get())
|
|
||||||
if widgets.cfproxy_var is not None:
|
|
||||||
new_cfg["cfproxy"] = bool(widgets.cfproxy_var.get())
|
|
||||||
if widgets.cfproxy_user_domain_enabled_var is not None:
|
|
||||||
new_cfg["cfproxy_user_domain_enabled"] = bool(
|
|
||||||
widgets.cfproxy_user_domain_enabled_var.get()
|
|
||||||
)
|
|
||||||
if widgets.cfproxy_user_domain_var is not None:
|
|
||||||
new_cfg["cfproxy_user_domain"] = coerce_domain_list(widgets.cfproxy_user_domain_var.get())
|
|
||||||
if widgets.cfproxy_worker_enabled_var is not None:
|
|
||||||
new_cfg["cfproxy_worker_enabled"] = bool(widgets.cfproxy_worker_enabled_var.get())
|
|
||||||
if widgets.cfproxy_worker_domain_var is not None:
|
|
||||||
new_cfg["cfproxy_worker_domain"] = coerce_domain_list(widgets.cfproxy_worker_domain_var.get())
|
|
||||||
if widgets.appearance_var is not None:
|
|
||||||
new_cfg["appearance"] = _appearance_to_cfg(widgets.appearance_var.get())
|
|
||||||
if widgets.language_var is not None:
|
|
||||||
new_cfg["language"] = language_from_label(widgets.language_var.get()).value
|
|
||||||
if widgets.no_secure_var is not None:
|
|
||||||
new_cfg["no_secure"] = bool(widgets.no_secure_var.get())
|
|
||||||
return new_cfg
|
|
||||||
|
|
||||||
|
|
||||||
def install_tray_config_buttons(
|
def install_tray_config_buttons(
|
||||||
ctk: Any,
|
ctk: Any,
|
||||||
frame: Any,
|
frame: Any,
|
||||||
@@ -1051,7 +137,7 @@ def populate_first_run_window(
|
|||||||
corner_radius=0).pack(fill="x", pady=(0, 12))
|
corner_radius=0).pack(fill="x", pady=(0, 12))
|
||||||
|
|
||||||
auto_var = ctk.BooleanVar(value=True)
|
auto_var = ctk.BooleanVar(value=True)
|
||||||
_checkbox(ctk, frame, theme, t("first_run.open_now"),
|
create_checkbox(ctk, frame, theme, t("first_run.open_now"),
|
||||||
auto_var).pack(anchor="w", pady=(0, 16))
|
auto_var).pack(anchor="w", pady=(0, 16))
|
||||||
|
|
||||||
def on_ok():
|
def on_ok():
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import socket
|
||||||
|
from copy import deepcopy
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from proxy import coerce_domain_list, parse_dc_ip_list
|
||||||
|
from ui.i18n import t
|
||||||
|
|
||||||
|
UI_ONLY_KEYS = frozenset({
|
||||||
|
"appearance", "autostart", "check_updates", "language"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SettingsChange:
|
||||||
|
config: dict
|
||||||
|
changed_keys: frozenset
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires_restart(self) -> bool:
|
||||||
|
return bool(self.changed_keys - UI_ONLY_KEYS)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_settings(current: dict, values: dict, defaults: dict) -> SettingsChange:
|
||||||
|
config = deepcopy(defaults)
|
||||||
|
config.update(deepcopy(current))
|
||||||
|
changed_keys = frozenset(
|
||||||
|
key for key, value in values.items() if value != config.get(key)
|
||||||
|
)
|
||||||
|
config.update(deepcopy(values))
|
||||||
|
return SettingsChange(config, changed_keys)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_settings(values: dict, defaults: dict) -> Union[dict, str]:
|
||||||
|
config = dict(values)
|
||||||
|
|
||||||
|
host = values["host"].strip()
|
||||||
|
try:
|
||||||
|
socket.inet_aton(host)
|
||||||
|
except OSError:
|
||||||
|
return t("validation.bad_host")
|
||||||
|
|
||||||
|
try:
|
||||||
|
port = int(values["port"].strip())
|
||||||
|
if not 1 <= port <= 65535:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
return t("validation.bad_port")
|
||||||
|
|
||||||
|
lines = [line.strip() for line in values["dc_ip"].splitlines() if line.strip()]
|
||||||
|
try:
|
||||||
|
parse_dc_ip_list(lines)
|
||||||
|
except ValueError as exc:
|
||||||
|
entry = getattr(exc, "entry", None)
|
||||||
|
if entry is None:
|
||||||
|
return str(exc)
|
||||||
|
key = "dc_format" if getattr(exc, "kind", "invalid") == "format" else "dc_invalid"
|
||||||
|
return t(f"validation.{key}", entry=entry)
|
||||||
|
|
||||||
|
secret = values["secret"].strip()
|
||||||
|
if len(secret) != 32:
|
||||||
|
return t("validation.bad_secret_len")
|
||||||
|
if any(char not in "0123456789abcdefABCDEF" for char in secret):
|
||||||
|
return t("validation.bad_secret_hex")
|
||||||
|
|
||||||
|
config.update(host=host, port=port, secret=secret, dc_ip=lines)
|
||||||
|
|
||||||
|
for key in ("buf_kb", "pool_size", "log_max_mb"):
|
||||||
|
try:
|
||||||
|
value = float(values[key].strip())
|
||||||
|
if not math.isfinite(value):
|
||||||
|
raise ValueError
|
||||||
|
config[key] = int(value) if key != "log_max_mb" else value
|
||||||
|
except (ValueError, OverflowError):
|
||||||
|
config[key] = defaults[key]
|
||||||
|
|
||||||
|
for key in ("cfproxy_user_domain", "cfproxy_worker_domain"):
|
||||||
|
if key in config:
|
||||||
|
config[key] = coerce_domain_list(config[key])
|
||||||
|
|
||||||
|
return config
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from tkinter import messagebox
|
||||||
|
|
||||||
|
from ui.settings_form import validate_config_form
|
||||||
|
from ui.i18n import set_language, t
|
||||||
|
from ui.settings import prepare_settings
|
||||||
|
|
||||||
|
log = logging.getLogger("tg-ws-tray")
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsDialog:
|
||||||
|
def __init__(
|
||||||
|
self, *, ctk, root, widgets, config, defaults, persist,
|
||||||
|
refresh_menu, finish, restart, include_autostart=False,
|
||||||
|
apply_autostart=None,
|
||||||
|
):
|
||||||
|
self.ctk = ctk
|
||||||
|
self.root = root
|
||||||
|
self.widgets = widgets
|
||||||
|
self.config = config
|
||||||
|
self.defaults = defaults
|
||||||
|
self.persist = persist
|
||||||
|
self.refresh_menu = refresh_menu
|
||||||
|
self.finish = finish
|
||||||
|
self.restart = restart
|
||||||
|
self.include_autostart = include_autostart
|
||||||
|
self.apply_autostart = apply_autostart
|
||||||
|
self.closed = False
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
if self.closed:
|
||||||
|
return
|
||||||
|
mode = self.config.get("appearance", "auto")
|
||||||
|
self.ctk.set_appearance_mode("system" if mode == "auto" else mode)
|
||||||
|
set_language(self.config.get("language", self.defaults["language"]))
|
||||||
|
self.refresh_menu()
|
||||||
|
self._finish()
|
||||||
|
|
||||||
|
def _finish(self):
|
||||||
|
self.closed = True
|
||||||
|
self.finish()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
if self.closed:
|
||||||
|
return
|
||||||
|
values = validate_config_form(
|
||||||
|
self.widgets, self.defaults, include_autostart=self.include_autostart,
|
||||||
|
)
|
||||||
|
if isinstance(values, str):
|
||||||
|
messagebox.showerror(t("app.error_title"), values, parent=self.root)
|
||||||
|
return
|
||||||
|
change = prepare_settings(self.config, values, self.defaults)
|
||||||
|
if not change.changed_keys:
|
||||||
|
self._finish()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.persist(change.config)
|
||||||
|
except (OSError, ValueError, TypeError) as exc:
|
||||||
|
log.exception("Failed to save settings")
|
||||||
|
messagebox.showerror(t("app.error_title"), str(exc), parent=self.root)
|
||||||
|
return
|
||||||
|
self.config.update(deepcopy(change.config))
|
||||||
|
set_language(change.config["language"])
|
||||||
|
log.info("Settings saved: %s", ", ".join(sorted(change.changed_keys)))
|
||||||
|
if self.apply_autostart is not None:
|
||||||
|
self.apply_autostart(bool(change.config.get("autostart", False)))
|
||||||
|
self.refresh_menu()
|
||||||
|
do_restart = change.requires_restart and messagebox.askyesno(
|
||||||
|
t("dialog.restart_title"), t("dialog.restart_body"), parent=self.root,
|
||||||
|
)
|
||||||
|
self._finish()
|
||||||
|
if do_restart:
|
||||||
|
threading.Thread(
|
||||||
|
target=self.restart, args=(deepcopy(change.config),),
|
||||||
|
daemon=True, name="proxy-restart",
|
||||||
|
).start()
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import webbrowser
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from proxy import __version__, coerce_domain_list
|
||||||
|
from proxy.balancer import balancer
|
||||||
|
from utils.update_check import RELEASES_PAGE_URL, get_status
|
||||||
|
from ui.background_task import BackgroundTask
|
||||||
|
from ui.connectivity import (
|
||||||
|
run_cfproxy_multi_test, run_cfproxy_auto_test, run_cfworker_multi_test,
|
||||||
|
show_connectivity_results, show_multi_connectivity_results,
|
||||||
|
)
|
||||||
|
from ui.ctk_controls import (
|
||||||
|
create_entry, create_checkbox, create_label, create_labeled_entry,
|
||||||
|
create_config_section, create_combobox,
|
||||||
|
)
|
||||||
|
from ui.ctk_theme import CtkTheme
|
||||||
|
from ui.ctk_tooltip import attach_ctk_tooltip, attach_tooltip_to_widgets
|
||||||
|
from ui.settings import validate_settings
|
||||||
|
from ui.i18n import (
|
||||||
|
label_from_language, language_from_label, language_option_labels,
|
||||||
|
set_language, get_language, t,
|
||||||
|
)
|
||||||
|
|
||||||
|
_INNER_W = 396
|
||||||
|
_APPEARANCE_KEYS = ("auto", "light", "dark")
|
||||||
|
_APPEARANCE_TO_CTK = {"auto": "system", "light": "Light", "dark": "Dark"}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_doc_url(doc_name: str) -> str:
|
||||||
|
lang = get_language().value
|
||||||
|
lang_folder = "EN" if lang == "en" else "RU"
|
||||||
|
return f"https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/{lang_folder}/{doc_name}.md"
|
||||||
|
|
||||||
|
|
||||||
|
def _appearance_options() -> List[str]:
|
||||||
|
return [t(f"appearance.{key}") for key in _APPEARANCE_KEYS]
|
||||||
|
|
||||||
|
|
||||||
|
def _appearance_from_cfg(value: str) -> str:
|
||||||
|
if value in _APPEARANCE_KEYS:
|
||||||
|
return t(f"appearance.{value}")
|
||||||
|
return t("appearance.auto")
|
||||||
|
|
||||||
|
|
||||||
|
def _appearance_to_cfg(label: str) -> str:
|
||||||
|
for key in _APPEARANCE_KEYS:
|
||||||
|
if t(f"appearance.{key}") == label:
|
||||||
|
return key
|
||||||
|
return "auto"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TrayConfigFormWidgets:
|
||||||
|
host_var: Any
|
||||||
|
port_var: Any
|
||||||
|
secret_var: Any
|
||||||
|
dc_textbox: Any
|
||||||
|
verbose_var: Any
|
||||||
|
no_secure_var: Any
|
||||||
|
advanced_vars: Dict[str, Any]
|
||||||
|
autostart_var: Optional[Any]
|
||||||
|
check_updates_var: Optional[Any]
|
||||||
|
cfproxy_var: Optional[Any] = None
|
||||||
|
cfproxy_user_domain_enabled_var: Optional[Any] = None
|
||||||
|
cfproxy_user_domain_var: Optional[Any] = None
|
||||||
|
cfproxy_worker_enabled_var: Optional[Any] = None
|
||||||
|
cfproxy_worker_domain_var: Optional[Any] = None
|
||||||
|
appearance_var: Optional[Any] = None
|
||||||
|
language_var: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
|
def install_tray_config_form(
|
||||||
|
ctk: Any,
|
||||||
|
frame: Any,
|
||||||
|
theme: CtkTheme,
|
||||||
|
cfg: dict,
|
||||||
|
default_config: dict,
|
||||||
|
*,
|
||||||
|
show_autostart: bool = False,
|
||||||
|
autostart_value: bool = False,
|
||||||
|
on_update_click: Optional[Callable[[], None]] = None,
|
||||||
|
) -> TrayConfigFormWidgets:
|
||||||
|
set_language(cfg.get("language", default_config["language"]))
|
||||||
|
no_secure_var = ctk.BooleanVar(master=frame, value=cfg.get("no_secure", False))
|
||||||
|
|
||||||
|
_create_header(ctk, frame, theme)
|
||||||
|
appearance_var, language_var = _create_interface(ctk, frame, theme, cfg, default_config)
|
||||||
|
host_var, port_var, secret_var = _create_connection(ctk, frame, theme, cfg, default_config)
|
||||||
|
dc_textbox = _create_dc(ctk, frame, theme, cfg, default_config)
|
||||||
|
cfproxy_var, cf_custom_cb_var, cfproxy_user_domain_var = _create_cfproxy(
|
||||||
|
ctk, frame, theme, cfg, default_config, no_secure_var,
|
||||||
|
)
|
||||||
|
cfproxy_worker_enabled_var, cfproxy_worker_domain_var = _create_cfworker(
|
||||||
|
ctk, frame, theme, cfg, default_config, no_secure_var,
|
||||||
|
)
|
||||||
|
verbose_var, advanced_vars = _create_logging(
|
||||||
|
ctk, frame, theme, cfg, default_config, no_secure_var,
|
||||||
|
)
|
||||||
|
check_updates_var = _create_updates(ctk, frame, theme, cfg, default_config, on_update_click)
|
||||||
|
autostart_var = _create_autostart(ctk, frame, theme, show_autostart, autostart_value)
|
||||||
|
|
||||||
|
return TrayConfigFormWidgets(
|
||||||
|
host_var=host_var, port_var=port_var, secret_var=secret_var,
|
||||||
|
dc_textbox=dc_textbox, verbose_var=verbose_var, no_secure_var=no_secure_var,
|
||||||
|
advanced_vars=advanced_vars,
|
||||||
|
autostart_var=autostart_var, check_updates_var=check_updates_var,
|
||||||
|
cfproxy_var=cfproxy_var,
|
||||||
|
cfproxy_user_domain_enabled_var=cf_custom_cb_var,
|
||||||
|
cfproxy_user_domain_var=cfproxy_user_domain_var,
|
||||||
|
cfproxy_worker_enabled_var=cfproxy_worker_enabled_var,
|
||||||
|
cfproxy_worker_domain_var=cfproxy_worker_domain_var,
|
||||||
|
appearance_var=appearance_var,
|
||||||
|
language_var=language_var,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_header(ctk, frame, theme):
|
||||||
|
header = ctk.CTkFrame(frame, fg_color="transparent")
|
||||||
|
header.pack(fill="x", pady=(0, 2))
|
||||||
|
ctk.CTkLabel(
|
||||||
|
header, text=t("settings.title"),
|
||||||
|
font=(theme.ui_font_family, 17, "bold"),
|
||||||
|
text_color=theme.text_primary, anchor="w",
|
||||||
|
).pack(side="left")
|
||||||
|
ctk.CTkLabel(
|
||||||
|
header, text=f"v{__version__}",
|
||||||
|
font=(theme.ui_font_family, 12),
|
||||||
|
text_color=theme.text_secondary, anchor="e",
|
||||||
|
).pack(side="right", padx=(4, 0))
|
||||||
|
|
||||||
|
ctk.CTkButton(
|
||||||
|
header, text="Donate ♥", width=90, height=28,
|
||||||
|
font=(theme.ui_font_family, 13, "bold"), corner_radius=8,
|
||||||
|
fg_color="#22c55e", hover_color="#16a34a",
|
||||||
|
text_color="#ffffff", border_width=0,
|
||||||
|
command=lambda: (
|
||||||
|
header.winfo_toplevel().iconify(),
|
||||||
|
webbrowser.open(_get_doc_url("Funding")),
|
||||||
|
),
|
||||||
|
).pack(side="right", padx=(0, 6))
|
||||||
|
|
||||||
|
|
||||||
|
def _create_interface(ctk, frame, theme, cfg, default_config):
|
||||||
|
lang_cfg = cfg.get("language", default_config["language"])
|
||||||
|
appearance_var = ctk.StringVar(
|
||||||
|
master=frame, value=_appearance_from_cfg(cfg.get("appearance", "auto"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_appearance_change(choice: str) -> None:
|
||||||
|
cfg_val = _appearance_to_cfg(choice)
|
||||||
|
ctk.set_appearance_mode(_APPEARANCE_TO_CTK[cfg_val])
|
||||||
|
|
||||||
|
ui_inner = create_config_section(ctk, frame, theme, t("section.interface"))
|
||||||
|
ui_row = ctk.CTkFrame(ui_inner, fg_color="transparent")
|
||||||
|
ui_row.pack(fill="x")
|
||||||
|
|
||||||
|
lang_col = ctk.CTkFrame(ui_row, fg_color="transparent")
|
||||||
|
lang_col.pack(side="left", fill="x", expand=True, padx=(0, 8))
|
||||||
|
|
||||||
|
theme_col = ctk.CTkFrame(ui_row, fg_color="transparent")
|
||||||
|
theme_col.pack(side="left", fill="x", expand=True, padx=(8, 0))
|
||||||
|
|
||||||
|
language_var = ctk.StringVar(master=frame, value=label_from_language(lang_cfg))
|
||||||
|
create_label(ctk, lang_col, theme, t("settings.language"), size=11).pack(
|
||||||
|
anchor="w", pady=(0, 2)
|
||||||
|
)
|
||||||
|
create_combobox(
|
||||||
|
ctk, lang_col, theme, variable=language_var,
|
||||||
|
values=[label for _, label in language_option_labels()],
|
||||||
|
).pack(fill="x")
|
||||||
|
create_label(ctk, theme_col, theme, t("settings.theme"), size=11).pack(
|
||||||
|
anchor="w", pady=(0, 2)
|
||||||
|
)
|
||||||
|
create_combobox(
|
||||||
|
ctk, theme_col, theme, variable=appearance_var,
|
||||||
|
values=_appearance_options(), command=_on_appearance_change,
|
||||||
|
).pack(fill="x")
|
||||||
|
return appearance_var, language_var
|
||||||
|
|
||||||
|
|
||||||
|
def _create_connection(ctk, frame, theme, cfg, default_config):
|
||||||
|
conn = create_config_section(ctk, frame, theme, t("section.mtproto"))
|
||||||
|
|
||||||
|
host_row = ctk.CTkFrame(conn, fg_color="transparent")
|
||||||
|
host_row.pack(fill="x")
|
||||||
|
|
||||||
|
host_col, host_var = create_labeled_entry(
|
||||||
|
ctk, host_row, theme, t("label.host"),
|
||||||
|
cfg.get("host", default_config["host"]),
|
||||||
|
tip=t("tip.host"), width=160, pack_fill=True,
|
||||||
|
)
|
||||||
|
host_col.pack(side="left", fill="x", expand=True, padx=(0, 10))
|
||||||
|
|
||||||
|
port_col, port_var = create_labeled_entry(
|
||||||
|
ctk, host_row, theme, t("label.port"),
|
||||||
|
cfg.get("port", default_config["port"]),
|
||||||
|
tip=t("tip.port"), width=100,
|
||||||
|
)
|
||||||
|
port_col.pack(side="left")
|
||||||
|
|
||||||
|
secret_row = ctk.CTkFrame(conn, fg_color="transparent")
|
||||||
|
secret_row.pack(fill="x")
|
||||||
|
|
||||||
|
secret_col, secret_var = create_labeled_entry(
|
||||||
|
ctk, secret_row, theme, t("label.secret"),
|
||||||
|
cfg.get("secret", default_config["secret"]),
|
||||||
|
tip=t("tip.secret"), width=160, pack_fill=True,
|
||||||
|
)
|
||||||
|
secret_col.pack(side="left", fill="x", expand=True, padx=(0, 10))
|
||||||
|
|
||||||
|
regen_col = ctk.CTkFrame(secret_row, fg_color="transparent")
|
||||||
|
regen_col.pack(side="left", anchor="s")
|
||||||
|
ctk.CTkLabel(regen_col, text="", font=(theme.ui_font_family, 12)).pack(pady=(0, 2))
|
||||||
|
ctk.CTkButton(
|
||||||
|
regen_col, text="↺", width=36, height=36,
|
||||||
|
font=(theme.ui_font_family, 18), corner_radius=10,
|
||||||
|
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
||||||
|
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
||||||
|
command=lambda: secret_var.set(os.urandom(16).hex()),
|
||||||
|
).pack()
|
||||||
|
return host_var, port_var, secret_var
|
||||||
|
|
||||||
|
|
||||||
|
def _create_dc(ctk, frame, theme, cfg, default_config):
|
||||||
|
dc_inner = create_config_section(ctk, frame, theme, t("section.dc"))
|
||||||
|
dc_lbl = create_label(ctk, dc_inner, theme, t("label.dc_hint"), size=11)
|
||||||
|
dc_lbl.pack(anchor="w", pady=(0, 4))
|
||||||
|
dc_textbox = ctk.CTkTextbox(
|
||||||
|
dc_inner, width=_INNER_W, height=88,
|
||||||
|
font=(theme.mono_font_family, 12), corner_radius=10,
|
||||||
|
fg_color=theme.bg, border_color=theme.field_border,
|
||||||
|
border_width=1, text_color=theme.text_primary,
|
||||||
|
)
|
||||||
|
dc_textbox.pack(fill="x")
|
||||||
|
dc_textbox.insert("1.0", "\n".join(cfg.get("dc_ip", default_config["dc_ip"])))
|
||||||
|
attach_tooltip_to_widgets([dc_lbl, dc_textbox], t("tip.dc"))
|
||||||
|
return dc_textbox
|
||||||
|
|
||||||
|
|
||||||
|
def _create_cfproxy(ctk, frame, theme, cfg, default_config, no_secure_var):
|
||||||
|
cf_inner = create_config_section(ctk, frame, theme, t("section.cfproxy"))
|
||||||
|
|
||||||
|
cf_row = ctk.CTkFrame(cf_inner, fg_color="transparent")
|
||||||
|
cf_row.pack(fill="x", pady=(0, 4))
|
||||||
|
|
||||||
|
cfproxy_var = ctk.BooleanVar(
|
||||||
|
master=frame, value=cfg.get("cfproxy", default_config.get("cfproxy", True))
|
||||||
|
)
|
||||||
|
cf_cb = create_checkbox(ctk, cf_row, theme, t("label.cf_enable"), cfproxy_var)
|
||||||
|
cf_cb.pack(side="left", padx=(0, 16))
|
||||||
|
attach_ctk_tooltip(cf_cb, t("tip.cfproxy"))
|
||||||
|
|
||||||
|
cf_test = BackgroundTask(frame.winfo_toplevel())
|
||||||
|
|
||||||
|
def _on_cf_test():
|
||||||
|
secure = not no_secure_var.get()
|
||||||
|
user_domains = (
|
||||||
|
coerce_domain_list(cfproxy_user_domain_var.get())
|
||||||
|
if cf_custom_cb_var.get() else []
|
||||||
|
)
|
||||||
|
if cf_test.running:
|
||||||
|
return
|
||||||
|
_cf_test_widget.configure(text=t("button.test_loading"), state="disabled")
|
||||||
|
|
||||||
|
def finish():
|
||||||
|
_cf_test_widget.configure(text=t("button.test"), state="normal")
|
||||||
|
|
||||||
|
if user_domains:
|
||||||
|
cf_test.start(
|
||||||
|
lambda: run_cfproxy_multi_test(user_domains, secure=secure),
|
||||||
|
lambda per: show_multi_connectivity_results(
|
||||||
|
t("connectivity.cfproxy_title"), per, label_prefix="kws",
|
||||||
|
parent=frame.winfo_toplevel(),
|
||||||
|
),
|
||||||
|
finish,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
domains = list(balancer.domains)
|
||||||
|
cf_test.start(
|
||||||
|
lambda: run_cfproxy_auto_test(domains, secure=secure),
|
||||||
|
lambda result: show_connectivity_results(
|
||||||
|
t("connectivity.cfproxy_title"), result[1],
|
||||||
|
domain=result[0] or "", auto_mode=True,
|
||||||
|
unavailable_message=t("connectivity.cf_auto_fail"),
|
||||||
|
parent=frame.winfo_toplevel(),
|
||||||
|
),
|
||||||
|
finish,
|
||||||
|
)
|
||||||
|
|
||||||
|
_cf_test_widget = ctk.CTkButton(
|
||||||
|
cf_row, text=t("button.test"), width=56, height=28,
|
||||||
|
font=(theme.ui_font_family, 13), corner_radius=8,
|
||||||
|
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
||||||
|
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
||||||
|
command=_on_cf_test,
|
||||||
|
)
|
||||||
|
_cf_test_widget.pack(side="right")
|
||||||
|
|
||||||
|
cf_custom_row = ctk.CTkFrame(cf_inner, fg_color="transparent")
|
||||||
|
cf_custom_row.pack(fill="x")
|
||||||
|
|
||||||
|
saved_user_domains = coerce_domain_list(
|
||||||
|
cfg.get("cfproxy_user_domain", default_config.get("cfproxy_user_domain", ""))
|
||||||
|
)
|
||||||
|
cf_custom_cb_var = ctk.BooleanVar(
|
||||||
|
master=frame, value=cfg.get("cfproxy_user_domain_enabled", bool(saved_user_domains))
|
||||||
|
)
|
||||||
|
cf_custom_cb = create_checkbox(ctk, cf_custom_row, theme, t("label.cf_custom_domain"), cf_custom_cb_var)
|
||||||
|
cf_custom_cb.pack(side="left", padx=(0, 10))
|
||||||
|
attach_ctk_tooltip(cf_custom_cb, t("tip.cfproxy_user_domain_cb"))
|
||||||
|
|
||||||
|
ctk.CTkButton(
|
||||||
|
cf_custom_row, text="?", width=28, height=32,
|
||||||
|
font=(theme.ui_font_family, 14), corner_radius=8,
|
||||||
|
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
||||||
|
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
||||||
|
command=lambda: webbrowser.open(_get_doc_url("CfProxy")),
|
||||||
|
).pack(side="right")
|
||||||
|
|
||||||
|
cfproxy_user_domain_var = ctk.StringVar(master=frame, value=", ".join(saved_user_domains))
|
||||||
|
cf_domain_entry = create_entry(
|
||||||
|
ctk, cf_custom_row, theme, var=cfproxy_user_domain_var,
|
||||||
|
height=32, radius=8,
|
||||||
|
)
|
||||||
|
cf_domain_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))
|
||||||
|
attach_ctk_tooltip(cf_domain_entry, t("tip.cfproxy_domain"))
|
||||||
|
|
||||||
|
def _sync_domain_entry(*_):
|
||||||
|
state = "normal" if cf_custom_cb_var.get() else "disabled"
|
||||||
|
cf_domain_entry.configure(state=state)
|
||||||
|
|
||||||
|
cf_custom_cb_var.trace_add("write", _sync_domain_entry)
|
||||||
|
_sync_domain_entry()
|
||||||
|
return cfproxy_var, cf_custom_cb_var, cfproxy_user_domain_var
|
||||||
|
|
||||||
|
|
||||||
|
def _create_cfworker(ctk, frame, theme, cfg, default_config, no_secure_var):
|
||||||
|
cf_worker_inner = create_config_section(ctk, frame, theme, t("section.cfworker"))
|
||||||
|
|
||||||
|
cf_worker_row = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
|
||||||
|
cf_worker_row.pack(fill="x", pady=(0, 4))
|
||||||
|
cf_worker_lbl = create_label(ctk, cf_worker_row, theme, t("label.cfworker_domains"), size=11)
|
||||||
|
cf_worker_lbl.pack(side="left", anchor="w", pady=(0, 2))
|
||||||
|
|
||||||
|
cf_worker_input = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
|
||||||
|
cf_worker_input.pack(fill="x")
|
||||||
|
|
||||||
|
saved_worker_domains = coerce_domain_list(
|
||||||
|
cfg.get("cfproxy_worker_domain", default_config.get("cfproxy_worker_domain", ""))
|
||||||
|
)
|
||||||
|
cfproxy_worker_enabled_var = ctk.BooleanVar(
|
||||||
|
master=frame, value=cfg.get("cfproxy_worker_enabled", bool(saved_worker_domains))
|
||||||
|
)
|
||||||
|
cf_worker_cb = create_checkbox(
|
||||||
|
ctk, cf_worker_input, theme, t("label.cf_custom_domain"),
|
||||||
|
cfproxy_worker_enabled_var,
|
||||||
|
)
|
||||||
|
cf_worker_cb.pack(side="left", padx=(0, 10))
|
||||||
|
attach_ctk_tooltip(cf_worker_cb, t("tip.cfworker_domain"))
|
||||||
|
|
||||||
|
cfproxy_worker_domain_var = ctk.StringVar(master=frame, value=", ".join(saved_worker_domains))
|
||||||
|
cf_worker_entry = create_entry(
|
||||||
|
ctk, cf_worker_input, theme, var=cfproxy_worker_domain_var,
|
||||||
|
height=32, radius=8,
|
||||||
|
)
|
||||||
|
cf_worker_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))
|
||||||
|
attach_tooltip_to_widgets([cf_worker_lbl, cf_worker_entry], t("tip.cfworker_domain"))
|
||||||
|
|
||||||
|
worker_test = BackgroundTask(frame.winfo_toplevel())
|
||||||
|
|
||||||
|
def _sync_cfworker_test_button(*_):
|
||||||
|
btn = _cfworker_test_widget
|
||||||
|
enabled = (
|
||||||
|
not worker_test.running and cfproxy_worker_enabled_var.get()
|
||||||
|
and bool(coerce_domain_list(cfproxy_worker_domain_var.get()))
|
||||||
|
)
|
||||||
|
btn.configure(state="normal" if enabled else "disabled")
|
||||||
|
|
||||||
|
def _on_cfworker_test():
|
||||||
|
secure = not no_secure_var.get()
|
||||||
|
domains = coerce_domain_list(cfproxy_worker_domain_var.get())
|
||||||
|
if worker_test.running or not cfproxy_worker_enabled_var.get() or not domains:
|
||||||
|
return
|
||||||
|
_cfworker_test_widget.configure(text=t("button.test_loading"), state="disabled")
|
||||||
|
|
||||||
|
def finish():
|
||||||
|
_cfworker_test_widget.configure(text=t("button.test"))
|
||||||
|
_sync_cfworker_test_button()
|
||||||
|
|
||||||
|
worker_test.start(
|
||||||
|
lambda: run_cfworker_multi_test(domains, secure=secure),
|
||||||
|
lambda per: show_multi_connectivity_results(
|
||||||
|
t("connectivity.cfworker_title"), per, label_prefix="DC",
|
||||||
|
parent=frame.winfo_toplevel(),
|
||||||
|
),
|
||||||
|
finish,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctk.CTkButton(
|
||||||
|
cf_worker_input, text="?", width=28, height=32,
|
||||||
|
font=(theme.ui_font_family, 14), corner_radius=8,
|
||||||
|
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
||||||
|
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
||||||
|
command=lambda: webbrowser.open(_get_doc_url("CfWorker")),
|
||||||
|
).pack(side="right")
|
||||||
|
|
||||||
|
_cfworker_test_widget = ctk.CTkButton(
|
||||||
|
cf_worker_row, text=t("button.test"), width=56, height=28,
|
||||||
|
font=(theme.ui_font_family, 13), corner_radius=8,
|
||||||
|
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
||||||
|
text_color="#ffffff", border_width=1, border_color=theme.field_border,
|
||||||
|
command=_on_cfworker_test,
|
||||||
|
)
|
||||||
|
_cfworker_test_widget.pack(side="right")
|
||||||
|
|
||||||
|
def _sync_cfworker_entry(*_):
|
||||||
|
state = "normal" if cfproxy_worker_enabled_var.get() else "disabled"
|
||||||
|
cf_worker_entry.configure(state=state)
|
||||||
|
_sync_cfworker_test_button()
|
||||||
|
|
||||||
|
cfproxy_worker_enabled_var.trace_add("write", _sync_cfworker_entry)
|
||||||
|
cfproxy_worker_domain_var.trace_add("write", _sync_cfworker_test_button)
|
||||||
|
_sync_cfworker_entry()
|
||||||
|
return cfproxy_worker_enabled_var, cfproxy_worker_domain_var
|
||||||
|
|
||||||
|
|
||||||
|
def _create_logging(ctk, frame, theme, cfg, default_config, no_secure_var):
|
||||||
|
log_inner = create_config_section(ctk, frame, theme, t("section.logs"))
|
||||||
|
|
||||||
|
verbose_var = ctk.BooleanVar(master=frame, value=cfg.get("verbose", False))
|
||||||
|
verbose_cb = create_checkbox(ctk, log_inner, theme, t("label.verbose"), verbose_var)
|
||||||
|
verbose_cb.pack(anchor="w", pady=(0, 6))
|
||||||
|
attach_ctk_tooltip(verbose_cb, t("tip.verbose"))
|
||||||
|
|
||||||
|
no_secure_cb = create_checkbox(ctk, log_inner, theme, t("label.no_secure"), no_secure_var)
|
||||||
|
no_secure_cb.pack(anchor="w", pady=(0, 6))
|
||||||
|
attach_ctk_tooltip(no_secure_cb, t("tip.no_secure"))
|
||||||
|
|
||||||
|
adv_frame = ctk.CTkFrame(log_inner, fg_color="transparent")
|
||||||
|
adv_frame.pack(fill="x")
|
||||||
|
|
||||||
|
adv_rows = [
|
||||||
|
(t("label.buf_kb"), "buf_kb", t("tip.buf_kb")),
|
||||||
|
(t("label.pool_size"), "pool_size", t("tip.pool")),
|
||||||
|
(t("label.log_max_mb"), "log_max_mb", t("tip.log_mb")),
|
||||||
|
]
|
||||||
|
advanced_vars = {}
|
||||||
|
for label_text, key, tip in adv_rows:
|
||||||
|
col = ctk.CTkFrame(adv_frame, fg_color="transparent")
|
||||||
|
col.pack(fill="x", pady=(0, 0 if key == "log_max_mb" else 5))
|
||||||
|
adv_l = create_label(ctk, col, theme, label_text, size=11)
|
||||||
|
adv_l.pack(anchor="w", pady=(0, 2))
|
||||||
|
advanced_vars[key] = ctk.StringVar(master=frame, value=str(cfg.get(key, default_config[key])))
|
||||||
|
adv_e = create_entry(
|
||||||
|
ctk, col, theme, width=_INNER_W, height=32, radius=8,
|
||||||
|
var=advanced_vars[key],
|
||||||
|
)
|
||||||
|
adv_e.pack(fill="x")
|
||||||
|
attach_tooltip_to_widgets([adv_l, adv_e, col], tip)
|
||||||
|
return verbose_var, advanced_vars
|
||||||
|
|
||||||
|
|
||||||
|
def _create_updates(ctk, frame, theme, cfg, default_config, on_update_click):
|
||||||
|
upd_inner = create_config_section(ctk, frame, theme, t("section.updates"))
|
||||||
|
st = get_status()
|
||||||
|
check_updates_var = ctk.BooleanVar(
|
||||||
|
master=frame, value=bool(cfg.get("check_updates", default_config.get("check_updates", True)))
|
||||||
|
)
|
||||||
|
upd_cb = create_checkbox(ctk, upd_inner, theme, t("label.check_updates"), check_updates_var)
|
||||||
|
upd_cb.pack(anchor="w", pady=(0, 6))
|
||||||
|
attach_ctk_tooltip(upd_cb, t("tip.check_updates"))
|
||||||
|
|
||||||
|
if st.get("error"):
|
||||||
|
upd_status = t("updates.status_error")
|
||||||
|
elif not st.get("checked"):
|
||||||
|
upd_status = t("updates.status_pending")
|
||||||
|
elif st.get("has_update") and st.get("latest"):
|
||||||
|
upd_status = t("updates.status_available", latest=st["latest"], current=__version__)
|
||||||
|
elif st.get("ahead_of_release") and st.get("latest"):
|
||||||
|
upd_status = t("updates.status_ahead", current=__version__, latest=st["latest"])
|
||||||
|
else:
|
||||||
|
upd_status = t("updates.status_latest")
|
||||||
|
|
||||||
|
create_label(ctk, upd_inner, theme, upd_status, size=11,
|
||||||
|
justify="left", wraplength=_INNER_W).pack(anchor="w", pady=(0, 8))
|
||||||
|
|
||||||
|
rel_url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||||
|
if st.get("has_update") and on_update_click is not None:
|
||||||
|
upd_btn_row = ctk.CTkFrame(upd_inner, fg_color="transparent")
|
||||||
|
upd_btn_row.pack(fill="x")
|
||||||
|
upd_btn_row.grid_columnconfigure(0, weight=1)
|
||||||
|
upd_btn_row.grid_columnconfigure(1, weight=1)
|
||||||
|
ctk.CTkButton(
|
||||||
|
upd_btn_row, text=t("button.open_release"), height=32,
|
||||||
|
font=(theme.ui_font_family, 13), corner_radius=8,
|
||||||
|
fg_color=theme.field_bg, hover_color=theme.field_border,
|
||||||
|
text_color=theme.text_primary, border_width=1,
|
||||||
|
border_color=theme.field_border,
|
||||||
|
command=lambda u=rel_url: webbrowser.open(u),
|
||||||
|
).grid(row=0, column=0, sticky="ew", padx=(0, 4))
|
||||||
|
ctk.CTkButton(
|
||||||
|
upd_btn_row, text=t("button.update"), height=32,
|
||||||
|
font=(theme.ui_font_family, 13, "bold"), corner_radius=8,
|
||||||
|
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
|
||||||
|
text_color="#ffffff",
|
||||||
|
command=on_update_click,
|
||||||
|
).grid(row=0, column=1, sticky="ew", padx=(4, 0))
|
||||||
|
else:
|
||||||
|
ctk.CTkButton(
|
||||||
|
upd_inner, text=t("button.open_release"), height=32,
|
||||||
|
font=(theme.ui_font_family, 13), corner_radius=8,
|
||||||
|
fg_color=theme.field_bg, hover_color=theme.field_border,
|
||||||
|
text_color=theme.text_primary, border_width=1,
|
||||||
|
border_color=theme.field_border,
|
||||||
|
command=lambda u=rel_url: webbrowser.open(u),
|
||||||
|
).pack(anchor="w")
|
||||||
|
return check_updates_var
|
||||||
|
|
||||||
|
|
||||||
|
def _create_autostart(ctk, frame, theme, show_autostart, autostart_value):
|
||||||
|
autostart_var = None
|
||||||
|
if show_autostart:
|
||||||
|
sys_inner = create_config_section(ctk, frame, theme, t("section.windows_startup"), bottom_spacer=4)
|
||||||
|
autostart_var = ctk.BooleanVar(master=frame, value=autostart_value)
|
||||||
|
as_cb = create_checkbox(ctk, sys_inner, theme, t("label.autostart"), autostart_var)
|
||||||
|
as_cb.pack(anchor="w", pady=(0, 4))
|
||||||
|
as_hint = create_label(
|
||||||
|
ctk, sys_inner, theme,
|
||||||
|
t("label.autostart_hint"),
|
||||||
|
size=11, justify="left", wraplength=_INNER_W,
|
||||||
|
)
|
||||||
|
as_hint.pack(anchor="w")
|
||||||
|
attach_tooltip_to_widgets([as_cb, as_hint], t("tip.autostart"))
|
||||||
|
return autostart_var
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config_form(
|
||||||
|
widgets: TrayConfigFormWidgets,
|
||||||
|
default_config: dict,
|
||||||
|
*,
|
||||||
|
include_autostart: bool,
|
||||||
|
) -> Union[dict, str]:
|
||||||
|
values = {
|
||||||
|
"host": widgets.host_var.get(),
|
||||||
|
"port": widgets.port_var.get(),
|
||||||
|
"secret": widgets.secret_var.get(),
|
||||||
|
"dc_ip": widgets.dc_textbox.get("1.0", "end"),
|
||||||
|
"verbose": bool(widgets.verbose_var.get()),
|
||||||
|
}
|
||||||
|
for key, var in widgets.advanced_vars.items():
|
||||||
|
values[key] = var.get()
|
||||||
|
for key in (
|
||||||
|
"check_updates", "cfproxy", "cfproxy_user_domain_enabled",
|
||||||
|
"cfproxy_worker_enabled", "no_secure",
|
||||||
|
):
|
||||||
|
var = getattr(widgets, f"{key}_var")
|
||||||
|
if var is not None:
|
||||||
|
values[key] = bool(var.get())
|
||||||
|
for key in ("cfproxy_user_domain", "cfproxy_worker_domain"):
|
||||||
|
var = getattr(widgets, f"{key}_var")
|
||||||
|
if var is not None:
|
||||||
|
values[key] = var.get()
|
||||||
|
if include_autostart:
|
||||||
|
values["autostart"] = (
|
||||||
|
bool(widgets.autostart_var.get())
|
||||||
|
if widgets.autostart_var is not None else False
|
||||||
|
)
|
||||||
|
if widgets.appearance_var is not None:
|
||||||
|
values["appearance"] = _appearance_to_cfg(widgets.appearance_var.get())
|
||||||
|
if widgets.language_var is not None:
|
||||||
|
values["language"] = language_from_label(widgets.language_var.get()).value
|
||||||
|
return validate_settings(values, default_config)
|
||||||
|
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
from ui.i18n import detect_system_language
|
from ui.i18n import detect_system_language
|
||||||
|
|
||||||
@@ -26,11 +27,12 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
|
|||||||
"cfproxy_worker_domain": [],
|
"cfproxy_worker_domain": [],
|
||||||
"force_test_dc": False,
|
"force_test_dc": False,
|
||||||
"no_secure": False,
|
"no_secure": False,
|
||||||
|
"appearance": "auto",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def default_tray_config() -> Dict[str, Any]:
|
def default_tray_config() -> Dict[str, Any]:
|
||||||
cfg = dict(_TRAY_DEFAULTS_COMMON)
|
cfg = deepcopy(_TRAY_DEFAULTS_COMMON)
|
||||||
cfg["secret"] = os.urandom(16).hex()
|
cfg["secret"] = os.urandom(16).hex()
|
||||||
cfg["language"] = detect_system_language().value
|
cfg["language"] = detect_system_language().value
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, Optional, Tuple
|
from typing import Any, Callable, Dict, Optional, Tuple
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
@@ -211,7 +212,7 @@ def load_config() -> dict:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.warning("Failed to load config: %s", repr(exc))
|
log.warning("Failed to load config: %s", repr(exc))
|
||||||
if cfg is None:
|
if cfg is None:
|
||||||
cfg = dict(DEFAULT_CONFIG)
|
cfg = deepcopy(DEFAULT_CONFIG)
|
||||||
_apply_ui_language(cfg)
|
_apply_ui_language(cfg)
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|||||||
+11
-57
@@ -53,13 +53,13 @@ from utils.update_check import (
|
|||||||
from ui.ctk_tray_ui import (
|
from ui.ctk_tray_ui import (
|
||||||
install_tray_config_buttons, install_tray_config_form,
|
install_tray_config_buttons, install_tray_config_form,
|
||||||
populate_first_run_window, tray_settings_scroll_and_footer,
|
populate_first_run_window, tray_settings_scroll_and_footer,
|
||||||
validate_config_form,
|
|
||||||
)
|
)
|
||||||
from ui.ctk_theme import (
|
from ui.ctk_theme import (
|
||||||
CONFIG_DIALOG_FRAME_PAD, CONFIG_DIALOG_SIZE, FIRST_RUN_SIZE,
|
CONFIG_DIALOG_FRAME_PAD, CONFIG_DIALOG_SIZE, FIRST_RUN_SIZE,
|
||||||
create_ctk_toplevel, ctk_theme_for_platform, main_content_frame,
|
create_ctk_toplevel, ctk_theme_for_platform, main_content_frame,
|
||||||
)
|
)
|
||||||
from ui.i18n import set_language, t
|
from ui.i18n import t
|
||||||
|
from ui.settings_dialog import SettingsDialog
|
||||||
|
|
||||||
_tray_icon: Optional[object] = None
|
_tray_icon: Optional[object] = None
|
||||||
_config: dict = {}
|
_config: dict = {}
|
||||||
@@ -500,8 +500,6 @@ def _edit_config_dialog() -> None:
|
|||||||
|
|
||||||
cfg = dict(_config)
|
cfg = dict(_config)
|
||||||
cfg["autostart"] = is_autostart_enabled()
|
cfg["autostart"] = is_autostart_enabled()
|
||||||
if _supports_autostart() and not cfg["autostart"]:
|
|
||||||
set_autostart_enabled(False)
|
|
||||||
|
|
||||||
def _build(done: threading.Event) -> None:
|
def _build(done: threading.Event) -> None:
|
||||||
theme = ctk_theme_for_platform()
|
theme = ctk_theme_for_platform()
|
||||||
@@ -521,72 +519,28 @@ def _edit_config_dialog() -> None:
|
|||||||
if _tray_icon is not None:
|
if _tray_icon is not None:
|
||||||
_tray_icon.menu = _build_menu()
|
_tray_icon.menu = _build_menu()
|
||||||
|
|
||||||
_original_language = _config.get("language", DEFAULT_CONFIG["language"])
|
|
||||||
|
|
||||||
widgets = install_tray_config_form(
|
widgets = install_tray_config_form(
|
||||||
ctk, scroll, theme, cfg, DEFAULT_CONFIG,
|
ctk, scroll, theme, cfg, DEFAULT_CONFIG,
|
||||||
show_autostart=_supports_autostart(),
|
show_autostart=_supports_autostart(),
|
||||||
autostart_value=cfg.get("autostart", False),
|
autostart_value=cfg.get("autostart", False),
|
||||||
on_language_change=_refresh_tray_menu,
|
|
||||||
on_update_click=_trigger_update_flow,
|
on_update_click=_trigger_update_flow,
|
||||||
)
|
)
|
||||||
|
|
||||||
_original_appearance = ctk.get_appearance_mode()
|
|
||||||
|
|
||||||
def _restore_ui_locale() -> None:
|
|
||||||
set_language(_original_language)
|
|
||||||
_refresh_tray_menu()
|
|
||||||
|
|
||||||
def _finish() -> None:
|
def _finish() -> None:
|
||||||
root.destroy()
|
root.destroy()
|
||||||
done.set()
|
done.set()
|
||||||
|
|
||||||
def _cancel() -> None:
|
dialog = SettingsDialog(
|
||||||
ctk.set_appearance_mode(_original_appearance)
|
ctk=ctk, root=root, widgets=widgets, config=_config,
|
||||||
_restore_ui_locale()
|
defaults=DEFAULT_CONFIG, persist=save_config,
|
||||||
_finish()
|
refresh_menu=_refresh_tray_menu, finish=_finish,
|
||||||
|
restart=lambda config: restart_proxy(config, _show_error),
|
||||||
def on_save() -> None:
|
include_autostart=_supports_autostart(),
|
||||||
from tkinter import messagebox
|
apply_autostart=set_autostart_enabled if _supports_autostart() else None,
|
||||||
merged = validate_config_form(widgets, DEFAULT_CONFIG, include_autostart=_supports_autostart())
|
|
||||||
if isinstance(merged, str):
|
|
||||||
messagebox.showerror(t("app.error_title"), merged, parent=root)
|
|
||||||
return
|
|
||||||
|
|
||||||
merged["force_test_dc"] = _config.get("force_test_dc", DEFAULT_CONFIG["force_test_dc"])
|
|
||||||
|
|
||||||
_ui_only_keys = {"appearance", "autostart", "check_updates", "language"}
|
|
||||||
config_changed = any(merged.get(k) != cfg.get(k) for k in merged)
|
|
||||||
proxy_changed = any(merged.get(k) != _config.get(k) for k in merged if k not in _ui_only_keys)
|
|
||||||
|
|
||||||
if not config_changed:
|
|
||||||
_restore_ui_locale()
|
|
||||||
_finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
save_config(merged)
|
|
||||||
_config.update(merged)
|
|
||||||
set_language(merged.get("language", DEFAULT_CONFIG["language"]))
|
|
||||||
log.info("Config saved: %s", merged)
|
|
||||||
if _supports_autostart():
|
|
||||||
set_autostart_enabled(bool(merged.get("autostart", False)))
|
|
||||||
_tray_icon.menu = _build_menu()
|
|
||||||
|
|
||||||
if not proxy_changed:
|
|
||||||
_finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
do_restart = messagebox.askyesno(
|
|
||||||
t("dialog.restart_title"),
|
|
||||||
t("dialog.restart_body"),
|
|
||||||
parent=root,
|
|
||||||
)
|
)
|
||||||
_finish()
|
|
||||||
if do_restart:
|
|
||||||
threading.Thread(target=lambda: restart_proxy(_config, _show_error), daemon=True).start()
|
|
||||||
|
|
||||||
root.protocol("WM_DELETE_WINDOW", _cancel)
|
root.protocol("WM_DELETE_WINDOW", dialog.cancel)
|
||||||
install_tray_config_buttons(ctk, footer, theme, on_save=on_save, on_cancel=_cancel)
|
install_tray_config_buttons(ctk, footer, theme, on_save=dialog.save, on_cancel=dialog.cancel)
|
||||||
|
|
||||||
ctk_run_dialog(_build)
|
ctk_run_dialog(_build)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user