From f0044255fa57f2a268501cb8aba0f3958c0eb08a Mon Sep 17 00:00:00 2001 From: deexsed Date: Thu, 26 Mar 2026 11:21:33 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20GitHub,=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=20utils?= =?UTF-8?q?,=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=BF=D0=BE=20=D0=9E=D0=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка последнего релиза при запуске (опционально), диалог и ссылка на страницу релиза; секция в настройках CTk и меню macOS; единый default_tray_config; requirements/*.txt; README. Refs: https://github.com/Flowseal/tg-ws-proxy/issues/357 --- README.md | 21 +++++- linux.py | 56 +++++++++++++--- macos.py | 72 ++++++++++++++++++--- pyproject.toml | 2 +- requirements/README.md | 66 +++++++++++++++++++ requirements/linux.txt | 11 ++++ requirements/macos.txt | 9 +++ requirements/windows-py38.txt | 9 +++ requirements/windows-py39plus.txt | 10 +++ ui/ctk_tray_ui.py | 75 ++++++++++++++++++++++ utils/__init__.py | 5 ++ utils/default_config.py | 27 ++++++++ utils/update_check.py | 102 ++++++++++++++++++++++++++++++ windows.py | 58 ++++++++++++++--- 14 files changed, 492 insertions(+), 31 deletions(-) create mode 100644 requirements/README.md create mode 100644 requirements/linux.txt create mode 100644 requirements/macos.txt create mode 100644 requirements/windows-py38.txt create mode 100644 requirements/windows-py39plus.txt create mode 100644 utils/__init__.py create mode 100644 utils/default_config.py create mode 100644 utils/update_check.py diff --git a/README.md b/README.md index a44043b..dc25e1e 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,12 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegra - **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку - **Перезапустить прокси** — перезапуск без выхода из приложения -- **Настройки...** — GUI-редактор конфигурации +- **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub) - **Открыть логи** — открыть файл логов - **Выход** — остановить прокси и закрыть приложение +При первом запуске после старта может появиться запрос об открытии страницы релиза, если на GitHub вышла новая версия (отключается в настройках). + ### macOS Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel. @@ -82,6 +84,11 @@ chmod +x TgWsProxy_linux_amd64 ## Установка из исходников +Список пакетов `pip` с версиями **по ОС** (Windows/Linux/macOS, отдельно для Python 3.8 на Windows) — в каталоге [`requirements/`](requirements/README.md). Кратко: + +- **Linux (трей):** системный пакет Tcl/Tk для `tkinter`, например `python3-tk` (Debian/Ubuntu). +- Дальше: `pip install -r requirements/<файл>.txt` и `pip install -e .` из корня репозитория. + ### Консольный proxy Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки: @@ -94,6 +101,7 @@ tg-ws-proxy ### Windows 7/10+ ```bash +pip install -r requirements/windows-py39plus.txt pip install -e . tg-ws-proxy-tray-win ``` @@ -101,6 +109,7 @@ tg-ws-proxy-tray-win ### macOS ```bash +pip install -r requirements/macos.txt pip install -e . tg-ws-proxy-tray-macos ``` @@ -108,6 +117,7 @@ tg-ws-proxy-tray-macos ### Linux ```bash +pip install -r requirements/linux.txt pip install -e . tg-ws-proxy-tray-linux ``` @@ -179,15 +189,22 @@ Tray-приложение хранит данные в: ```json { + "host": "127.0.0.1", "port": 1080, "dc_ip": [ "2:149.154.167.220", "4:149.154.167.220" ], - "verbose": false + "verbose": false, + "buf_kb": 256, + "pool_size": 4, + "log_max_mb": 5.0, + "check_updates": true } ``` +Ключ **`check_updates`** — при `true` при запросе к GitHub сравнивается версия с последним релизом (только уведомление и ссылка на страницу загрузки). На Windows в конфиге может быть **`autostart`** (автозапуск при входе в систему). + ## Автоматическая сборка Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.spec), [`packaging/linux.spec`](packaging/linux.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки. diff --git a/linux.py b/linux.py index e46c398..fe59ad6 100644 --- a/linux.py +++ b/linux.py @@ -8,6 +8,7 @@ import os import subprocess import sys import threading +import webbrowser import time from pathlib import Path from typing import Dict, Optional @@ -20,6 +21,7 @@ from PIL import Image, ImageDraw, ImageFont import proxy.tg_ws_proxy as tg_ws_proxy from proxy import __version__ +from utils.default_config import default_tray_config from ui.ctk_tray_ui import ( install_tray_config_buttons, install_tray_config_form, @@ -44,15 +46,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done" IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned" -DEFAULT_CONFIG = { - "port": 1080, - "host": "127.0.0.1", - "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], - "verbose": False, - "log_max_mb": 5, - "buf_kb": 256, - "pool_size": 4, -} +DEFAULT_CONFIG = default_tray_config() _proxy_thread: Optional[threading.Thread] = None @@ -350,6 +344,48 @@ def _show_info(text: str, title: str = "TG WS Proxy"): root.destroy() +def _ask_yes_no_dialog(text: str, title: str = "TG WS Proxy") -> bool: + import tkinter as _tk + from tkinter import messagebox as _mb + + root = _tk.Tk() + root.withdraw() + try: + root.attributes("-topmost", True) + except Exception: + pass + r = _mb.askyesno(title, text, parent=root) + root.destroy() + return bool(r) + + +def _maybe_notify_update_async(): + def _work(): + time.sleep(1.5) + if _exiting: + return + if not _config.get("check_updates", True): + return + try: + from utils.update_check import RELEASES_PAGE_URL, get_status, run_check + run_check(__version__) + st = get_status() + if not st.get("has_update"): + return + url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL + ver = st.get("latest") or "?" + text = ( + f"Доступна новая версия: {ver}\n\n" + f"Открыть страницу релиза в браузере?" + ) + if _ask_yes_no_dialog(text, "TG WS Proxy — обновление"): + webbrowser.open(url) + except Exception as exc: + log.debug("Update check failed: %s", exc) + + threading.Thread(target=_work, daemon=True, name="update-check").start() + + def _on_open_in_telegram(icon=None, item=None): host = _config.get("host", DEFAULT_CONFIG["host"]) port = _config.get("port", DEFAULT_CONFIG["port"]) @@ -624,6 +660,8 @@ def run_tray(): start_proxy() + _maybe_notify_update_async() + _show_first_run() _check_ipv6_warning() diff --git a/macos.py b/macos.py index bf00e06..b8660bf 100644 --- a/macos.py +++ b/macos.py @@ -31,6 +31,7 @@ except ImportError: import proxy.tg_ws_proxy as tg_ws_proxy from proxy import __version__ +from utils.default_config import default_tray_config APP_NAME = "TgWsProxy" APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME @@ -40,15 +41,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done" IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned" MENUBAR_ICON_PATH = APP_DIR / "menubar_icon.png" -DEFAULT_CONFIG = { - "port": 1080, - "host": "127.0.0.1", - "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], - "verbose": False, - "log_max_mb": 5, - "buf_kb": 256, - "pool_size": 4, -} +DEFAULT_CONFIG = default_tray_config() _proxy_thread: Optional[threading.Thread] = None _async_stop: Optional[object] = None @@ -427,6 +420,55 @@ def _on_edit_config(_=None): threading.Thread(target=_edit_config_dialog, daemon=True).start() +def _check_updates_menu_title() -> str: + on = bool(_config.get("check_updates", True)) + return ( + "✓ Проверять обновления при запуске" + if on + else "Проверять обновления при запуске (выкл)" + ) + + +def _toggle_check_updates(_=None): + global _config + _config["check_updates"] = not bool(_config.get("check_updates", True)) + save_config(_config) + if _app is not None: + _app._check_updates_item.title = _check_updates_menu_title() + + +def _on_open_release_page(_=None): + from utils.update_check import RELEASES_PAGE_URL + webbrowser.open(RELEASES_PAGE_URL) + + +def _maybe_notify_update_async(): + def _work(): + time.sleep(1.5) + if _exiting: + return + if not _config.get("check_updates", True): + return + try: + from utils.update_check import RELEASES_PAGE_URL, get_status, run_check + run_check(__version__) + st = get_status() + if not st.get("has_update"): + return + url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL + ver = st.get("latest") or "?" + if _ask_yes_no( + f"Доступна новая версия: {ver}\n\n" + f"Открыть страницу релиза в браузере?", + "TG WS Proxy — обновление", + ): + webbrowser.open(url) + except Exception as exc: + log.debug("Update check failed: %s", exc) + + threading.Thread(target=_work, daemon=True, name="update-check").start() + + # Settings via native macOS dialogs def _edit_config_dialog(): cfg = load_config() @@ -617,6 +659,12 @@ class TgWsProxyApp(_TgWsProxyAppBase): self._logs_item = rumps.MenuItem( "Открыть логи", callback=_on_open_logs) + self._release_page_item = rumps.MenuItem( + "Страница релиза на GitHub…", + callback=_on_open_release_page) + self._check_updates_item = rumps.MenuItem( + _check_updates_menu_title(), + callback=_toggle_check_updates) self._version_item = rumps.MenuItem( f"Версия {__version__}", callback=lambda _: None) @@ -633,6 +681,9 @@ class TgWsProxyApp(_TgWsProxyAppBase): self._settings_item, self._logs_item, None, + self._release_page_item, + self._check_updates_item, + None, self._version_item, ]) @@ -672,6 +723,9 @@ def run_menubar(): return start_proxy() + + _maybe_notify_update_async() + _show_first_run() _check_ipv6_warning() diff --git a/pyproject.toml b/pyproject.toml index f4897ff..5e440a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ Source = "https://github.com/Flowseal/tg-ws-proxy" Issues = "https://github.com/Flowseal/tg-ws-proxy/issues" [tool.hatch.build.targets.wheel] -packages = ["proxy", "ui"] +packages = ["proxy", "ui", "utils"] [tool.hatch.build.force-include] "windows.py" = "windows.py" diff --git a/requirements/README.md b/requirements/README.md new file mode 100644 index 0000000..d6f291e --- /dev/null +++ b/requirements/README.md @@ -0,0 +1,66 @@ +# Зависимости по ОС (разработка из исходников) + +Версии совпадают с [`pyproject.toml`](../pyproject.toml). Для установки **пакета проекта** после зависимостей выполните из корня репозитория: + +```bash +pip install -e . +``` + +Скрипты точек входа: `tg-ws-proxy`, `tg-ws-proxy-tray-win` / `tg-ws-proxy-tray-linux` / `tg-ws-proxy-tray-macos`. + +--- + +## Windows + +| Файл | Условие | +|------|---------| +| [`windows-py39plus.txt`](windows-py39plus.txt) | Python **3.9+** (рекомендуется) | +| [`windows-py38.txt`](windows-py38.txt) | Python **3.8** | + +**Системно:** для сборки/запуска из исходников — установленный [Python](https://www.python.org/downloads/) с опцией *tcl/tk* (обычно включена). Отдельный Tcl/Tk не требуется. + +```powershell +pip install -r requirements\windows-py39plus.txt +pip install -e . +tg-ws-proxy-tray-win +``` + +--- + +## Linux + +Файл: [`linux.txt`](linux.txt) (Python **3.9+**). + +**Системно:** модуль `tkinter` (нужен CustomTkinter для трея): + +- Debian/Ubuntu: `sudo apt install python3-tk` +- Fedora: `sudo dnf install python3-tkinter` +- Arch: `sudo pacman -S tk` + +Также нужны заголовки Python при сборке расширений (если pip собирает колёса): `python3-dev` / `gcc` — по сообщениям pip. + +```bash +pip install -r requirements/linux.txt +pip install -e . +tg-ws-proxy-tray-linux +``` + +--- + +## macOS + +Файл: [`macos.txt`](macos.txt). + +**Системно:** Python с официального установщика или Homebrew; Tcl/Tk обычно уже в комплекте. Для графики трея используется **rumps** (нативная строка меню), не CustomTkinter. + +```bash +pip install -r requirements/macos.txt +pip install -e . +tg-ws-proxy-tray-macos +``` + +--- + +## Примечание + +Готовые бинарники для пользователей — на [странице релизов](https://github.com/Flowseal/tg-ws-proxy/releases); отдельная установка Python и `pip` для них не требуется. diff --git a/requirements/linux.txt b/requirements/linux.txt new file mode 100644 index 0000000..35e32ec --- /dev/null +++ b/requirements/linux.txt @@ -0,0 +1,11 @@ +# Linux (не macOS), Python 3.9+ (как в pyproject.toml) +# Системно: пакет Tcl/Tk для tkinter (CustomTkinter), см. README в этой папке. +# Установка: pip install -r requirements/linux.txt +# Затем из корня репозитория: pip install -e . + +pyperclip==1.9.0 +psutil==7.0.0 +cryptography==46.0.5 +Pillow==12.1.1 +customtkinter==5.2.2 +pystray==0.19.5 diff --git a/requirements/macos.txt b/requirements/macos.txt new file mode 100644 index 0000000..77e357a --- /dev/null +++ b/requirements/macos.txt @@ -0,0 +1,9 @@ +# macOS, Python 3.9+ (без customtkinter/pystray — нативное меню rumps) +# Установка: pip install -r requirements/macos.txt +# Затем из корня репозитория: pip install -e . + +pyperclip==1.9.0 +psutil==7.0.0 +cryptography==46.0.5 +Pillow==12.1.0 +rumps==0.4.0 diff --git a/requirements/windows-py38.txt b/requirements/windows-py38.txt new file mode 100644 index 0000000..8da5d73 --- /dev/null +++ b/requirements/windows-py38.txt @@ -0,0 +1,9 @@ +# Windows, Python 3.8 (устаревшие пины из pyproject.toml) +# Установка: pip install -r requirements/windows-py38.txt + +pyperclip==1.9.0 +psutil==5.9.8 +cryptography==41.0.7 +Pillow==10.4.0 +customtkinter==5.2.2 +pystray==0.19.5 diff --git a/requirements/windows-py39plus.txt b/requirements/windows-py39plus.txt new file mode 100644 index 0000000..f586c87 --- /dev/null +++ b/requirements/windows-py39plus.txt @@ -0,0 +1,10 @@ +# Windows, Python 3.9+ (как в pyproject.toml для platform_system == Windows, python_version >= 3.9) +# Установка: pip install -r requirements/windows-py39plus.txt +# Затем из корня репозитория: pip install -e . + +pyperclip==1.9.0 +psutil==7.0.0 +cryptography==46.0.5 +Pillow==12.1.1 +customtkinter==5.2.2 +pystray==0.19.5 diff --git a/ui/ctk_tray_ui.py b/ui/ctk_tray_ui.py index b06a051..e8096a2 100644 --- a/ui/ctk_tray_ui.py +++ b/ui/ctk_tray_ui.py @@ -5,11 +5,13 @@ from __future__ import annotations +import webbrowser from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional, Tuple, Union import proxy.tg_ws_proxy as tg_ws_proxy from proxy import __version__ +from utils.update_check import RELEASES_PAGE_URL, get_status from ui.ctk_theme import ( FIRST_RUN_FRAME_PAD, @@ -52,6 +54,10 @@ _TIP_AUTOSTART = ( "Запускать TG WS Proxy при входе в Windows. " "Если вы переместите программу в другую папку, запись автозапуска может сброситься." ) +_TIP_CHECK_UPDATES = ( + "При запуске запрашивать с GitHub информацию о последнем релизе. " + "При появлении новой версии можно открыть страницу загрузки." +) _TIP_SAVE = "Сохранить настройки в файл. После сохранения можно перезапустить прокси." _TIP_CANCEL = "Закрыть окно без сохранения изменений." @@ -121,6 +127,7 @@ class TrayConfigFormWidgets: adv_entries: List[Any] adv_keys: Tuple[str, ...] autostart_var: Optional[Any] + check_updates_var: Optional[Any] def install_tray_config_form( @@ -291,6 +298,71 @@ def install_tray_config_form( adv_entries = list(adv_frame.winfo_children()) adv_keys = ("buf_kb", "pool_size", "log_max_mb") + upd_inner = _config_section(ctk, frame, theme, "Обновления") + st = get_status() + check_updates_var = ctk.BooleanVar( + value=bool( + cfg.get("check_updates", default_config.get("check_updates", True)) + ) + ) + upd_cb = ctk.CTkCheckBox( + upd_inner, + text="Проверять обновления при запуске", + variable=check_updates_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, + ) + upd_cb.pack(anchor="w", pady=(0, 6)) + attach_ctk_tooltip(upd_cb, _TIP_CHECK_UPDATES) + + if st.get("error"): + upd_status = "Не удалось связаться с GitHub. Проверьте сеть." + elif not st.get("checked"): + upd_status = "Статус появится после фоновой проверки при запуске." + elif st.get("has_update") and st.get("latest"): + upd_status = ( + f"На GitHub доступна версия {st['latest']} " + f"(у вас {__version__})." + ) + elif st.get("ahead_of_release") and st.get("latest"): + upd_status = ( + f"У вас {__version__} — новее последнего релиза на GitHub " + f"({st['latest']})." + ) + else: + upd_status = "Установлена последняя известная версия с GitHub." + + ctk.CTkLabel( + upd_inner, + text=upd_status, + font=(theme.ui_font_family, 11), + text_color=theme.text_secondary, + anchor="w", + justify="left", + wraplength=inner_w, + ).pack(anchor="w", pady=(0, 8)) + + rel_url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL + open_rel_btn = ctk.CTkButton( + upd_inner, + text="Открыть страницу релиза", + 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), + ) + open_rel_btn.pack(anchor="w") + autostart_var = None if show_autostart: sys_inner = _config_section( @@ -330,6 +402,7 @@ def install_tray_config_form( adv_entries=adv_entries, adv_keys=adv_keys, autostart_var=autostart_var, + check_updates_var=check_updates_var, ) @@ -399,6 +472,8 @@ def validate_config_form( ) 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()) return new_cfg diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..d370cd6 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,5 @@ +"""Вспомогательные утилиты (проверка релизов и т.п.).""" + +from utils.update_check import RELEASES_PAGE_URL, get_status, run_check + +__all__ = ["RELEASES_PAGE_URL", "get_status", "run_check"] diff --git a/utils/default_config.py b/utils/default_config.py new file mode 100644 index 0000000..30b7bc6 --- /dev/null +++ b/utils/default_config.py @@ -0,0 +1,27 @@ +""" +Общие значения по умолчанию для tray-приложений (Windows / Linux / macOS). +Единственное отличие по платформе — ключ autostart только на Windows. +""" +from __future__ import annotations + +import sys +from typing import Any, Dict + +_TRAY_DEFAULTS_COMMON: Dict[str, Any] = { + "port": 1080, + "host": "127.0.0.1", + "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], + "verbose": False, + "check_updates": True, + "log_max_mb": 5, + "buf_kb": 256, + "pool_size": 4, +} + + +def default_tray_config() -> Dict[str, Any]: + """Новая копия конфига по умолчанию для текущей ОС.""" + cfg = dict(_TRAY_DEFAULTS_COMMON) + if sys.platform == "win32": + cfg["autostart"] = False + return cfg diff --git a/utils/update_check.py b/utils/update_check.py new file mode 100644 index 0000000..76c8770 --- /dev/null +++ b/utils/update_check.py @@ -0,0 +1,102 @@ +""" +Минимальная проверка новой версии через GitHub Releases API (без сторонних зависимостей). +""" +from __future__ import annotations + +import json +from itertools import zip_longest +from typing import Any, Dict, Optional +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +REPO = "Flowseal/tg-ws-proxy" +RELEASES_LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest" +RELEASES_PAGE_URL = f"https://github.com/{REPO}/releases/latest" + +_state: Dict[str, Any] = { + "checked": False, + "has_update": False, + "ahead_of_release": False, + "latest": None, + "html_url": None, + "error": None, +} + + +def _parse_version_tuple(s: str) -> tuple: + s = (s or "").strip().lstrip("vV") + if not s: + return (0,) + parts = [] + for seg in s.split("."): + digits = "".join(c for c in seg if c.isdigit()) + if digits: + try: + parts.append(int(digits)) + except ValueError: + parts.append(0) + else: + parts.append(0) + return tuple(parts) if parts else (0,) + + +def _version_gt(a: str, b: str) -> bool: + """True, если версия a новее b (простое сравнение по сегментам).""" + ta = _parse_version_tuple(a) + tb = _parse_version_tuple(b) + for x, y in zip_longest(ta, tb, fillvalue=0): + if x > y: + return True + if x < y: + return False + return False + + +def fetch_latest_release(timeout: float = 12.0) -> Optional[dict]: + req = Request( + RELEASES_LATEST_API, + headers={ + "Accept": "application/vnd.github+json", + "User-Agent": "tg-ws-proxy-update-check", + }, + method="GET", + ) + with urlopen(req, timeout=timeout) as resp: + raw = resp.read().decode("utf-8", errors="replace") + return json.loads(raw) + + +def run_check(current_version: str) -> None: + """Запрашивает последний релиз и обновляет внутреннее состояние.""" + global _state + _state["checked"] = True + _state["error"] = None + try: + data = fetch_latest_release() + tag = (data.get("tag_name") or "").strip() + html_url = (data.get("html_url") or "").strip() or RELEASES_PAGE_URL + if not tag: + _state["has_update"] = False + _state["ahead_of_release"] = False + _state["latest"] = None + _state["html_url"] = html_url + return + latest_clean = tag.lstrip("vV") + cur = (current_version or "").strip().lstrip("vV") + _state["latest"] = latest_clean + _state["html_url"] = html_url + _state["has_update"] = _version_gt(latest_clean, cur) + _state["ahead_of_release"] = bool(latest_clean) and _version_gt( + cur, latest_clean + ) + except (HTTPError, URLError, OSError, TimeoutError, ValueError, json.JSONDecodeError) as e: + _state["error"] = str(e) + _state["has_update"] = False + _state["ahead_of_release"] = False + _state["latest"] = None + _state["html_url"] = RELEASES_PAGE_URL + + +def get_status() -> Dict[str, Any]: + """Снимок состояния после run_check (для подписей в настройках).""" + return dict(_state) diff --git a/windows.py b/windows.py index b042f20..4357fe0 100644 --- a/windows.py +++ b/windows.py @@ -38,6 +38,7 @@ except ImportError: import proxy.tg_ws_proxy as tg_ws_proxy from proxy import __version__ +from utils.default_config import default_tray_config from ui.ctk_tray_ui import ( install_tray_config_buttons, install_tray_config_form, @@ -65,16 +66,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done" IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned" -DEFAULT_CONFIG = { - "port": 1080, - "host": "127.0.0.1", - "dc_ip": ["2:149.154.167.220", "4:149.154.167.220"], - "verbose": False, - "autostart": False, - "log_max_mb": 5, - "buf_kb": 256, - "pool_size": 4, -} +DEFAULT_CONFIG = default_tray_config() _proxy_thread: Optional[threading.Thread] = None @@ -404,6 +396,50 @@ def _show_info(text: str, title: str = "TG WS Proxy"): _user32.MessageBoxW(None, text, title, 0x40) +def _ask_open_release_page(latest_version: str, url: str) -> bool: + """Win32 Yes/No: открыть страницу релиза.""" + MB_YESNO = 0x4 + MB_ICONQUESTION = 0x20 + IDYES = 6 + text = ( + f"Доступна новая версия: {latest_version}\n\n" + f"Открыть страницу релиза в браузере?" + ) + r = _user32.MessageBoxW( + None, + text, + "TG WS Proxy — обновление", + MB_YESNO | MB_ICONQUESTION, + ) + return r == IDYES + + +def _maybe_notify_update_async(): + """ + Фоновая проверка GitHub Releases и уведомление (не блокирует трей). + """ + def _work(): + time.sleep(1.5) + if _exiting: + return + if not _config.get("check_updates", True): + return + try: + from utils.update_check import RELEASES_PAGE_URL, get_status, run_check + run_check(__version__) + st = get_status() + if not st.get("has_update"): + return + url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL + ver = st.get("latest") or "?" + if _ask_open_release_page(str(ver), url): + webbrowser.open(url) + except Exception as exc: + log.debug("Update check failed: %s", exc) + + threading.Thread(target=_work, daemon=True, name="update-check").start() + + def _on_open_in_telegram(icon=None, item=None): host = _config.get("host", DEFAULT_CONFIG["host"]) port = _config.get("port", DEFAULT_CONFIG["port"]) @@ -705,6 +741,8 @@ def run_tray(): start_proxy() + _maybe_notify_update_async() + _show_first_run() _check_ipv6_warning()