diff --git a/ui/ctk_tray_ui.py b/ui/ctk_tray_ui.py index 5dd8b94..035c042 100644 --- a/ui/ctk_tray_ui.py +++ b/ui/ctk_tray_ui.py @@ -373,6 +373,7 @@ def install_tray_config_form( 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) @@ -776,14 +777,35 @@ def install_tray_config_form( justify="left", wraplength=_INNER_W).pack(anchor="w", pady=(0, 8)) rel_url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL - 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") + 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: diff --git a/ui/i18n/en.json b/ui/i18n/en.json index 876788e..7de55ac 100644 --- a/ui/i18n/en.json +++ b/ui/i18n/en.json @@ -113,6 +113,7 @@ "tray.restart": "Restart proxy", "tray.settings": "Settings...", "tray.logs": "Open logs", + "tray.update": "Update ({current} → {new})", "tray.exit": "Exit", "dialog.restart_title": "Restart?", diff --git a/ui/i18n/ru.json b/ui/i18n/ru.json index efa97aa..67f266a 100644 --- a/ui/i18n/ru.json +++ b/ui/i18n/ru.json @@ -113,6 +113,7 @@ "tray.restart": "Перезапустить прокси", "tray.settings": "Настройки...", "tray.logs": "Открыть логи", + "tray.update": "Обновить ({current} → {new})", "tray.exit": "Выход", "dialog.restart_title": "Перезапустить?", diff --git a/windows.py b/windows.py index 1b3efbe..2c65ee1 100644 --- a/windows.py +++ b/windows.py @@ -34,7 +34,7 @@ try: except ImportError: Image = None -from proxy import get_link_host +from proxy import __version__, get_link_host from utils.win32_theme import ( is_windows_dark_theme, @@ -317,6 +317,34 @@ def _perform_update(download_url: str, set_status=None) -> None: os._exit(0) +def _trigger_update_flow(icon=None, item=None) -> None: + """Show the update dialog for an update already known to be available. + + Reused by the tray "Update" item and the settings dialog's "Update" + button, so an update can still be started after the initial startup + prompt was skipped/closed. + """ + from utils.update_check import RELEASES_PAGE_URL, get_status, get_update_asset + + 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 "?" + asset = get_update_asset(Path(sys.executable), __version__) if IS_FROZEN else None + + def _show() -> None: + choice = update_ctk_form( + t("update.available", version=ver), + download_url=asset[0] if asset else None, + release_url=url, + ) + if choice == "open": + webbrowser.open(url) + + threading.Thread(target=_show, daemon=True, name="manual-update").start() + + def _maybe_do_update(cfg: dict, is_exiting) -> None: if not cfg.get("check_updates", True): return @@ -326,23 +354,14 @@ def _maybe_do_update(cfg: dict, is_exiting) -> None: if is_exiting(): return try: - from proxy import __version__ - from utils.update_check import RELEASES_PAGE_URL, get_status, get_update_asset, run_check + from utils.update_check import run_check run_check(__version__) - st = get_status() - if not st.get("has_update") or is_exiting(): + if _tray_icon is not None: + _tray_icon.menu = _build_menu() + if is_exiting(): return - url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL - ver = st.get("latest") or "?" - asset = get_update_asset(Path(sys.executable), __version__) if IS_FROZEN else None - choice = update_ctk_form( - t("update.available", version=ver), - download_url=asset[0] if asset else None, - release_url=url, - ) - if choice == "open": - webbrowser.open(url) + _trigger_update_flow() except Exception as exc: log.warning("Update check failed: %s", repr(exc)) @@ -498,6 +517,7 @@ def _edit_config_dialog() -> None: show_autostart=_supports_autostart(), autostart_value=cfg.get("autostart", False), on_language_change=_refresh_tray_menu, + on_update_click=_trigger_update_flow, ) _original_appearance = ctk.get_appearance_mode() @@ -602,16 +622,25 @@ def _build_menu(): host = _config.get("host", DEFAULT_CONFIG["host"]) port = _config.get("port", DEFAULT_CONFIG["port"]) link_host = get_link_host(host) - return pystray.Menu( + items = [ pystray.MenuItem(t("tray.open_telegram", host=link_host, port=port), _on_open_in_telegram, default=True), pystray.MenuItem(t("tray.copy_link"), _on_copy_link), pystray.Menu.SEPARATOR, pystray.MenuItem(t("tray.restart"), _on_restart), pystray.MenuItem(t("tray.settings"), _on_edit_config), pystray.MenuItem(t("tray.logs"), _on_open_logs), - pystray.Menu.SEPARATOR, - pystray.MenuItem(t("tray.exit"), _on_exit), - ) + ] + from utils.update_check import get_status + st = get_status() + if st.get("has_update"): + items.append(pystray.Menu.SEPARATOR) + items.append(pystray.MenuItem( + t("tray.update", current=__version__, new=st.get("latest") or "?"), + _trigger_update_flow, + )) + items.append(pystray.Menu.SEPARATOR) + items.append(pystray.MenuItem(t("tray.exit"), _on_exit)) + return pystray.Menu(*items) # entry point