From 6278ef8eedefbb669065ec3a56cfbc5e67588f13 Mon Sep 17 00:00:00 2001 From: claudi Date: Thu, 29 Jan 2026 09:09:04 +0100 Subject: [PATCH] feat: Enhance Help menu with About dialog and update check functionality --- src/webdrop_bridge/ui/main_window.py | 179 +++++++++++++++++++++++++-- tests/unit/test_main_window.py | 15 ++- tests/unit/test_startup_check.py | 18 ++- 3 files changed, 196 insertions(+), 16 deletions(-) diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index 850d8de..3de11da 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -364,7 +364,14 @@ class MainWindow(QMainWindow): # Check for Updates action check_updates_action = help_menu.addAction("Check for Updates...") - check_updates_action.triggered.connect(self._on_check_for_updates) + check_updates_action.triggered.connect(self._on_manual_check_for_updates) + + # Separator + help_menu.addSeparator() + + # About action + about_action = help_menu.addAction("About WebDrop Bridge...") + about_action.triggered.connect(self._show_about_dialog) def _create_status_bar(self) -> None: """Create status bar with update status indicator.""" @@ -387,13 +394,30 @@ class MainWindow(QMainWindow): else: self.update_status_label.setText(status) - def _on_check_for_updates(self) -> None: - """Handle check for updates menu action. + def _on_manual_check_for_updates(self) -> None: + """Handle manual check for updates from menu. - Emits the check_for_updates signal to allow the main application - to perform the update check asynchronously. + Triggers an immediate update check (bypass cache). """ - self.check_for_updates.emit() + logger.info("Manual update check requested from menu") + # Same as startup check, but user-initiated + self.check_for_updates_startup() + + def _show_about_dialog(self) -> None: + """Show About dialog with version and information.""" + from PySide6.QtWidgets import QMessageBox + + about_text = ( + f"{self.config.app_name}
" + f"Version: {self.config.app_version}
" + f"
" + f"A professional Qt-based desktop application that converts " + f"web-based drag-and-drop text paths into native file operations.
" + f"
" + f"© 2026 WebDrop Bridge Contributors" + ) + + QMessageBox.about(self, f"About {self.config.app_name}", about_text) def _navigate_home(self) -> None: """Navigate to the home (start) URL.""" @@ -490,8 +514,147 @@ class MainWindow(QMainWindow): # Update status to show update available self.set_update_status(f"Update available: v{release.version}", emoji="✅") - # Emit signal for main app to show dialog - self.update_available.emit(release) + # Show update available dialog + from webdrop_bridge.ui.update_manager_ui import UpdateAvailableDialog + + dialog = UpdateAvailableDialog( + version=release.version, + changelog=release.body, + parent=self + ) + + # Connect dialog signals + dialog.update_now.connect(lambda: self._on_user_update_now(release)) + dialog.update_later.connect(lambda: self._on_user_update_later()) + dialog.skip_version.connect(lambda: self._on_user_skip_version(release.version)) + + # Show dialog (modal) + dialog.exec() + + def _on_user_update_now(self, release) -> None: + """Handle user clicking 'Update Now' button. + + Args: + release: Release object to download and install + """ + logger.info(f"User clicked 'Update Now' for v{release.version}") + + # Start download + self._start_update_download(release) + + def _on_user_update_later(self) -> None: + """Handle user clicking 'Later' button.""" + logger.info("User deferred update") + self.set_update_status("Update deferred", emoji="") + + def _on_user_skip_version(self, version: str) -> None: + """Handle user clicking 'Skip Version' button. + + Args: + version: Version to skip + """ + logger.info(f"User skipped version {version}") + + # Store skipped version in preferences + skipped_file = Path.home() / ".webdrop-bridge" / "skipped_version.txt" + skipped_file.parent.mkdir(parents=True, exist_ok=True) + skipped_file.write_text(version) + + self.set_update_status(f"Skipped v{version}", emoji="") + + def _start_update_download(self, release) -> None: + """Start downloading the update. + + Args: + release: Release object to download + """ + logger.info(f"Starting download for v{release.version}") + self.set_update_status(f"Downloading v{release.version}", emoji="⬇️") + + # For now, just start installer directly (simplified) + # In production, would show download progress dialog + self._perform_update(release) + + def _perform_update(self, release) -> None: + """Download and install the update. + + Args: + release: Release object to download and install + """ + from webdrop_bridge.core.updater import UpdateManager + from webdrop_bridge.ui.update_manager_ui import InstallDialog + + try: + logger.info(f"Downloading and installing v{release.version}") + + # Create update manager + manager = UpdateManager( + current_version=self.config.app_version, + config_dir=Path.home() / ".webdrop-bridge" + ) + + # Download synchronously for simplicity + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + installer_path = loop.run_until_complete( + manager.download_update(release) + ) + + if not installer_path: + self.set_update_status("Download failed", emoji="❌") + logger.error("Download failed - no installer found") + return + + logger.info(f"Downloaded to {installer_path}") + + # Verify checksum + checksum_ok = loop.run_until_complete( + manager.verify_checksum(installer_path, release) + ) + + loop.close() + + if not checksum_ok: + self.set_update_status("Checksum verification failed", emoji="❌") + logger.error("Checksum verification failed") + return + + logger.info("Checksum verification passed") + self.set_update_status(f"Ready to install v{release.version}", emoji="✅") + + # Show install confirmation dialog + install_dialog = InstallDialog(parent=self) + install_dialog.install_now.connect( + lambda: self._do_install(installer_path) + ) + install_dialog.exec() + + except Exception as e: + logger.error(f"Update failed: {e}") + self.set_update_status(f"Update failed: {str(e)[:30]}", emoji="❌") + + def _do_install(self, installer_path: Path) -> None: + """Execute the installer. + + Args: + installer_path: Path to installer executable + """ + logger.info(f"Installing from {installer_path}") + + from webdrop_bridge.core.updater import UpdateManager + + manager = UpdateManager( + current_version=self.config.app_version, + config_dir=Path.home() / ".webdrop-bridge" + ) + + if manager.install_update(installer_path): + self.set_update_status("Installation started", emoji="✅") + logger.info("Update installer launched successfully") + else: + self.set_update_status("Installation failed", emoji="❌") + logger.error("Failed to launch update installer") class UpdateCheckWorker: diff --git a/tests/unit/test_main_window.py b/tests/unit/test_main_window.py index 48b7c97..d7f7321 100644 --- a/tests/unit/test_main_window.py +++ b/tests/unit/test_main_window.py @@ -346,13 +346,22 @@ class TestMainWindowMenuBar: assert callable(window.check_for_updates.emit) def test_on_check_for_updates_method_exists(self, qtbot, sample_config): - """Test _on_check_for_updates method exists.""" + """Test _on_manual_check_for_updates method exists.""" window = MainWindow(sample_config) qtbot.addWidget(window) # Test that the method exists - assert hasattr(window, "_on_check_for_updates") - assert callable(window._on_check_for_updates) + assert hasattr(window, "_on_manual_check_for_updates") + assert callable(window._on_manual_check_for_updates) + + def test_show_about_dialog_method_exists(self, qtbot, sample_config): + """Test _show_about_dialog method exists.""" + window = MainWindow(sample_config) + qtbot.addWidget(window) + + # Test that the method exists + assert hasattr(window, "_show_about_dialog") + assert callable(window._show_about_dialog) class TestMainWindowStatusBar: diff --git a/tests/unit/test_startup_check.py b/tests/unit/test_startup_check.py index b64d912..1065743 100644 --- a/tests/unit/test_startup_check.py +++ b/tests/unit/test_startup_check.py @@ -99,8 +99,9 @@ class TestMainWindowStartupCheck: assert "✓" in window.update_status_label.text() def test_on_update_available_emits_signal(self, qtbot, sample_config): - """Test _on_update_available emits update_available signal.""" + """Test _on_update_available shows dialog and updates status.""" from webdrop_bridge.ui.main_window import MainWindow + from unittest.mock import patch window = MainWindow(sample_config) qtbot.addWidget(window) @@ -108,13 +109,17 @@ class TestMainWindowStartupCheck: # Create mock release mock_release = MagicMock() mock_release.version = "0.0.2" + mock_release.body = "Bug fixes" - with qtbot.waitSignal(window.update_available): + # Mock the dialog creation to avoid showing it + with patch('webdrop_bridge.ui.update_manager_ui.UpdateAvailableDialog'): window._on_update_available(mock_release) + assert "0.0.2" in window.update_status_label.text() def test_on_update_available_updates_status(self, qtbot, sample_config): """Test _on_update_available updates status bar.""" from webdrop_bridge.ui.main_window import MainWindow + from unittest.mock import patch window = MainWindow(sample_config) qtbot.addWidget(window) @@ -122,7 +127,10 @@ class TestMainWindowStartupCheck: # Create mock release mock_release = MagicMock() mock_release.version = "0.0.2" + mock_release.body = "Bug fixes" - window._on_update_available(mock_release) - assert "0.0.2" in window.update_status_label.text() - assert "✅" in window.update_status_label.text() + # Mock the dialog creation to avoid showing it + with patch('webdrop_bridge.ui.update_manager_ui.UpdateAvailableDialog'): + window._on_update_available(mock_release) + assert "0.0.2" in window.update_status_label.text() + assert "✅" in window.update_status_label.text()