feat(tray): версия в логах и настройках, UI настроек, фикс Tk при выходе

Реализует предложение из issue #430: версия в логах при старте, в окне настроек (CTk) и в меню macOS; прокрутка и вёрстка; защита Variable.__del__ и destroy корня окна.

Refs: https://github.com/Flowseal/tg-ws-proxy/issues/430
This commit is contained in:
deexsed 2026-03-26 10:29:10 +03:00
parent 86794f34a9
commit 35ea33ee3f
5 changed files with 298 additions and 75 deletions

View File

@ -19,10 +19,12 @@ import pystray
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy import __version__
from ui.ctk_tray_ui import ( from ui.ctk_tray_ui import (
install_tray_config_buttons, install_tray_config_buttons,
install_tray_config_form, install_tray_config_form,
populate_first_run_window, populate_first_run_window,
tray_settings_scroll_and_footer,
validate_config_form, validate_config_form,
) )
from ui.ctk_theme import ( from ui.ctk_theme import (
@ -395,8 +397,10 @@ def _edit_config_dialog():
fpx, fpy = CONFIG_DIALOG_FRAME_PAD fpx, fpy = CONFIG_DIALOG_FRAME_PAD
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy) frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
widgets = install_tray_config_form( widgets = install_tray_config_form(
ctk, frame, theme, cfg, DEFAULT_CONFIG, ctk, scroll, theme, cfg, DEFAULT_CONFIG,
show_autostart=False, show_autostart=False,
) )
@ -430,9 +434,17 @@ def _edit_config_dialog():
root.destroy() root.destroy()
install_tray_config_buttons( install_tray_config_buttons(
ctk, frame, theme, on_save=on_save, on_cancel=on_cancel) ctk, footer, theme, on_save=on_save, on_cancel=on_cancel)
root.mainloop() try:
root.mainloop()
finally:
import tkinter as tk
try:
if root.winfo_exists():
root.destroy()
except tk.TclError:
pass
def _on_open_logs(icon=None, item=None): def _on_open_logs(icon=None, item=None):
@ -506,7 +518,15 @@ def _show_first_run():
populate_first_run_window( populate_first_run_window(
ctk, root, theme, host=host, port=port, on_done=on_done) ctk, root, theme, host=host, port=port, on_done=on_done)
root.mainloop() try:
root.mainloop()
finally:
import tkinter as tk
try:
if root.winfo_exists():
root.destroy()
except tk.TclError:
pass
def _has_ipv6_enabled() -> bool: def _has_ipv6_enabled() -> bool:
@ -588,7 +608,7 @@ def run_tray():
setup_logging(_config.get("verbose", False), setup_logging(_config.get("verbose", False),
log_max_mb=_config.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])) log_max_mb=_config.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"]))
log.info("TG WS Proxy tray app starting") log.info("TG WS Proxy версия %s, tray app starting", __version__)
log.info("Config: %s", _config) log.info("Config: %s", _config)
log.info("Log file: %s", LOG_FILE) log.info("Log file: %s", LOG_FILE)

View File

@ -30,6 +30,7 @@ except ImportError:
pyperclip = None pyperclip = None
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy import __version__
APP_NAME = "TgWsProxy" APP_NAME = "TgWsProxy"
APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME
@ -616,6 +617,9 @@ class TgWsProxyApp(_TgWsProxyAppBase):
self._logs_item = rumps.MenuItem( self._logs_item = rumps.MenuItem(
"Открыть логи", "Открыть логи",
callback=_on_open_logs) callback=_on_open_logs)
self._version_item = rumps.MenuItem(
f"Версия {__version__}",
callback=lambda _: None)
super().__init__( super().__init__(
"TG WS Proxy", "TG WS Proxy",
@ -628,6 +632,8 @@ class TgWsProxyApp(_TgWsProxyAppBase):
self._restart_item, self._restart_item,
self._settings_item, self._settings_item,
self._logs_item, self._logs_item,
None,
self._version_item,
]) ])
def update_menu_title(self): def update_menu_title(self):
@ -651,7 +657,7 @@ def run_menubar():
setup_logging(_config.get("verbose", False), setup_logging(_config.get("verbose", False),
log_max_mb=_config.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])) log_max_mb=_config.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"]))
log.info("TG WS Proxy menubar app starting") log.info("TG WS Proxy версия %s, menubar app starting", __version__)
log.info("Config: %s", _config) log.info("Config: %s", _config)
log.info("Log file: %s", LOG_FILE) log.info("Log file: %s", LOG_FILE)

