tg-ws-proxy/ui/ctk_tray_ui.py

354 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Общая разметка CustomTkinter для tray (Windows / Linux): настройки и первый запуск.
Логика сохранения и колбэки остаются в платформенных модулях.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import proxy.tg_ws_proxy as tg_ws_proxy
from ui.ctk_theme import (
FIRST_RUN_FRAME_PAD,
CtkTheme,
main_content_frame,
)
from ui.ctk_tooltip import attach_ctk_tooltip, attach_tooltip_to_widgets
# Подсказки для формы настроек (новые пользователи)
_TIP_HOST = (
"Адрес, на котором прокси принимает SOCKS5-подключения.\n"
"Обычно 127.0.0.1 — только этот компьютер. Другой IP нужен, "
"если к прокси подключаются по локальной сети."
)
_TIP_PORT = (
"Порт SOCKS5. В Telegram Desktop в настройках прокси должен быть "
"указан тот же порт (часто 1080)."
)
_TIP_DC = (
"Соответствие номера датацентра Telegram (DC) и IP-адреса сервера.\n"
"Каждая строка: «номер:IP», например 2:149.154.167.220. "
"Прокси по этим правилам направляет трафик к нужным серверам Telegram."
)
_TIP_VERBOSE = (
"Если включено, в файл логов пишется больше подробностей — "
"удобно при поиске неполадок."
)
_TIP_BUF_KB = (
"Размер буфера приёма/передачи в килобайтах.\n"
"Больше значение — обычно стабильнее на быстрых каналах, выше расход памяти."
)
_TIP_POOL = (
"Сколько параллельных WebSocket-сессий к одному датацентру можно держать.\n"
"Увеличение может помочь при высокой нагрузке."
)
_TIP_LOG_MB = (
"Максимальный размер файла лога; при достижении лимита файл перезаписывается."
)
_TIP_AUTOSTART = (
"Запускать TG WS Proxy при входе в Windows. "
"Если вы переместите программу в другую папку, запись автозапуска может сброситься."
)
_TIP_SAVE = "Сохранить настройки в файл. После сохранения можно перезапустить прокси."
_TIP_CANCEL = "Закрыть окно без сохранения изменений."
@dataclass
class TrayConfigFormWidgets:
host_var: Any
port_var: Any
dc_textbox: Any
verbose_var: Any
adv_entries: List[Any]
adv_keys: Tuple[str, ...]
autostart_var: Optional[Any]
def install_tray_config_form(
ctk: Any,
frame: Any,
theme: CtkTheme,
cfg: dict,
default_config: dict,
*,
show_autostart: bool = False,
autostart_value: bool = False,
) -> TrayConfigFormWidgets:
"""Поля настроек прокси внутри уже созданного `frame`."""
host_lbl = ctk.CTkLabel(frame, text="IP-адрес прокси",
font=(theme.ui_font_family, 13),
text_color=theme.text_primary, anchor="w")
host_lbl.pack(anchor="w", pady=(0, 4))
host_var = ctk.StringVar(value=cfg.get("host", default_config["host"]))
host_entry = ctk.CTkEntry(
frame, textvariable=host_var, width=200, height=36,
font=(theme.ui_font_family, 13), corner_radius=10,
fg_color=theme.field_bg, border_color=theme.field_border,
border_width=1, text_color=theme.text_primary)
host_entry.pack(anchor="w", pady=(0, 12))
attach_tooltip_to_widgets([host_lbl, host_entry], _TIP_HOST)
port_lbl = ctk.CTkLabel(frame, text="Порт прокси",
font=(theme.ui_font_family, 13),
text_color=theme.text_primary, anchor="w")
port_lbl.pack(anchor="w", pady=(0, 4))
port_var = ctk.StringVar(value=str(cfg.get("port", default_config["port"])))
port_entry = ctk.CTkEntry(
frame, textvariable=port_var, width=120, height=36,
font=(theme.ui_font_family, 13), corner_radius=10,
fg_color=theme.field_bg, border_color=theme.field_border,
border_width=1, text_color=theme.text_primary)
port_entry.pack(anchor="w", pady=(0, 12))
attach_tooltip_to_widgets([port_lbl, port_entry], _TIP_PORT)
dc_lbl = ctk.CTkLabel(
frame, text="DC → IP маппинги (по одному на строку, формат DC:IP)",
font=(theme.ui_font_family, 13), text_color=theme.text_primary,
anchor="w")
dc_lbl.pack(anchor="w", pady=(0, 4))
dc_textbox = ctk.CTkTextbox(
frame, width=370, height=120,
font=(theme.mono_font_family, 12), corner_radius=10,
fg_color=theme.field_bg, border_color=theme.field_border,
border_width=1, text_color=theme.text_primary)
dc_textbox.pack(anchor="w", pady=(0, 12))
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)
verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False))
verbose_cb = ctk.CTkCheckBox(
frame, text="Подробное логирование (verbose)",
variable=verbose_var, 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)
verbose_cb.pack(anchor="w", pady=(0, 8))
attach_ctk_tooltip(verbose_cb, _TIP_VERBOSE)
adv_frame = ctk.CTkFrame(frame, fg_color="transparent")
adv_frame.pack(anchor="w", fill="x", pady=(4, 8))
adv_rows = [
("Буфер (KB, 256 default)", "buf_kb", 120, _TIP_BUF_KB),
("WS пулов (4 default)", "pool_size", 120, _TIP_POOL),
("Log size (MB, 5 def)", "log_max_mb", 120, _TIP_LOG_MB),
]
for lbl, key, w_, tip in adv_rows:
col_frame = ctk.CTkFrame(adv_frame, fg_color="transparent")
col_frame.pack(side="left", padx=(0, 10))
adv_l = ctk.CTkLabel(col_frame, text=lbl, font=(theme.ui_font_family, 11),
text_color=theme.text_secondary, anchor="w")
adv_l.pack(anchor="w")
adv_e = ctk.CTkEntry(
col_frame, width=w_, height=30, font=(theme.ui_font_family, 12),
corner_radius=8, fg_color=theme.field_bg,
border_color=theme.field_border, border_width=1,
text_color=theme.text_primary,
textvariable=ctk.StringVar(
value=str(cfg.get(key, default_config[key]))
))
adv_e.pack(anchor="w")
attach_tooltip_to_widgets([adv_l, adv_e, col_frame], tip)
adv_entries = list(adv_frame.winfo_children())
adv_keys = ("buf_kb", "pool_size", "log_max_mb")
autostart_var = None
if show_autostart:
autostart_var = ctk.BooleanVar(value=autostart_value)
as_cb = ctk.CTkCheckBox(
frame, text="Автозапуск при включении Windows",
variable=autostart_var, 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)
as_cb.pack(anchor="w", pady=(0, 8))
as_hint = ctk.CTkLabel(
frame, text="При перемещении файла или открытии из другой папки\n"
"автозапуск будет сброшен",
font=(theme.ui_font_family, 13), text_color=theme.text_secondary,
anchor="w", justify="left")
as_hint.pack(anchor="w", pady=(0, 8))
attach_tooltip_to_widgets([as_cb, as_hint], _TIP_AUTOSTART)
return TrayConfigFormWidgets(
host_var=host_var,
port_var=port_var,
dc_textbox=dc_textbox,
verbose_var=verbose_var,
adv_entries=adv_entries,
adv_keys=adv_keys,
autostart_var=autostart_var,
)
def merge_adv_from_form(
widgets: TrayConfigFormWidgets,
base: Dict[str, Any],
default_config: dict,
) -> None:
"""Дополняет base значениями buf_kb / pool_size / log_max_mb (in-place)."""
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 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 "Некорректный IP-адрес."
try:
port_val = int(widgets.port_var.get().strip())
if not (1 <= port_val <= 65535):
raise ValueError
except ValueError:
return "Порт должен быть числом 1-65535"
lines = [
l.strip()
for l in widgets.dc_textbox.get("1.0", "end").strip().splitlines()
if l.strip()
]
try:
tg_ws_proxy.parse_dc_ip_list(lines)
except ValueError as e:
return str(e)
new_cfg: Dict[str, Any] = {
"host": host_val,
"port": port_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)
return new_cfg
def install_tray_config_buttons(
ctk: Any,
frame: Any,
theme: CtkTheme,
*,
on_save: Callable[[], None],
on_cancel: Callable[[], None],
) -> None:
btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
btn_frame.pack(fill="x", pady=(20, 0))
save_btn = ctk.CTkButton(
btn_frame, text="Сохранить", height=38,
font=(theme.ui_font_family, 14, "bold"), corner_radius=10,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
text_color="#ffffff",
command=on_save)
save_btn.pack(side="left", fill="x", expand=True, padx=(0, 8))
attach_ctk_tooltip(save_btn, _TIP_SAVE)
cancel_btn = ctk.CTkButton(
btn_frame, text="Отмена", height=38,
font=(theme.ui_font_family, 14), corner_radius=10,
fg_color=theme.field_bg, hover_color=theme.field_border,
text_color=theme.text_primary, border_width=1,
border_color=theme.field_border,
command=on_cancel)
cancel_btn.pack(side="right", fill="x", expand=True)
attach_ctk_tooltip(cancel_btn, _TIP_CANCEL)
def populate_first_run_window(
ctk: Any,
root: Any,
theme: CtkTheme,
*,
host: str,
port: int,
on_done: Callable[[bool], None],
) -> None:
"""
Содержимое окна первого запуска. on_done(open_in_telegram) — по «Начать» и по закрытию окна.
"""
tg_url = f"tg://socks?server={host}&port={port}"
fpx, fpy = FIRST_RUN_FRAME_PAD
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
title_frame = ctk.CTkFrame(frame, fg_color="transparent")
title_frame.pack(anchor="w", pady=(0, 16), fill="x")
accent_bar = ctk.CTkFrame(title_frame, fg_color=theme.tg_blue,
width=4, height=32, corner_radius=2)
accent_bar.pack(side="left", padx=(0, 12))
ctk.CTkLabel(title_frame, text="Прокси запущен и работает в системном трее",
font=(theme.ui_font_family, 17, "bold"),
text_color=theme.text_primary).pack(side="left")
sections = [
("Как подключить Telegram Desktop:", True),
(" Автоматически:", True),
(" ПКМ по иконке в трее → «Открыть в Telegram»", False),
(f" Или ссылка: {tg_url}", False),
("\n Вручную:", True),
(" Настройки → Продвинутые → Тип подключения → Прокси", False),
(f" SOCKS5 → {host} : {port} (без логина/пароля)", False),
]
for text, bold in sections:
weight = "bold" if bold else "normal"
ctk.CTkLabel(frame, text=text,
font=(theme.ui_font_family, 13, weight),
text_color=theme.text_primary,
anchor="w", justify="left").pack(anchor="w", pady=1)
ctk.CTkFrame(frame, fg_color="transparent", height=16).pack()
ctk.CTkFrame(frame, fg_color=theme.field_border, height=1,
corner_radius=0).pack(fill="x", pady=(0, 12))
auto_var = ctk.BooleanVar(value=True)
ctk.CTkCheckBox(frame, text="Открыть прокси в Telegram сейчас",
variable=auto_var, 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).pack(anchor="w", pady=(0, 16))
def on_ok():
on_done(auto_var.get())
ctk.CTkButton(frame, text="Начать", width=180, height=42,
font=(theme.ui_font_family, 15, "bold"), corner_radius=10,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
text_color="#ffffff",
command=on_ok).pack(pady=(0, 0))
root.protocol("WM_DELETE_WINDOW", on_ok)