1616
1717from ..app_controller import AppController
1818from ..storage import PassphraseRequired
19- from ..constants import APP_NAME
19+ from ..constants import APP_NAME , APP_VERSION
2020from ..models import AppSettings , Node , RoutingSettings
21- from ..update_checker import check_update
21+ from ..app_updater import AppUpdate , UpdateChecker , UpdateDownloader
2222from ..xray_core_updater import XrayCoreUpdateResult
2323from .bulk_edit_dialog import BulkEditDialog
2424from .dashboard_page import DashboardPage
2828from .nodes_page import NodesPage
2929from .routing_page import RoutingPage
3030from .settings_page import SettingsPage
31+ from .updates_page import UpdatesPage
3132
3233
3334class MainWindow (FluentWindow ):
@@ -56,6 +57,7 @@ def initialize(self) -> None:
5657 self .routing_page = RoutingPage (self )
5758 self .logs_page = LogsPage (self )
5859 self .settings_page = SettingsPage (self )
60+ self .updates_page = UpdatesPage (self )
5961
6062 self ._create_navigation ()
6163 self ._create_tray ()
@@ -72,6 +74,11 @@ def initialize(self) -> None:
7274 if loaded and unlocked :
7375 self .controller .auto_connect_if_needed ()
7476
77+ # Set Xray version on updates page
78+ from ..xray_manager import get_xray_version
79+ xv = get_xray_version (self .controller .state .settings .xray_path )
80+ self .updates_page .set_xray_version (xv or "" )
81+
7582 if self .controller .state .settings .check_updates :
7683 QTimer .singleShot (2500 , lambda : self ._check_updates (silent = True ))
7784
@@ -85,6 +92,7 @@ def _create_navigation(self) -> None:
8592 self .addSubInterface (self .nodes_page , FIF .LINK , "Nodes" )
8693 self .addSubInterface (self .routing_page , FIF .GLOBE , "Routing" )
8794 self .addSubInterface (self .logs_page , FIF .DOCUMENT , "Logs" )
95+ self .addSubInterface (self .updates_page , FIF .UPDATE , "Updates" , NavigationItemPosition .BOTTOM )
8896 self .addSubInterface (self .settings_page , FIF .SETTING , "Settings" , NavigationItemPosition .BOTTOM )
8997
9098 def _create_tray (self ) -> None :
@@ -166,9 +174,9 @@ def _connect_signals(self) -> None:
166174 self .settings_page .set_password_requested .connect (self ._set_password )
167175 self .settings_page .disable_password_requested .connect (self .controller .disable_master_password )
168176 self .settings_page .lock_now_requested .connect (self .controller .lock )
169- self .settings_page . check_updates_requested .connect (self ._check_updates )
170- self .settings_page . check_xray_updates_requested .connect (lambda : self .controller .run_xray_core_update (False , silent = False ))
171- self .settings_page .update_xray_requested .connect (lambda : self .controller .run_xray_core_update (True , silent = False ))
177+ self .updates_page . check_app_requested .connect (self ._check_updates )
178+ self .updates_page . check_xray_requested .connect (lambda : self .controller .run_xray_core_update (False , silent = False ))
179+ self .updates_page .update_xray_requested .connect (lambda : self .controller .run_xray_core_update (True , silent = False ))
172180 self .settings_page .export_backup_requested .connect (self ._export_backup )
173181 self .settings_page .import_backup_requested .connect (self ._import_backup )
174182 self .settings_page .set_encryption_requested .connect (self ._set_encryption )
@@ -372,29 +380,57 @@ def _export_diagnostics(self) -> None:
372380 self ._show_status ("success" , f"Diagnostics exported: { path } " )
373381
374382 def _check_updates (self , silent : bool = False ) -> None :
375- settings = self .controller .state .settings
376- if not settings .allow_updates :
377- if not silent :
378- self ._show_status ("info" , "Updates disabled in settings" )
379- return
380- if not settings .update_feed_url :
383+ self ._pending_update : AppUpdate | None = None
384+ self ._update_checker = UpdateChecker (parent = self )
385+ self ._update_checker .result .connect (lambda u : self ._on_update_check_result (u , silent ))
386+ self ._update_checker .start ()
387+ if not silent :
388+ self .updates_page .show_checking ()
389+
390+ def _on_update_check_result (self , update : AppUpdate | None , silent : bool ) -> None :
391+ if update is None :
392+ self .updates_page .show_up_to_date ()
381393 if not silent :
382- self ._show_status ("warning " , "Update feed URL is empty " )
394+ self ._show_status ("info " , "You are on the latest version " )
383395 return
384396
385- try :
386- info = check_update (settings .update_feed_url , channel = settings .release_channel )
387- except Exception as exc :
388- if not silent :
389- self ._show_status ("error" , f"Update check failed: { exc } " )
390- else :
391- self .logs_page .append_line (f"[update] check failed: { exc } " )
392- return
397+ self ._pending_update = update
398+ self .updates_page .show_update_available (update .version )
399+ self .updates_page .download_btn .clicked .connect (
400+ lambda : self ._start_update_download (self ._pending_update )
401+ )
393402
394- if info :
395- self ._show_status ("success" , f"Latest { info .channel } : { info .version } " )
396- elif not silent :
397- self ._show_status ("info" , "No update info available" )
403+ # Blocking dialog
404+ from qfluentwidgets import MessageBox
405+ box = MessageBox (
406+ "Update available" ,
407+ f"New version v{ update .version } is available.\n "
408+ f"Current: v{ APP_VERSION } \n \n "
409+ f"The app will download, close, and restart automatically." ,
410+ self ,
411+ )
412+ box .yesButton .setText ("Download && Install" )
413+ box .cancelButton .setText ("Later" )
414+ if box .exec ():
415+ self ._start_update_download (update )
416+
417+ def _start_update_download (self , update : AppUpdate ) -> None :
418+ self .updates_page .show_download_progress (0 )
419+ self ._update_downloader = UpdateDownloader (update , parent = self )
420+ self ._update_downloader .progress .connect (self .updates_page .show_download_progress )
421+ self ._update_downloader .finished_ok .connect (self ._on_update_ready )
422+ self ._update_downloader .error .connect (self ._on_update_error )
423+ self ._update_downloader .start ()
424+
425+ def _on_update_ready (self ) -> None :
426+ self .updates_page .set_app_status ("Update downloaded. Restarting..." )
427+ self ._show_status ("success" , "Update downloaded. Restarting..." )
428+ QTimer .singleShot (1500 , lambda : QApplication .quit ())
429+
430+ def _on_update_error (self , err : str ) -> None :
431+ self .updates_page .show_idle ()
432+ self .updates_page .set_app_status (f"Update failed: { err } " )
433+ self ._show_status ("error" , f"Update failed: { err } " )
398434
399435 def _apply_theme (self , theme_name : str , accent_color : str ) -> None :
400436 normalized = theme_name .lower ().strip ()
0 commit comments