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()