webdrop-bridge/src/webdrop_bridge/ui/main_window.py
claudi 86034358b7 Add URL whitelist enforcement for Kiosk-mode and enhance configuration management
- Introduced `allowed_urls` in configuration to specify whitelisted domains/patterns.
- Implemented `RestrictedWebEngineView` to enforce URL restrictions in the web view.
- Updated `MainWindow` to utilize the new restricted web view and added navigation toolbar.
- Enhanced unit tests for configuration and restricted web view to cover new functionality.
2026-01-28 11:33:37 +01:00

210 lines
6.6 KiB
Python

"""Main application window with web engine integration."""
from pathlib import Path
from typing import Optional
from PySide6.QtCore import QSize, Qt, QUrl
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QMainWindow, QToolBar, QVBoxLayout, QWidget
from webdrop_bridge.config import Config
from webdrop_bridge.core.drag_interceptor import DragInterceptor
from webdrop_bridge.core.validator import PathValidator
from webdrop_bridge.ui.restricted_web_view import RestrictedWebEngineView
class MainWindow(QMainWindow):
"""Main application window for WebDrop Bridge.
Displays web content in a QWebEngineView and provides drag-and-drop
integration with the native filesystem.
"""
def __init__(
self,
config: Config,
parent: Optional[QWidget] = None,
):
"""Initialize the main window.
Args:
config: Application configuration
parent: Parent widget
"""
super().__init__(parent)
self.config = config
# Set window properties
self.setWindowTitle(f"{config.app_name} v{config.app_version}")
self.setGeometry(
100,
100,
config.window_width,
config.window_height,
)
# Create web engine view
self.web_view = RestrictedWebEngineView(config.allowed_urls)
# Create navigation toolbar (Kiosk-mode navigation)
self._create_navigation_toolbar()
# Create drag interceptor
self.drag_interceptor = DragInterceptor()
# Set up path validator
validator = PathValidator(config.allowed_roots)
self.drag_interceptor.set_validator(validator)
# Connect drag interceptor signals
self.drag_interceptor.drag_started.connect(self._on_drag_started)
self.drag_interceptor.drag_failed.connect(self._on_drag_failed)
# Set up central widget with layout
central_widget = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.web_view)
layout.setContentsMargins(0, 0, 0, 0)
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
# Load web application
self._load_webapp()
# Apply styling if available
self._apply_stylesheet()
def _load_webapp(self) -> None:
"""Load the web application.
Loads HTML from the configured webapp URL or from local file.
"""
webapp_url = self.config.webapp_url
if webapp_url.startswith("http://") or webapp_url.startswith("https://"):
# Remote URL
self.web_view.load(QUrl(webapp_url))
else:
# Local file path
try:
file_path = Path(webapp_url).resolve()
if not file_path.exists():
self.web_view.setHtml(
f"<html><body><h1>Error</h1>"
f"<p>Web application file not found: {file_path}</p>"
f"</body></html>"
)
return
# Load local file as file:// URL
file_url = file_path.as_uri()
self.web_view.load(QUrl(file_url))
except (OSError, ValueError) as e:
self.web_view.setHtml(
f"<html><body><h1>Error</h1>"
f"<p>Failed to load web application: {e}</p>"
f"</body></html>"
)
def _apply_stylesheet(self) -> None:
"""Apply application stylesheet if available."""
stylesheet_path = Path(__file__).parent.parent.parent.parent / \
"resources" / "stylesheets" / "default.qss"
if stylesheet_path.exists():
try:
with open(stylesheet_path, "r") as f:
stylesheet = f.read()
self.setStyleSheet(stylesheet)
except (OSError, IOError):
# Silently fail if stylesheet can't be read
pass
def _on_drag_started(self, paths: list) -> None:
"""Handle successful drag initiation.
Args:
paths: List of paths that were dragged
"""
# Can be extended with logging or status bar updates
pass
def _on_drag_failed(self, error: str) -> None:
"""Handle drag operation failure.
Args:
error: Error message
"""
# Can be extended with logging or user notification
pass
def _create_navigation_toolbar(self) -> None:
"""Create navigation toolbar with Home, Back, Forward, Refresh buttons.
In Kiosk-mode, users can navigate history but cannot freely browse.
"""
toolbar = QToolBar("Navigation")
toolbar.setMovable(False)
toolbar.setIconSize(QSize(24, 24))
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar)
# Back button
back_action = self.web_view.pageAction(
self.web_view.WebAction.Back
)
toolbar.addAction(back_action)
# Forward button
forward_action = self.web_view.pageAction(
self.web_view.WebAction.Forward
)
toolbar.addAction(forward_action)
# Separator
toolbar.addSeparator()
# Home button
home_action = toolbar.addAction("Home")
home_action.triggered.connect(self._navigate_home)
# Refresh button
refresh_action = self.web_view.pageAction(
self.web_view.WebAction.Reload
)
toolbar.addAction(refresh_action)
def _navigate_home(self) -> None:
"""Navigate to the home (start) URL."""
home_url = self.config.webapp_url
if home_url.startswith("http://") or home_url.startswith("https://"):
self.web_view.load(QUrl(home_url))
else:
try:
file_path = Path(home_url).resolve()
file_url = file_path.as_uri()
self.web_view.load(QUrl(file_url))
except (OSError, ValueError):
pass
def closeEvent(self, event) -> None:
"""Handle window close event.
Args:
event: Close event
"""
# Can be extended with save operations or cleanup
event.accept()
def initiate_drag(self, file_paths: list) -> bool:
"""Initiate a drag operation for the given files.
Called from web content via JavaScript bridge.
Args:
file_paths: List of file paths to drag
Returns:
True if drag was initiated successfully
"""
return self.drag_interceptor.initiate_drag(file_paths)