View File

@ -6,12 +6,35 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
import tkinter
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Callable, Optional, Tuple from typing import Any, Callable, Optional, Tuple
_tk_variable_del_guard_installed = False
def _install_tkinter_variable_del_guard() -> None:
"""
Убирает «Exception ignored» при выходе процесса: Tcl уже разрушен, а GC ещё
вызывает Variable.__del__ (StringVar и т.д.) напр. окно CTk в фоновом потоке.
"""
global _tk_variable_del_guard_installed
if _tk_variable_del_guard_installed:
return
_orig = tkinter.Variable.__del__
def _safe_variable_del(self: Any, _orig: Any = _orig) -> None:
try:
_orig(self)
except (RuntimeError, tkinter.TclError):
pass
tkinter.Variable.__del__ = _safe_variable_del # type: ignore[assignment]
_tk_variable_del_guard_installed = True
# Размеры и отступы (единые для диалогов настроек и первого запуска) # Размеры и отступы (единые для диалогов настроек и первого запуска)
CONFIG_DIALOG_SIZE: Tuple[int, int] = (420, 540) CONFIG_DIALOG_SIZE: Tuple[int, int] = (460, 560)
CONFIG_DIALOG_FRAME_PAD: Tuple[int, int] = (24, 20) CONFIG_DIALOG_FRAME_PAD: Tuple[int, int] = (20, 14)
FIRST_RUN_SIZE: Tuple[int, int] = (520, 440) FIRST_RUN_SIZE: Tuple[int, int] = (520, 440)
FIRST_RUN_FRAME_PAD: Tuple[int, int] = (28, 24) FIRST_RUN_FRAME_PAD: Tuple[int, int] = (28, 24)
@ -62,6 +85,7 @@ def create_ctk_root(
Создаёт CTk: глобальная тема, заголовок, без ресайза, по центру экрана, фон из палитры. Создаёт CTk: глобальная тема, заголовок, без ресайза, по центру экрана, фон из палитры.
after_create опционально: установка иконки окна (различается по ОС). after_create опционально: установка иконки окна (различается по ОС).
""" """
_install_tkinter_variable_del_guard()
apply_ctk_appearance(ctk) apply_ctk_appearance(ctk)
root = ctk.CTk() root = ctk.CTk()
root.title(title) root.title(title)

View File

@ -9,6 +9,7 @@ from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, Union from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy import __version__
from ui.ctk_theme import ( from ui.ctk_theme import (
FIRST_RUN_FRAME_PAD, FIRST_RUN_FRAME_PAD,
@ -54,6 +55,62 @@ _TIP_AUTOSTART = (
_TIP_SAVE = "Сохранить настройки в файл. После сохранения можно перезапустить прокси." _TIP_SAVE = "Сохранить настройки в файл. После сохранения можно перезапустить прокси."
_TIP_CANCEL = "Закрыть окно без сохранения изменений." _TIP_CANCEL = "Закрыть окно без сохранения изменений."
# Внутренняя ширина полей относительно ширины окна настроек (см. CONFIG_DIALOG_SIZE)
_CONFIG_FORM_INNER_WIDTH = 396
def tray_settings_scroll_and_footer(
ctk: Any,
content_parent: Any,
theme: CtkTheme,
) -> Tuple[Any, Any]:
"""
Нижняя панель под кнопки и прокручиваемая область для формы (форма не обрезает кнопки).
Возвращает (scroll_frame, footer_frame).
"""
footer = ctk.CTkFrame(content_parent, fg_color=theme.bg)
footer.pack(side="bottom", fill="x")
scroll = ctk.CTkScrollableFrame(
content_parent,
fg_color=theme.bg,
corner_radius=0,
scrollbar_button_color=theme.field_border,
scrollbar_button_hover_color=theme.text_secondary,
)
scroll.pack(fill="both", expand=True)
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))
ctk.CTkLabel(
wrap,
text=title,
font=(theme.ui_font_family, 12, "bold"),
text_color=theme.text_primary,
anchor="w",
).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 @dataclass
class TrayConfigFormWidgets: class TrayConfigFormWidgets:
@ -77,80 +134,158 @@ def install_tray_config_form(
autostart_value: bool = False, autostart_value: bool = False,
) -> TrayConfigFormWidgets: ) -> TrayConfigFormWidgets:
"""Поля настроек прокси внутри уже созданного `frame`.""" """Поля настроек прокси внутри уже созданного `frame`."""
host_lbl = ctk.CTkLabel(frame, text="IP-адрес прокси", header = ctk.CTkFrame(frame, fg_color="transparent")
font=(theme.ui_font_family, 13), header.pack(fill="x", pady=(0, 2))
text_color=theme.text_primary, anchor="w") ctk.CTkLabel(
host_lbl.pack(anchor="w", pady=(0, 4)) header,
text="Настройки прокси",
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")
inner_w = _CONFIG_FORM_INNER_WIDTH
conn = _config_section(ctk, frame, theme, "Подключение SOCKS5")
host_row = ctk.CTkFrame(conn, fg_color="transparent")
host_row.pack(fill="x")
host_col = ctk.CTkFrame(host_row, fg_color="transparent")
host_col.pack(side="left", fill="x", expand=True, padx=(0, 10))
host_lbl = ctk.CTkLabel(
host_col,
text="IP-адрес",
font=(theme.ui_font_family, 12),
text_color=theme.text_secondary,
anchor="w",
)
host_lbl.pack(anchor="w", pady=(0, 2))
host_var = ctk.StringVar(value=cfg.get("host", default_config["host"])) host_var = ctk.StringVar(value=cfg.get("host", default_config["host"]))
host_entry = ctk.CTkEntry( host_entry = ctk.CTkEntry(
frame, textvariable=host_var, width=200, height=36, host_col,
font=(theme.ui_font_family, 13), corner_radius=10, textvariable=host_var,
fg_color=theme.field_bg, border_color=theme.field_border, width=160,
border_width=1, text_color=theme.text_primary) height=36,
host_entry.pack(anchor="w", pady=(0, 12)) font=(theme.ui_font_family, 13),
attach_tooltip_to_widgets([host_lbl, host_entry], _TIP_HOST) corner_radius=10,
fg_color=theme.bg,
border_color=theme.field_border,
border_width=1,
text_color=theme.text_primary,
)
host_entry.pack(fill="x", pady=(0, 0))
attach_tooltip_to_widgets([host_lbl, host_entry, host_col], _TIP_HOST)
port_lbl = ctk.CTkLabel(frame, text="Порт прокси", port_col = ctk.CTkFrame(host_row, fg_color="transparent")
font=(theme.ui_font_family, 13), port_col.pack(side="left")
text_color=theme.text_primary, anchor="w") port_lbl = ctk.CTkLabel(
port_lbl.pack(anchor="w", pady=(0, 4)) port_col,
text="Порт",
font=(theme.ui_font_family, 12),
text_color=theme.text_secondary,
anchor="w",
)
port_lbl.pack(anchor="w", pady=(0, 2))
port_var = ctk.StringVar(value=str(cfg.get("port", default_config["port"]))) port_var = ctk.StringVar(value=str(cfg.get("port", default_config["port"])))
port_entry = ctk.CTkEntry( port_entry = ctk.CTkEntry(
frame, textvariable=port_var, width=120, height=36, port_col,
font=(theme.ui_font_family, 13), corner_radius=10, textvariable=port_var,
fg_color=theme.field_bg, border_color=theme.field_border, width=100,
border_width=1, text_color=theme.text_primary) height=36,
port_entry.pack(anchor="w", pady=(0, 12)) font=(theme.ui_font_family, 13),
attach_tooltip_to_widgets([port_lbl, port_entry], _TIP_PORT) corner_radius=10,
fg_color=theme.bg,
border_color=theme.field_border,
border_width=1,
text_color=theme.text_primary,
)
port_entry.pack(anchor="w")
attach_tooltip_to_widgets([port_lbl, port_entry, port_col], _TIP_PORT)
dc_inner = _config_section(ctk, frame, theme, "Датацентры Telegram (DC → IP)")
dc_lbl = ctk.CTkLabel( dc_lbl = ctk.CTkLabel(
frame, text="DC → IP маппинги (по одному на строку, формат DC:IP)", dc_inner,
font=(theme.ui_font_family, 13), text_color=theme.text_primary, text="По одному правилу на строку, формат: номер:IP",
anchor="w") font=(theme.ui_font_family, 11),
text_color=theme.text_secondary,
anchor="w",
)
dc_lbl.pack(anchor="w", pady=(0, 4)) dc_lbl.pack(anchor="w", pady=(0, 4))
dc_textbox = ctk.CTkTextbox( dc_textbox = ctk.CTkTextbox(
frame, width=370, height=120, dc_inner,
font=(theme.mono_font_family, 12), corner_radius=10, width=inner_w,
fg_color=theme.field_bg, border_color=theme.field_border, height=88,
border_width=1, text_color=theme.text_primary) font=(theme.mono_font_family, 12),
dc_textbox.pack(anchor="w", pady=(0, 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"]))) dc_textbox.insert("1.0", "\n".join(cfg.get("dc_ip", default_config["dc_ip"])))
attach_tooltip_to_widgets([dc_lbl, dc_textbox], _TIP_DC) attach_tooltip_to_widgets([dc_lbl, dc_textbox], _TIP_DC)
log_inner = _config_section(ctk, frame, theme, "Логи и производительность")
verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False)) verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False))
verbose_cb = ctk.CTkCheckBox( verbose_cb = ctk.CTkCheckBox(
frame, text="Подробное логирование (verbose)", log_inner,
variable=verbose_var, font=(theme.ui_font_family, 13), text="Подробное логирование (verbose)",
variable=verbose_var,
font=(theme.ui_font_family, 13),
text_color=theme.text_primary, text_color=theme.text_primary,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover, fg_color=theme.tg_blue,
corner_radius=6, border_width=2, hover_color=theme.tg_blue_hover,
border_color=theme.field_border) corner_radius=6,
verbose_cb.pack(anchor="w", pady=(0, 8)) border_width=2,
border_color=theme.field_border,
)
verbose_cb.pack(anchor="w", pady=(0, 6))
attach_ctk_tooltip(verbose_cb, _TIP_VERBOSE) attach_ctk_tooltip(verbose_cb, _TIP_VERBOSE)
adv_frame = ctk.CTkFrame(frame, fg_color="transparent") adv_frame = ctk.CTkFrame(log_inner, fg_color="transparent")
adv_frame.pack(anchor="w", fill="x", pady=(4, 8)) adv_frame.pack(fill="x")
adv_rows = [ adv_rows = [
("Буфер (KB, 256 default)", "buf_kb", 120, _TIP_BUF_KB), ("Буфер, КБ (по умолчанию 256)", "buf_kb", _TIP_BUF_KB),
("WS пулов (4 default)", "pool_size", 120, _TIP_POOL), ("Пул WebSocket-сессий (по умолчанию 4)", "pool_size", _TIP_POOL),
("Log size (MB, 5 def)", "log_max_mb", 120, _TIP_LOG_MB), ("Макс. размер лога, МБ (по умолчанию 5)", "log_max_mb", _TIP_LOG_MB),
] ]
for lbl, key, w_, tip in adv_rows: for lbl, key, tip in adv_rows:
col_frame = ctk.CTkFrame(adv_frame, fg_color="transparent") col_frame = ctk.CTkFrame(adv_frame, fg_color="transparent")
col_frame.pack(side="left", padx=(0, 10)) col_frame.pack(fill="x", pady=(0, 0 if key == "log_max_mb" else 5))
adv_l = ctk.CTkLabel(col_frame, text=lbl, font=(theme.ui_font_family, 11), adv_l = ctk.CTkLabel(
text_color=theme.text_secondary, anchor="w") col_frame,
adv_l.pack(anchor="w") text=lbl,
font=(theme.ui_font_family, 11),
text_color=theme.text_secondary,
anchor="w",
)
adv_l.pack(anchor="w", pady=(0, 2))
adv_e = ctk.CTkEntry( adv_e = ctk.CTkEntry(
col_frame, width=w_, height=30, font=(theme.ui_font_family, 12), col_frame,
corner_radius=8, fg_color=theme.field_bg, width=inner_w,
border_color=theme.field_border, border_width=1, height=32,
font=(theme.ui_font_family, 13),
corner_radius=8,
fg_color=theme.bg,
border_color=theme.field_border,
border_width=1,
text_color=theme.text_primary, text_color=theme.text_primary,
textvariable=ctk.StringVar( textvariable=ctk.StringVar(
value=str(cfg.get(key, default_config[key])) value=str(cfg.get(key, default_config[key]))
)) ),
adv_e.pack(anchor="w") )
adv_e.pack(fill="x")
attach_tooltip_to_widgets([adv_l, adv_e, col_frame], tip) attach_tooltip_to_widgets([adv_l, adv_e, col_frame], tip)
adv_entries = list(adv_frame.winfo_children()) adv_entries = list(adv_frame.winfo_children())
@ -158,21 +293,33 @@ def install_tray_config_form(
autostart_var = None autostart_var = None
if show_autostart: if show_autostart:
sys_inner = _config_section(
ctk, frame, theme, "Запуск Windows", bottom_spacer=4
)
autostart_var = ctk.BooleanVar(value=autostart_value) autostart_var = ctk.BooleanVar(value=autostart_value)
as_cb = ctk.CTkCheckBox( as_cb = ctk.CTkCheckBox(
frame, text="Автозапуск при включении Windows", sys_inner,
variable=autostart_var, font=(theme.ui_font_family, 13), text="Автозапуск при включении компьютера",
variable=autostart_var,
font=(theme.ui_font_family, 13),
text_color=theme.text_primary, text_color=theme.text_primary,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover, fg_color=theme.tg_blue,
corner_radius=6, border_width=2, hover_color=theme.tg_blue_hover,
border_color=theme.field_border) corner_radius=6,
as_cb.pack(anchor="w", pady=(0, 8)) border_width=2,
border_color=theme.field_border,
)
as_cb.pack(anchor="w", pady=(0, 4))
as_hint = ctk.CTkLabel( as_hint = ctk.CTkLabel(
frame, text="При перемещении файла или открытии из другой папки\n" sys_inner,
"автозапуск будет сброшен", text="Если переместить программу в другую папку, запись автозапуска может сброситься.",
font=(theme.ui_font_family, 13), text_color=theme.text_secondary, font=(theme.ui_font_family, 11),
anchor="w", justify="left") text_color=theme.text_secondary,
as_hint.pack(anchor="w", pady=(0, 8)) anchor="w",
justify="left",
wraplength=inner_w,
)
as_hint.pack(anchor="w")
attach_tooltip_to_widgets([as_cb, as_hint], _TIP_AUTOSTART) attach_tooltip_to_widgets([as_cb, as_hint], _TIP_AUTOSTART)
return TrayConfigFormWidgets( return TrayConfigFormWidgets(
@ -263,8 +410,14 @@ def install_tray_config_buttons(
on_save: Callable[[], None], on_save: Callable[[], None],
on_cancel: Callable[[], None], on_cancel: Callable[[], None],
) -> None: ) -> None:
ctk.CTkFrame(
frame,
fg_color=theme.field_border,
height=1,
corner_radius=0,
).pack(fill="x", pady=(4, 10))
btn_frame = ctk.CTkFrame(frame, fg_color="transparent") btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
btn_frame.pack(fill="x", pady=(20, 0)) btn_frame.pack(fill="x", pady=(0, 0))
save_btn = ctk.CTkButton( save_btn = ctk.CTkButton(
btn_frame, text="Сохранить", height=38, btn_frame, text="Сохранить", height=38,
font=(theme.ui_font_family, 14, "bold"), corner_radius=10, font=(theme.ui_font_family, 14, "bold"), corner_radius=10,

View File

@ -37,10 +37,12 @@ except ImportError:
Image = ImageDraw = ImageFont = None Image = ImageDraw = ImageFont = None
import proxy.tg_ws_proxy as tg_ws_proxy import proxy.tg_ws_proxy as tg_ws_proxy
from proxy import __version__
from ui.ctk_tray_ui import ( from ui.ctk_tray_ui import (
install_tray_config_buttons, install_tray_config_buttons,
install_tray_config_form, install_tray_config_form,
populate_first_run_window, populate_first_run_window,
tray_settings_scroll_and_footer,
validate_config_form, validate_config_form,
) )
from ui.ctk_theme import ( from ui.ctk_theme import (
@ -453,7 +455,7 @@ def _edit_config_dialog():
theme = ctk_theme_for_platform() theme = ctk_theme_for_platform()
w, h = CONFIG_DIALOG_SIZE w, h = CONFIG_DIALOG_SIZE
if _supports_autostart(): if _supports_autostart():
h += 70 h += 100
icon_path = str(Path(__file__).parent / "icon.ico") icon_path = str(Path(__file__).parent / "icon.ico")
@ -469,9 +471,11 @@ def _edit_config_dialog():
fpx, fpy = CONFIG_DIALOG_FRAME_PAD fpx, fpy = CONFIG_DIALOG_FRAME_PAD
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy) frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
widgets = install_tray_config_form( widgets = install_tray_config_form(
ctk, ctk,
frame, scroll,
theme, theme,
cfg, cfg,
DEFAULT_CONFIG, DEFAULT_CONFIG,
@ -515,9 +519,17 @@ def _edit_config_dialog():
root.destroy() root.destroy()
install_tray_config_buttons( install_tray_config_buttons(
ctk, frame, theme, on_save=on_save, on_cancel=on_cancel) ctk, footer, theme, on_save=on_save, on_cancel=on_cancel)
root.mainloop() try:
root.mainloop()
finally:
import tkinter as tk
try:
if root.winfo_exists():
root.destroy()
except tk.TclError:
pass
def _on_open_logs(icon=None, item=None): def _on_open_logs(icon=None, item=None):
@ -579,7 +591,15 @@ def _show_first_run():
populate_first_run_window( populate_first_run_window(
ctk, root, theme, host=host, port=port, on_done=on_done) ctk, root, theme, host=host, port=port, on_done=on_done)
root.mainloop() try:
root.mainloop()
finally:
import tkinter as tk
try:
if root.winfo_exists():
root.destroy()
except tk.TclError:
pass
def _has_ipv6_enabled() -> bool: def _has_ipv6_enabled() -> bool:
@ -667,7 +687,7 @@ def run_tray():
setup_logging(_config.get("verbose", False), setup_logging(_config.get("verbose", False),
log_max_mb=_config.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])) log_max_mb=_config.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"]))
log.info("TG WS Proxy tray app starting") log.info("TG WS Proxy версия %s, tray app starting", __version__)
log.info("Config: %s", _config) log.info("Config: %s", _config)
log.info("Log file: %s", LOG_FILE) log.info("Log file: %s", LOG_FILE)