feat: Enhance Help menu with About dialog and update check functionality
This commit is contained in:
parent
eb7ffe9969
commit
6278ef8eed
3 changed files with 196 additions and 16 deletions
|
|
@ -364,7 +364,14 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
# Check for Updates action
|
# Check for Updates action
|
||||||
check_updates_action = help_menu.addAction("Check for Updates...")
|
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:
|
def _create_status_bar(self) -> None:
|
||||||
"""Create status bar with update status indicator."""
|
"""Create status bar with update status indicator."""
|
||||||
|
|
@ -387,13 +394,30 @@ class MainWindow(QMainWindow):
|
||||||
else:
|
else:
|
||||||
self.update_status_label.setText(status)
|
self.update_status_label.setText(status)
|
||||||
|
|
||||||
def _on_check_for_updates(self) -> None:
|
def _on_manual_check_for_updates(self) -> None:
|
||||||
"""Handle check for updates menu action.
|
"""Handle manual check for updates from menu.
|
||||||
|
|
||||||
Emits the check_for_updates signal to allow the main application
|
Triggers an immediate update check (bypass cache).
|
||||||
to perform the update check asynchronously.
|
|
||||||
"""
|
"""
|
||||||
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"<b>{self.config.app_name}</b><br>"
|
||||||
|
f"Version: {self.config.app_version}<br>"
|
||||||
|
f"<br>"
|
||||||
|
f"A professional Qt-based desktop application that converts "
|
||||||
|
f"web-based drag-and-drop text paths into native file operations.<br>"
|
||||||
|
f"<br>"
|
||||||
|
f"<small>© 2026 WebDrop Bridge Contributors</small>"
|
||||||
|
)
|
||||||
|
|
||||||
|
QMessageBox.about(self, f"About {self.config.app_name}", about_text)
|
||||||
|
|
||||||
def _navigate_home(self) -> None:
|
def _navigate_home(self) -> None:
|
||||||
"""Navigate to the home (start) URL."""
|
"""Navigate to the home (start) URL."""
|
||||||
|
|
@ -490,8 +514,147 @@ class MainWindow(QMainWindow):
|
||||||
# Update status to show update available
|
# Update status to show update available
|
||||||
self.set_update_status(f"Update available: v{release.version}", emoji="✅")
|
self.set_update_status(f"Update available: v{release.version}", emoji="✅")
|
||||||
|
|
||||||
# Emit signal for main app to show dialog
|
# Show update available dialog
|
||||||
self.update_available.emit(release)
|
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:
|
class UpdateCheckWorker:
|
||||||
|
|
|
||||||
|
|
@ -346,13 +346,22 @@ class TestMainWindowMenuBar:
|
||||||
assert callable(window.check_for_updates.emit)
|
assert callable(window.check_for_updates.emit)
|
||||||
|
|
||||||
def test_on_check_for_updates_method_exists(self, qtbot, sample_config):
|
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)
|
window = MainWindow(sample_config)
|
||||||
qtbot.addWidget(window)
|
qtbot.addWidget(window)
|
||||||
|
|
||||||
# Test that the method exists
|
# Test that the method exists
|
||||||
assert hasattr(window, "_on_check_for_updates")
|
assert hasattr(window, "_on_manual_check_for_updates")
|
||||||
assert callable(window._on_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:
|
class TestMainWindowStatusBar:
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,9 @@ class TestMainWindowStartupCheck:
|
||||||
assert "✓" in window.update_status_label.text()
|
assert "✓" in window.update_status_label.text()
|
||||||
|
|
||||||
def test_on_update_available_emits_signal(self, qtbot, sample_config):
|
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 webdrop_bridge.ui.main_window import MainWindow
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
window = MainWindow(sample_config)
|
window = MainWindow(sample_config)
|
||||||
qtbot.addWidget(window)
|
qtbot.addWidget(window)
|
||||||
|
|
@ -108,13 +109,17 @@ class TestMainWindowStartupCheck:
|
||||||
# Create mock release
|
# Create mock release
|
||||||
mock_release = MagicMock()
|
mock_release = MagicMock()
|
||||||
mock_release.version = "0.0.2"
|
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)
|
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):
|
def test_on_update_available_updates_status(self, qtbot, sample_config):
|
||||||
"""Test _on_update_available updates status bar."""
|
"""Test _on_update_available updates status bar."""
|
||||||
from webdrop_bridge.ui.main_window import MainWindow
|
from webdrop_bridge.ui.main_window import MainWindow
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
window = MainWindow(sample_config)
|
window = MainWindow(sample_config)
|
||||||
qtbot.addWidget(window)
|
qtbot.addWidget(window)
|
||||||
|
|
@ -122,7 +127,10 @@ class TestMainWindowStartupCheck:
|
||||||
# Create mock release
|
# Create mock release
|
||||||
mock_release = MagicMock()
|
mock_release = MagicMock()
|
||||||
mock_release.version = "0.0.2"
|
mock_release.version = "0.0.2"
|
||||||
|
mock_release.body = "Bug fixes"
|
||||||
|
|
||||||
|
# Mock the dialog creation to avoid showing it
|
||||||
|
with patch('webdrop_bridge.ui.update_manager_ui.UpdateAvailableDialog'):
|
||||||
window._on_update_available(mock_release)
|
window._on_update_available(mock_release)
|
||||||
assert "0.0.2" in window.update_status_label.text()
|
assert "0.0.2" in window.update_status_label.text()
|
||||||
assert "✅" in window.update_status_label.text()
|
assert "✅" in window.update_status_label.text()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue