From 88dc35889456f9b9e4d2c6f054b33ef58a835c95 Mon Sep 17 00:00:00 2001 From: claudi Date: Tue, 17 Feb 2026 15:56:53 +0100 Subject: [PATCH 01/16] Refactor drag handling and update tests - Renamed `initiate_drag` to `handle_drag` in MainWindow and updated related tests. - Improved drag handling logic to utilize a bridge for starting file drags. - Updated `_on_drag_started` and `_on_drag_failed` methods to match new signatures. - Modified test cases to reflect changes in drag handling and assertions. Enhance path validation and logging - Updated `PathValidator` to log warnings for nonexistent roots instead of raising errors. - Adjusted tests to verify the new behavior of skipping nonexistent roots. Update web application UI and functionality - Changed displayed text for drag items to reflect local paths and Azure Blob Storage URLs. - Added debug logging for drag operations in the web application. - Improved instructions for testing drag and drop functionality. Add configuration documentation and example files - Created `CONFIG_README.md` to provide detailed configuration instructions for WebDrop Bridge. - Added `config.example.json` and `config_test.json` for reference and testing purposes. Implement URL conversion logic - Introduced `URLConverter` class to handle conversion of Azure Blob Storage URLs to local paths. - Added unit tests for URL conversion to ensure correct functionality. Develop download interceptor script - Created `download_interceptor.js` to intercept download-related actions in the web application. - Implemented logging for fetch calls, XMLHttpRequests, and Blob URL creations. Add download test page and related tests - Created `test_download.html` for testing various download scenarios. - Implemented `test_download.py` to verify download path resolution and file construction. - Added `test_url_mappings.py` to ensure URL mappings are loaded correctly. Add unit tests for URL converter - Created `test_url_converter.py` to validate URL conversion logic and mapping behavior. --- CONFIG_README.md | 209 ++++++++++ config.example.json | 22 ++ config_test.json | 15 + src/webdrop_bridge/config.py | 199 +++++++++- src/webdrop_bridge/core/drag_interceptor.py | 158 +++++--- src/webdrop_bridge/core/url_converter.py | 86 ++++ src/webdrop_bridge/core/validator.py | 64 +-- src/webdrop_bridge/main.py | 8 +- src/webdrop_bridge/ui/bridge_script.js | 213 ++++++++-- src/webdrop_bridge/ui/download_interceptor.js | 72 ++++ src/webdrop_bridge/ui/main_window.py | 373 ++++++++++++++++-- src/webdrop_bridge/ui/restricted_web_view.py | 162 +++++++- test_download.html | 95 +++++ test_download.py | 19 + test_url_mappings.py | 24 ++ tests/unit/test_config.py | 7 +- tests/unit/test_drag_interceptor.py | 336 ++++++---------- tests/unit/test_main_window.py | 25 +- tests/unit/test_url_converter.py | 144 +++++++ tests/unit/test_validator.py | 7 +- webapp/index.html | 64 ++- 21 files changed, 1870 insertions(+), 432 deletions(-) create mode 100644 CONFIG_README.md create mode 100644 config.example.json create mode 100644 config_test.json create mode 100644 src/webdrop_bridge/core/url_converter.py create mode 100644 src/webdrop_bridge/ui/download_interceptor.js create mode 100644 test_download.html create mode 100644 test_download.py create mode 100644 test_url_mappings.py create mode 100644 tests/unit/test_url_converter.py diff --git a/CONFIG_README.md b/CONFIG_README.md new file mode 100644 index 0000000..261e7db --- /dev/null +++ b/CONFIG_README.md @@ -0,0 +1,209 @@ +# WebDrop Bridge Configuration Guide + +## Configuration File Location + +WebDrop Bridge supports two configuration methods: + +1. **JSON Configuration File** (Recommended for Azure URL mapping) + - Windows: `%APPDATA%\webdrop_bridge\config.json` + - macOS/Linux: `~/.config/webdrop_bridge/config.json` + +2. **Environment Variables** (`.env` file in project root) + - Used as fallback if JSON config doesn't exist + +## JSON Configuration Format + +Create a `config.json` file with the following structure: + +```json +{ + "app_name": "WebDrop Bridge", + "webapp_url": "https://wps.agravity.io/", + "url_mappings": [ + { + "url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/", + "local_path": "Z:" + } + ], + "allowed_roots": [ + "Z:\\" + ], + "allowed_urls": [], + "check_file_exists": true, + "auto_check_updates": true, + "update_check_interval_hours": 24, + "log_level": "INFO", + "log_file": "logs/webdrop_bridge.log", + "window_width": 1024, + "window_height": 768, + "enable_logging": true +} +``` + +## Configuration Options + +### Core Settings + +- **`webapp_url`** (string): URL of the web application to load + - Example: `"https://wps.agravity.io/"` + - Supports `http://`, `https://`, or `file:///` URLs + +- **`url_mappings`** (array): Azure Blob Storage URL to local path mappings + - Each mapping has: + - `url_prefix`: Azure URL prefix (must end with `/`) + - `local_path`: Local drive letter or path + - Example: + ```json + { + "url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/", + "local_path": "Z:" + } + ``` + +- **`allowed_roots`** (array): Whitelisted root directories for file access + - Security feature: Only files within these directories can be dragged + - Example: `["Z:\\", "C:\\Users\\Public"]` + +### Azure URL Mapping Example + +When the web application provides a drag URL like: +``` +https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png +``` + +It will be converted to: +``` +Z:\aN5PysnXIuRECzcRbvHkjL7g0\Hintergrund_Agravity.png +``` + +### Security Settings + +- **`check_file_exists`** (boolean): Validate files exist before allowing drag + - Default: `true` + - Set to `false` only for testing + +- **`allowed_urls`** (array): Allowed URL patterns for web content + - Empty array = no restriction + - Example: `["wps.agravity.io", "*.example.com"]` + +### Update Settings + +- **`auto_check_updates`** (boolean): Automatically check for updates on startup + - Default: `true` + +- **`update_check_interval_hours`** (number): Hours between update checks + - Default: `24` + +### UI Settings + +- **`window_width`**, **`window_height`** (number): Initial window size in pixels + - Default: `1024` x `768` + +- **`log_level`** (string): Logging verbosity + - Options: `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`, `"CRITICAL"` + - Default: `"INFO"` + +- **`enable_logging`** (boolean): Whether to write logs to file + - Default: `true` + +## Quick Start + +1. Copy `config.example.json` to your config directory: + ```powershell + # Windows + mkdir "$env:APPDATA\webdrop_bridge" + copy config.example.json "$env:APPDATA\webdrop_bridge\config.json" + ``` + +2. Edit the configuration file with your Azure URL mappings and local paths + +3. Restart WebDrop Bridge + +## Multiple URL Mappings + +You can configure multiple Azure storage accounts: + +```json +{ + "url_mappings": [ + { + "url_prefix": "https://storage1.file.core.windows.net/container1/", + "local_path": "Z:" + }, + { + "url_prefix": "https://storage2.file.core.windows.net/container2/", + "local_path": "Y:" + } + ], + "allowed_roots": [ + "Z:\\", + "Y:\\" + ] +} +``` + +## Environment Variable Fallback + +If no JSON config exists, WebDrop Bridge will load from `.env`: + +```env +APP_NAME=WebDrop Bridge +WEBAPP_URL=https://wps.agravity.io/ +ALLOWED_ROOTS=Z:/ +LOG_LEVEL=INFO +WINDOW_WIDTH=1024 +WINDOW_HEIGHT=768 +``` + +**Note:** Environment variables don't support `url_mappings`. Use JSON config for Azure URL mapping. + +## Troubleshooting + +### "No mapping found for URL" +- Check that `url_prefix` matches the Azure URL exactly (including trailing `/`) +- Verify `url_mappings` is configured in your JSON config file +- Check logs in `logs/webdrop_bridge.log` + +### "Path is not within allowed roots" +- Ensure the mapped local path (e.g., `Z:`) is listed in `allowed_roots` +- Make sure the drive is mounted and accessible + +### "File does not exist" +- Verify the Azure sync is working and files are available locally +- Set `check_file_exists: false` temporarily for testing +- Check that the path mapping is correct + +## Example Configurations + +### Production (Agravity WPS) + +```json +{ + "webapp_url": "https://wps.agravity.io/", + "url_mappings": [ + { + "url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/", + "local_path": "Z:" + } + ], + "allowed_roots": ["Z:\\"], + "log_level": "INFO" +} +``` + +### Development (Local Testing) + +```json +{ + "webapp_url": "file:///./webapp/index.html", + "url_mappings": [ + { + "url_prefix": "https://test.blob.core.windows.net/test/", + "local_path": "C:\\temp\\test" + } + ], + "allowed_roots": ["C:\\temp\\test"], + "check_file_exists": false, + "log_level": "DEBUG" +} +``` diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..8da1b1b --- /dev/null +++ b/config.example.json @@ -0,0 +1,22 @@ +{ + "app_name": "WebDrop Bridge", + "webapp_url": "https://wps.agravity.io/", + "url_mappings": [ + { + "url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/", + "local_path": "Z:" + } + ], + "allowed_roots": [ + "Z:\\" + ], + "allowed_urls": [], + "check_file_exists": true, + "auto_check_updates": true, + "update_check_interval_hours": 24, + "log_level": "INFO", + "log_file": "logs/webdrop_bridge.log", + "window_width": 1024, + "window_height": 768, + "enable_logging": true +} diff --git a/config_test.json b/config_test.json new file mode 100644 index 0000000..3efdfcf --- /dev/null +++ b/config_test.json @@ -0,0 +1,15 @@ +{ + "app_name": "WebDrop Bridge - Download Test", + "webapp_url": "test_download.html", + "url_mappings": [], + "allowed_roots": [], + "allowed_urls": [], + "check_file_exists": true, + "auto_check_updates": false, + "update_check_interval_hours": 24, + "log_level": "DEBUG", + "log_file": "logs/webdrop_bridge.log", + "window_width": 1024, + "window_height": 768, + "enable_logging": true +} diff --git a/src/webdrop_bridge/config.py b/src/webdrop_bridge/config.py index ea0c5a6..667bd04 100644 --- a/src/webdrop_bridge/config.py +++ b/src/webdrop_bridge/config.py @@ -1,8 +1,9 @@ """Configuration management for WebDrop Bridge application.""" +import json import logging import os -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import List @@ -17,9 +18,29 @@ class ConfigurationError(Exception): pass +@dataclass +class URLMapping: + """Maps an Azure Blob Storage URL prefix to a local drive path.""" + + url_prefix: str + local_path: str + + def __post_init__(self): + """Validate mapping configuration.""" + if not self.url_prefix.startswith(("http://", "https://")): + raise ConfigurationError( + f"URL prefix must start with http:// or https://: {self.url_prefix}" + ) + # Ensure URL prefix ends with / + if not self.url_prefix.endswith("/"): + self.url_prefix += "/" + # Normalize local path + self.local_path = str(Path(self.local_path)) + + @dataclass class Config: - """Application configuration loaded from environment variables. + """Application configuration loaded from environment variables or JSON file. Attributes: app_name: Application display name @@ -28,7 +49,11 @@ class Config: log_file: Optional log file path allowed_roots: List of whitelisted root directories for file access allowed_urls: List of whitelisted URL domains/patterns (empty = no restriction) - webapp_url: URL to load in embedded web application + webapp_url: URL to load in embedded web application (default: https://wps.agravity.io/) + url_mappings: List of Azure URL to local path mappings + check_file_exists: Whether to validate that files exist before drag + auto_check_updates: Whether to automatically check for updates + update_check_interval_hours: Hours between update checks window_width: Initial window width in pixels window_height: Initial window height in pixels window_title: Main window title (default: "{app_name} v{app_version}") @@ -45,10 +70,85 @@ class Config: allowed_roots: List[Path] allowed_urls: List[str] webapp_url: str - window_width: int - window_height: int - window_title: str - enable_logging: bool + url_mappings: List[URLMapping] = field(default_factory=list) + check_file_exists: bool = True + auto_check_updates: bool = True + update_check_interval_hours: int = 24 + window_width: int = 1024 + window_height: int = 768 + window_title: str = "" + enable_logging: bool = True + + @classmethod + def from_file(cls, config_path: Path) -> "Config": + """Load configuration from JSON file. + + Args: + config_path: Path to configuration file + + Returns: + Config: Configured instance from JSON file + + Raises: + ConfigurationError: If configuration file is invalid + """ + if not config_path.exists(): + raise ConfigurationError(f"Configuration file not found: {config_path}") + + try: + with open(config_path, "r", encoding="utf-8") as f: + data = json.load(f) + except (json.JSONDecodeError, IOError) as e: + raise ConfigurationError(f"Failed to load configuration: {e}") from e + + # Get version from package + from webdrop_bridge import __version__ + + # Parse URL mappings + mappings = [ + URLMapping( + url_prefix=m["url_prefix"], + local_path=m["local_path"] + ) + for m in data.get("url_mappings", []) + ] + + # Parse allowed roots + allowed_roots = [Path(p).resolve() for p in data.get("allowed_roots", [])] + + # Validate allowed roots exist + for root in allowed_roots: + if not root.exists(): + logger.warning(f"Allowed root does not exist: {root}") + elif not root.is_dir(): + raise ConfigurationError(f"Allowed root is not a directory: {root}") + + # Get log file path + log_file = None + if data.get("enable_logging", True): + log_file_str = data.get("log_file", "logs/webdrop_bridge.log") + log_file = Path(log_file_str).resolve() + + app_name = data.get("app_name", "WebDrop Bridge") + window_title = data.get("window_title", f"{app_name} v{__version__}") + + return cls( + app_name=app_name, + app_version=__version__, + log_level=data.get("log_level", "INFO").upper(), + log_file=log_file, + allowed_roots=allowed_roots, + allowed_urls=data.get("allowed_urls", []), + webapp_url=data.get("webapp_url", "https://wps.agravity.io/"), + url_mappings=mappings, + check_file_exists=data.get("check_file_exists", True), + auto_check_updates=data.get("auto_check_updates", True), + update_check_interval_hours=data.get("update_check_interval_hours", 24), + window_width=data.get("window_width", 1024), + window_height=data.get("window_height", 768), + window_title=window_title, + enable_logging=data.get("enable_logging", True), + ) @classmethod def from_env(cls, env_file: str | None = None) -> "Config": @@ -81,7 +181,7 @@ class Config: log_file_str = os.getenv("LOG_FILE", "logs/webdrop_bridge.log") allowed_roots_str = os.getenv("ALLOWED_ROOTS", "Z:/,C:/Users/Public") allowed_urls_str = os.getenv("ALLOWED_URLS", "") - webapp_url = os.getenv("WEBAPP_URL", "file:///./webapp/index.html") + webapp_url = os.getenv("WEBAPP_URL", "https://wps.agravity.io/") window_width = int(os.getenv("WINDOW_WIDTH", "1024")) window_height = int(os.getenv("WINDOW_HEIGHT", "768")) # Window title defaults to app_name + version if not specified @@ -103,14 +203,13 @@ class Config: for p in allowed_roots_str.split(","): root_path = Path(p.strip()).resolve() if not root_path.exists(): - raise ConfigurationError( - f"Allowed root '{p.strip()}' does not exist" - ) - if not root_path.is_dir(): + logger.warning(f"Allowed root does not exist: {p.strip()}") + elif not root_path.is_dir(): raise ConfigurationError( f"Allowed root '{p.strip()}' is not a directory" ) - allowed_roots.append(root_path) + else: + allowed_roots.append(root_path) except ConfigurationError: raise except (ValueError, OSError) as e: @@ -140,6 +239,32 @@ class Config: if url.strip() ] if allowed_urls_str else [] + # Parse URL mappings (Azure Blob Storage → Local Paths) + # Format: url_prefix1=local_path1;url_prefix2=local_path2 + url_mappings_str = os.getenv("URL_MAPPINGS", "") + url_mappings = [] + if url_mappings_str: + try: + for mapping in url_mappings_str.split(";"): + mapping = mapping.strip() + if not mapping: + continue + if "=" not in mapping: + raise ConfigurationError( + f"Invalid URL mapping format: {mapping}. Expected 'url=path'" + ) + url_prefix, local_path_str = mapping.split("=", 1) + url_mappings.append( + URLMapping( + url_prefix=url_prefix.strip(), + local_path=local_path_str.strip() + ) + ) + except (ValueError, OSError) as e: + raise ConfigurationError( + f"Invalid URL_MAPPINGS: {url_mappings_str}. Error: {e}" + ) from e + return cls( app_name=app_name, app_version=app_version, @@ -148,12 +273,60 @@ class Config: allowed_roots=allowed_roots, allowed_urls=allowed_urls, webapp_url=webapp_url, + url_mappings=url_mappings, window_width=window_width, window_height=window_height, window_title=window_title, enable_logging=enable_logging, ) + def to_file(self, config_path: Path) -> None: + """Save configuration to JSON file. + + Args: + config_path: Path to save configuration + """ + data = { + "app_name": self.app_name, + "webapp_url": self.webapp_url, + "url_mappings": [ + { + "url_prefix": m.url_prefix, + "local_path": m.local_path + } + for m in self.url_mappings + ], + "allowed_roots": [str(p) for p in self.allowed_roots], + "allowed_urls": self.allowed_urls, + "check_file_exists": self.check_file_exists, + "auto_check_updates": self.auto_check_updates, + "update_check_interval_hours": self.update_check_interval_hours, + "log_level": self.log_level, + "log_file": str(self.log_file) if self.log_file else None, + "window_width": self.window_width, + "window_height": self.window_height, + "window_title": self.window_title, + "enable_logging": self.enable_logging, + } + + config_path.parent.mkdir(parents=True, exist_ok=True) + with open(config_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + @staticmethod + def get_default_config_path() -> Path: + """Get the default configuration file path. + + Returns: + Path to default config file + """ + import platform + if platform.system() == "Windows": + base = Path.home() / "AppData" / "Roaming" + else: + base = Path.home() / ".config" + return base / "webdrop_bridge" / "config.json" + def __repr__(self) -> str: """Return developer-friendly representation.""" return ( diff --git a/src/webdrop_bridge/core/drag_interceptor.py b/src/webdrop_bridge/core/drag_interceptor.py index 14502b5..f00eae2 100644 --- a/src/webdrop_bridge/core/drag_interceptor.py +++ b/src/webdrop_bridge/core/drag_interceptor.py @@ -1,5 +1,6 @@ """Qt widget for intercepting drag events and initiating native drag operations.""" +import logging from pathlib import Path from typing import List, Optional @@ -7,98 +8,129 @@ from PySide6.QtCore import QMimeData, Qt, QUrl, Signal from PySide6.QtGui import QDrag from PySide6.QtWidgets import QWidget +from webdrop_bridge.config import Config +from webdrop_bridge.core.url_converter import URLConverter from webdrop_bridge.core.validator import PathValidator, ValidationError +logger = logging.getLogger(__name__) + class DragInterceptor(QWidget): - """Widget that handles drag initiation for file paths. + """Widget that handles drag initiation for file paths or Azure URLs. - Intercepts drag events from web content and initiates native Qt drag - operations, allowing files to be dragged from web content to native - applications. + Intercepts drag events from web content, converts Azure Blob Storage URLs + to local paths, validates them, and initiates native Qt drag operations. Signals: drag_started: Emitted when a drag operation begins successfully drag_failed: Emitted when drag initiation fails """ - # Signals with string parameters (file paths that were dragged) - drag_started = Signal(list) # List[str] - list of file paths - drag_failed = Signal(str) # str - error message + # Signals with string parameters + drag_started = Signal(str, str) # (url_or_path, local_path) + drag_failed = Signal(str, str) # (url_or_path, error_message) - def __init__(self, parent: Optional[QWidget] = None): + def __init__(self, config: Config, parent: Optional[QWidget] = None): """Initialize the drag interceptor. Args: + config: Application configuration parent: Parent widget """ super().__init__(parent) - self._validator: Optional[PathValidator] = None + self.config = config + self._validator = PathValidator( + config.allowed_roots, + check_file_exists=config.check_file_exists + ) + self._url_converter = URLConverter(config) - def set_validator(self, validator: PathValidator) -> None: - """Set the path validator for this interceptor. + def handle_drag(self, text: str) -> bool: + """Handle drag event from web view. + + Determines if the text is an Azure URL or file path, converts if needed, + validates, and initiates native drag operation. Args: - validator: PathValidator instance to use for validation - """ - self._validator = validator - - def initiate_drag(self, file_paths: List[str]) -> bool: - """Initiate a native drag operation for the given files. - - Args: - file_paths: List of file paths to drag + text: Azure Blob Storage URL or file path from web drag Returns: - True if drag was successfully initiated, False otherwise + True if native drag was initiated, False otherwise """ - if not file_paths: - self.drag_failed.emit("No files to drag") + if not text or not text.strip(): + error_msg = "Empty drag text" + logger.warning(error_msg) + self.drag_failed.emit("", error_msg) return False - if not self._validator: - self.drag_failed.emit("Validator not configured") - return False + text = text.strip() + logger.debug(f"Handling drag for text: {text}") - # Validate all paths first - validated_paths = [] - for path_str in file_paths: - try: - path = Path(path_str) - if self._validator.validate(path): - validated_paths.append(path) - except ValidationError as e: - self.drag_failed.emit(f"Validation failed for {path_str}: {e}") + # Check if it's an Azure URL and convert to local path + if self._url_converter.is_azure_url(text): + local_path = self._url_converter.convert_url_to_path(text) + if local_path is None: + error_msg = "No mapping found for URL" + logger.warning(f"{error_msg}: {text}") + self.drag_failed.emit(text, error_msg) return False - - if not validated_paths: - self.drag_failed.emit("No valid files after validation") - return False - - # Create MIME data with file URLs - mime_data = QMimeData() - file_urls = [ - path.as_uri() for path in validated_paths - ] - mime_data.setUrls([QUrl(url) for url in file_urls]) - - # Create and execute drag operation - drag = QDrag(self) - drag.setMimeData(mime_data) - # Use default drag pixmap (small icon) - drag.setPixmap(self.grab(self.rect()).scaled( - 64, 64, Qt.AspectRatioMode.KeepAspectRatio - )) - - # Execute drag operation (blocking call) - drop_action = drag.exec(Qt.DropAction.CopyAction) - - # Check result - if drop_action == Qt.DropAction.CopyAction: - self.drag_started.emit(validated_paths) - return True + source_text = text else: - self.drag_failed.emit("Drag operation cancelled or failed") + # Treat as direct file path + local_path = Path(text) + source_text = text + + # Validate the path + try: + self._validator.validate(local_path) + except ValidationError as e: + error_msg = str(e) + logger.warning(f"Validation failed for {local_path}: {error_msg}") + self.drag_failed.emit(source_text, error_msg) return False + logger.info(f"Initiating drag for: {local_path}") + # Create native file drag + success = self._create_native_drag(local_path) + + if success: + self.drag_started.emit(source_text, str(local_path)) + else: + error_msg = "Failed to create native drag operation" + logger.error(error_msg) + self.drag_failed.emit(source_text, error_msg) + + return success + + def _create_native_drag(self, file_path: Path) -> bool: + """Create a native file system drag operation. + + Args: + file_path: Local file path to drag + + Returns: + True if drag was created successfully + """ + try: + # Create MIME data with file URL + mime_data = QMimeData() + file_url = QUrl.fromLocalFile(str(file_path)) + mime_data.setUrls([file_url]) + + # Create and execute drag + drag = QDrag(self) + drag.setMimeData(mime_data) + + # Optional: Set a drag icon/pixmap if available + # drag.setPixmap(...) + + # Start drag operation (blocks until drop or cancel) + # Qt.CopyAction allows copying files + result = drag.exec(Qt.DropAction.CopyAction) + + return result == Qt.DropAction.CopyAction + + except Exception as e: + logger.exception(f"Error creating native drag: {e}") + return False diff --git a/src/webdrop_bridge/core/url_converter.py b/src/webdrop_bridge/core/url_converter.py new file mode 100644 index 0000000..4841dfe --- /dev/null +++ b/src/webdrop_bridge/core/url_converter.py @@ -0,0 +1,86 @@ +"""URL to local path conversion for Azure Blob Storage URLs.""" + +import logging +from pathlib import Path +from typing import Optional +from urllib.parse import unquote + +from ..config import Config, URLMapping + +logger = logging.getLogger(__name__) + + +class URLConverter: + """Converts Azure Blob Storage URLs to local file paths.""" + + def __init__(self, config: Config): + """Initialize converter with configuration. + + Args: + config: Application configuration with URL mappings + """ + self.config = config + + def convert_url_to_path(self, url: str) -> Optional[Path]: + """Convert Azure Blob Storage URL to local file path. + + Args: + url: Azure Blob Storage URL + + Returns: + Local file path if mapping found, None otherwise + + Example: + >>> converter.convert_url_to_path( + ... "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/file.png" + ... ) + Path("Z:/aN5PysnXIuRECzcRbvHkjL7g0/file.png") + """ + if not url: + return None + + # URL decode (handles special characters like spaces) + url = unquote(url) + + # Find matching URL mapping + for mapping in self.config.url_mappings: + if url.startswith(mapping.url_prefix): + # Extract relative path after prefix + relative_path = url[len(mapping.url_prefix):] + + # Combine with local path + local_path = Path(mapping.local_path) / relative_path + + # Normalize path (resolve .. and .) but don't follow symlinks yet + try: + # On Windows, normalize separators + local_path = Path(str(local_path).replace("/", "\\")) + except (OSError, RuntimeError) as e: + logger.warning(f"Failed to normalize path {local_path}: {e}") + return None + + logger.debug(f"Converted URL to path: {url} -> {local_path}") + return local_path + + logger.debug(f"No mapping found for URL: {url}") + return None + + def is_azure_url(self, text: str) -> bool: + """Check if text is an Azure Blob Storage URL. + + Args: + text: Text to check + + Returns: + True if text matches configured URL prefixes + """ + if not text: + return False + + text = text.strip() + + for mapping in self.config.url_mappings: + if text.startswith(mapping.url_prefix): + return True + + return False diff --git a/src/webdrop_bridge/core/validator.py b/src/webdrop_bridge/core/validator.py index 218fe2d..aac28c0 100644 --- a/src/webdrop_bridge/core/validator.py +++ b/src/webdrop_bridge/core/validator.py @@ -1,7 +1,10 @@ """Path validation for secure file operations.""" +import logging from pathlib import Path -from typing import List +from typing import List, Optional + +logger = logging.getLogger(__name__) class ValidationError(Exception): @@ -18,28 +21,27 @@ class PathValidator: directory traversal attacks. """ - def __init__(self, allowed_roots: List[Path]): + def __init__(self, allowed_roots: List[Path], check_file_exists: bool = True): """Initialize validator with allowed root directories. Args: allowed_roots: List of Path objects representing allowed root dirs + check_file_exists: Whether to validate that files exist Raises: ValidationError: If any root doesn't exist or isn't a directory """ self.allowed_roots = [] + self.check_file_exists = check_file_exists for root in allowed_roots: root_path = Path(root).resolve() if not root_path.exists(): - raise ValidationError( - f"Allowed root '{root}' does not exist" - ) - if not root_path.is_dir(): - raise ValidationError( - f"Allowed root '{root}' is not a directory" - ) - self.allowed_roots.append(root_path) + logger.warning(f"Allowed root '{root}' does not exist") + elif not root_path.is_dir(): + raise ValidationError(f"Allowed root '{root}' is not a directory") + else: + self.allowed_roots.append(root_path) def validate(self, path: Path) -> bool: """Validate that path is within an allowed root directory. @@ -59,28 +61,32 @@ class PathValidator: except (OSError, ValueError) as e: raise ValidationError(f"Cannot resolve path '{path}': {e}") from e - # Check file exists - if not file_path.exists(): - raise ValidationError(f"File does not exist: {path}") + # Check file exists if required + if self.check_file_exists: + if not file_path.exists(): + raise ValidationError(f"File does not exist: {path}") - # Check it's a regular file (not directory, symlink to dir, etc) - if not file_path.is_file(): - raise ValidationError(f"Path is not a regular file: {path}") + # Check it's a regular file (not directory, symlink to dir, etc) + if not file_path.is_file(): + raise ValidationError(f"Path is not a regular file: {path}") - # Check path is within an allowed root - for allowed_root in self.allowed_roots: - try: - # This raises ValueError if file_path is not relative to root - file_path.relative_to(allowed_root) - return True - except ValueError: - continue + # Check path is within an allowed root (if roots configured) + if self.allowed_roots: + for allowed_root in self.allowed_roots: + try: + # This raises ValueError if file_path is not relative to root + file_path.relative_to(allowed_root) + return True + except ValueError: + continue - # Not in any allowed root - raise ValidationError( - f"Path '{file_path}' is not within allowed roots: " - f"{self.allowed_roots}" - ) + # Not in any allowed root + raise ValidationError( + f"Path '{file_path}' is not within allowed roots: " + f"{self.allowed_roots}" + ) + + return True def is_valid(self, path: Path) -> bool: """Check if path is valid without raising exception. diff --git a/src/webdrop_bridge/main.py b/src/webdrop_bridge/main.py index 6c33e88..d63ad97 100644 --- a/src/webdrop_bridge/main.py +++ b/src/webdrop_bridge/main.py @@ -19,8 +19,12 @@ def main() -> int: int: Exit code (0 for success, non-zero for error) """ try: - # Load configuration from environment - config = Config.from_env() + # Load configuration from file if it exists, otherwise from environment + config_path = Config.get_default_config_path() + if config_path.exists(): + config = Config.from_file(config_path) + else: + config = Config.from_env() # Set up logging log_file = None diff --git a/src/webdrop_bridge/ui/bridge_script.js b/src/webdrop_bridge/ui/bridge_script.js index aa5b8a3..622a0e4 100644 --- a/src/webdrop_bridge/ui/bridge_script.js +++ b/src/webdrop_bridge/ui/bridge_script.js @@ -5,6 +5,58 @@ if (window.__webdrop_bridge_injected) return; window.__webdrop_bridge_injected = true; + console.log('[WebDrop Bridge] Script loaded'); + + // Store web app's dragstart handlers by intercepting addEventListener + var webAppDragHandlers = []; + var originalAddEventListener = EventTarget.prototype.addEventListener; + var listenerPatchActive = true; + + // Patch addEventListener to intercept dragstart registrations + EventTarget.prototype.addEventListener = function(type, listener, options) { + if (listenerPatchActive && type === 'dragstart' && listener) { + // Store the web app's dragstart handler instead of registering it + console.log('[WebDrop Bridge] Intercepted dragstart listener registration on', this.tagName || this.constructor.name); + webAppDragHandlers.push({ + target: this, + listener: listener, + options: options + }); + return; + } + // All other events: use original + return originalAddEventListener.call(this, type, listener, options); + }; + + // Patch DataTransfer.setData to intercept URL setting by the web app + var originalSetData = null; + var currentDragData = null; + + try { + if (DataTransfer.prototype.setData) { + originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + // Store the data for our analysis + if (format === 'text/plain' || format === 'text/uri-list') { + currentDragData = data; + console.log('[WebDrop Bridge] DataTransfer.setData intercepted:', format, '=', data.substring(0, 80)); + + // Log via bridge if available + if (window.bridge && typeof window.bridge.debug_log === 'function') { + window.bridge.debug_log('setData intercepted: ' + format + ' = ' + data.substring(0, 60)); + } + } + // Call original to maintain web app functionality + return originalSetData.call(this, format, data); + }; + + console.log('[WebDrop Bridge] DataTransfer.setData patched'); + } + } catch(e) { + console.error('[WebDrop Bridge] Failed to patch DataTransfer:', e); + } + function ensureChannel(cb) { if (window.bridge) { cb(); return; } @@ -12,62 +64,151 @@ if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { new QWebChannel(window.qt.webChannelTransport, function(channel) { window.bridge = channel.objects.bridge; + console.log('[WebDrop Bridge] QWebChannel connected'); cb(); }); + } else { + // If QWebChannel is not available, log error + console.error('[WebDrop Bridge] QWebChannel not available! Check if qwebchannel.js was loaded.'); } } + // QWebChannel should already be loaded inline (no need to load from qrc://) if (window.QWebChannel) { init(); - return; + } else { + console.error('[WebDrop Bridge] QWebChannel not found! Cannot initialize bridge.'); } - - var s = document.createElement('script'); - s.src = 'qrc:///qtwebchannel/qwebchannel.js'; - s.onload = init; - document.documentElement.appendChild(s); } function hook() { - document.addEventListener('dragstart', function(e) { - var dt = e.dataTransfer; - if (!dt) return; - - // Get path from existing payload or from the card markup. - var path = dt.getData('text/plain'); - if (!path) { - var card = e.target.closest && e.target.closest('.drag-item'); - if (card) { - var pathEl = card.querySelector('p'); - if (pathEl) { - path = (pathEl.textContent || '').trim(); + console.log('[WebDrop Bridge] Installing hook, have ' + webAppDragHandlers.length + ' intercepted handlers'); + + if (window.bridge && typeof window.bridge.debug_log === 'function') { + window.bridge.debug_log('Installing drag interceptor with ' + webAppDragHandlers.length + ' intercepted handlers'); + } + + // Stop intercepting addEventListener - from now on, listeners register normally + listenerPatchActive = false; + + // Register OUR dragstart handler using capture phase on document + originalAddEventListener.call(document, 'dragstart', function(e) { + try { + console.log('[WebDrop Bridge] >>> DRAGSTART fired on:', e.target.tagName, 'altKey:', e.altKey, 'currentDragData:', currentDragData); + + if (window.bridge && typeof window.bridge.debug_log === 'function') { + window.bridge.debug_log('dragstart fired on ' + e.target.tagName + ' altKey=' + e.altKey); + } + + // Only intercept if ALT key is pressed (web app's text drag mode) + if (!e.altKey) { + console.log('[WebDrop Bridge] ALT not pressed, ignoring drag (normal web app drag)'); + return; // Let web app handle normal drags + } + + console.log('[WebDrop Bridge] ALT pressed - processing for file drag conversion'); + + // Manually invoke all the web app's dragstart handlers + var handlersInvoked = 0; + console.log('[WebDrop Bridge] About to invoke', webAppDragHandlers.length, 'stored handlers'); + + for (var i = 0; i < webAppDragHandlers.length; i++) { + try { + var handler = webAppDragHandlers[i]; + // Check if this handler should be called for this target + if (handler.target === document || + handler.target === e.target || + (handler.target.contains && handler.target.contains(e.target))) { + + console.log('[WebDrop Bridge] Calling stored handler #' + i); + handler.listener.call(e.target, e); + handlersInvoked++; + } + } catch (err) { + console.error('[WebDrop Bridge] Error calling web app handler #' + i + ':', err); } } + + console.log('[WebDrop Bridge] Invoked', handlersInvoked, 'handlers, currentDragData:', currentDragData ? currentDragData.substring(0, 60) : 'null'); + + // NOW check if we have a convertible URL + if (currentDragData) { + console.log('[WebDrop Bridge] Checking currentDragData:', currentDragData.substring(0, 80)); + var path = currentDragData; + var isZDrive = /^z:/i.test(path); + var isAzureUrl = /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(path); + + console.log('[WebDrop Bridge] isZDrive:', isZDrive, 'isAzureUrl:', isAzureUrl); + + if (isZDrive || isAzureUrl) { + console.log('[WebDrop Bridge] >>> CONVERTING URL TO NATIVE DRAG'); + + if (window.bridge && typeof window.bridge.debug_log === 'function') { + window.bridge.debug_log('Convertible URL detected - preventing browser drag'); + } + + // Prevent the browser's drag operation + e.preventDefault(); + e.stopPropagation(); + + // Start native file drag via Qt + ensureChannel(function() { + if (window.bridge && typeof window.bridge.start_file_drag === 'function') { + console.log('[WebDrop Bridge] Calling start_file_drag:', path.substring(0, 60)); + window.bridge.start_file_drag(path); + currentDragData = null; + } else { + console.error('[WebDrop Bridge] bridge.start_file_drag not available!'); + } + }); + + return false; + } else { + console.log('[WebDrop Bridge] URL not convertible:', path.substring(0, 60)); + } + } else { + console.log('[WebDrop Bridge] No currentDragData set'); + } + } catch (mainError) { + console.error('[WebDrop Bridge] CRITICAL ERROR in dragstart handler:', mainError); + if (window.bridge && typeof window.bridge.debug_log === 'function') { + window.bridge.debug_log('ERROR in dragstart: ' + mainError.message); + } } - if (!path) return; + }, true); // CAPTURE PHASE - intercept early + + // Reset state on dragend + originalAddEventListener.call(document, 'dragend', function(e) { + currentDragData = null; + }, false); + + console.log('[WebDrop Bridge] Drag listener registered on document (capture phase)'); + } - // Ensure text payload exists for non-file drags and downstream targets. - if (!dt.getData('text/plain')) { - dt.setData('text/plain', path); - } - - // Check if path is Z:\ — if yes, trigger native file drag. Otherwise, stay as text. - var isZDrive = /^z:/i.test(path); - if (!isZDrive) return; - - // Z:\ detected — prevent default browser drag and convert to native file drag - e.preventDefault(); + // Wait for DOMContentLoaded and then a bit more before installing hook + // This gives the web app time to register its handlers + function installHook() { + console.log('[WebDrop Bridge] DOM ready, waiting 2 seconds for web app to register handlers...'); + console.log('[WebDrop Bridge] Currently have', webAppDragHandlers.length, 'intercepted handlers'); + + setTimeout(function() { + console.log('[WebDrop Bridge] Installing hook now, have', webAppDragHandlers.length, 'intercepted handlers'); + hook(); + ensureChannel(function() { - if (window.bridge && typeof window.bridge.start_file_drag === 'function') { - window.bridge.start_file_drag(path); + if (window.bridge && typeof window.bridge.debug_log === 'function') { + window.bridge.debug_log('Hook installed with ' + webAppDragHandlers.length + ' captured handlers'); } }); - }, false); + }, 2000); // Wait 2 seconds after DOM ready } - + + // Install after DOM is ready if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', hook); + console.log('[WebDrop Bridge] Waiting for DOMContentLoaded...'); + originalAddEventListener.call(document, 'DOMContentLoaded', installHook); } else { - hook(); + console.log('[WebDrop Bridge] DOM already ready, installing hook...'); + installHook(); } })(); diff --git a/src/webdrop_bridge/ui/download_interceptor.js b/src/webdrop_bridge/ui/download_interceptor.js new file mode 100644 index 0000000..45a757e --- /dev/null +++ b/src/webdrop_bridge/ui/download_interceptor.js @@ -0,0 +1,72 @@ +// Download Interceptor Script +// Intercepts JavaScript-based downloads (fetch, XMLHttpRequest, Blob URLs) + +(function() { + 'use strict'; + + console.log('🔍 Download interceptor script loaded'); + + // Intercept fetch() calls + const originalFetch = window.fetch; + window.fetch = function(...args) { + const url = args[0]; + console.log('🌐 Fetch called:', url); + + // Check if this looks like a download + if (typeof url === 'string') { + const urlLower = url.toLowerCase(); + const downloadPatterns = [ + '/download', '/export', '/file', + '.pdf', '.zip', '.xlsx', '.docx', + 'attachment', 'content-disposition' + ]; + + if (downloadPatterns.some(pattern => urlLower.includes(pattern))) { + console.log('📥 Potential download detected via fetch:', url); + } + } + + return originalFetch.apply(this, args); + }; + + // Intercept XMLHttpRequest + const originalXHROpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function(method, url, ...rest) { + console.log('🌐 XHR opened:', method, url); + this._url = url; + return originalXHROpen.apply(this, [method, url, ...rest]); + }; + + const originalXHRSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.send = function(...args) { + console.log('📤 XHR send:', this._url); + return originalXHRSend.apply(this, args); + }; + + // Intercept Blob URL creation + const originalCreateObjectURL = URL.createObjectURL; + URL.createObjectURL = function(blob) { + console.log('🔗 Blob URL created, size:', blob.size, 'type:', blob.type); + return originalCreateObjectURL.apply(this, arguments); + }; + + // Intercept anchor clicks that might be downloads + document.addEventListener('click', function(e) { + const target = e.target.closest('a'); + if (target && target.href) { + const href = target.href; + const download = target.getAttribute('download'); + + if (download !== null) { + console.log('📥 Download link clicked:', href, 'filename:', download); + } + + // Check for blob URLs + if (href.startsWith('blob:')) { + console.log('📦 Blob download link clicked:', href); + } + } + }, true); + + console.log('✅ Download interceptor active'); +})(); diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index 84638a4..252e27b 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -6,10 +6,22 @@ from datetime import datetime from pathlib import Path from typing import Optional -from PySide6.QtCore import QObject, QPoint, QSize, Qt, QThread, QTimer, QUrl, Signal, Slot -from PySide6.QtGui import QIcon +from PySide6.QtCore import ( + QEvent, + QObject, + QPoint, + QSize, + QStandardPaths, + Qt, + QThread, + QTimer, + QUrl, + Signal, + Slot, +) +from PySide6.QtGui import QIcon, QMouseEvent from PySide6.QtWebChannel import QWebChannel -from PySide6.QtWebEngineCore import QWebEngineScript +from PySide6.QtWebEngineCore import QWebEngineDownloadRequest, QWebEngineScript from PySide6.QtWidgets import ( QLabel, QMainWindow, @@ -202,19 +214,29 @@ class _DragBridge(QObject): @Slot(str) def start_file_drag(self, path_text: str) -> None: - """Start a native file drag for the given path. + """Start a native file drag for the given path or Azure URL. - Called from JavaScript when user drags a Z:\ path item. + Called from JavaScript when user drags an item. + Accepts either local file paths or Azure Blob Storage URLs. Defers execution to avoid Qt drag manager state issues. Args: - path_text: File path string to drag + path_text: File path string or Azure URL to drag """ logger.debug(f"Bridge: start_file_drag called for {path_text}") # Defer to avoid drag manager state issues - # initiate_drag() handles validation internally - QTimer.singleShot(0, lambda: self.window.drag_interceptor.initiate_drag([path_text])) + # handle_drag() handles URL conversion and validation internally + QTimer.singleShot(0, lambda: self.window.drag_interceptor.handle_drag(path_text)) + + @Slot(str) + def debug_log(self, message: str) -> None: + """Log debug message from JavaScript. + + Args: + message: Debug message from JavaScript + """ + logger.info(f"JS Debug: {message}") class MainWindow(QMainWindow): @@ -257,6 +279,16 @@ class MainWindow(QMainWindow): # Create web engine view self.web_view = RestrictedWebEngineView(config.allowed_urls) + + # Enable the main window and web view to receive drag events + self.setAcceptDrops(True) + self.web_view.setAcceptDrops(True) + + # Track ongoing drags from web view + self._current_drag_url = None + + # Redirect JavaScript console messages to Python logger + self.web_view.page().javaScriptConsoleMessage = self._on_js_console_message # Create navigation toolbar (Kiosk-mode navigation) self._create_navigation_toolbar() @@ -264,11 +296,8 @@ class MainWindow(QMainWindow): # Create status bar self._create_status_bar() - # Create drag interceptor - self.drag_interceptor = DragInterceptor() - # Set up path validator - validator = PathValidator(config.allowed_roots) - self.drag_interceptor.set_validator(validator) + # Create drag interceptor with config (includes URL converter) + self.drag_interceptor = DragInterceptor(config) # Connect drag interceptor signals self.drag_interceptor.drag_started.connect(self._on_drag_started) @@ -282,6 +311,26 @@ class MainWindow(QMainWindow): # Install the drag bridge script self._install_bridge_script() + + # Connect to loadFinished to verify script injection + self.web_view.loadFinished.connect(self._on_page_loaded) + + # Set up download handler + profile = self.web_view.page().profile() + logger.info(f"Connecting download handler to profile: {profile}") + + # CRITICAL: Connect download handler BEFORE any page loads + profile.downloadRequested.connect(self._on_download_requested) + + # Enable downloads by setting download path + downloads_path = QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.DownloadLocation + ) + if downloads_path: + profile.setDownloadPath(downloads_path) + logger.info(f"Download path set to: {downloads_path}") + + logger.info("Download handler connected successfully") # Set up central widget with layout central_widget = QWidget() @@ -353,19 +402,55 @@ class MainWindow(QMainWindow): def _install_bridge_script(self) -> None: """Install the drag bridge JavaScript via QWebEngineScript. - Follows the POC pattern for proper script injection and QWebChannel setup. + Uses DocumentCreation injection point to ensure script runs as early as possible, + before any page scripts that might interfere with drag events. + + Embeds qwebchannel.js inline to avoid CSP issues with qrc:// URLs. """ + from PySide6.QtCore import QFile, QIODevice + script = QWebEngineScript() script.setName("webdrop-bridge") - script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady) + script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation) script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld) script.setRunsOnSubFrames(False) + # Load qwebchannel.js from Qt resources (avoids CSP blocking qrc:// URLs) + qwebchannel_code = "" + qwebchannel_file = QFile(":/qtwebchannel/qwebchannel.js") + if qwebchannel_file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text): + qwebchannel_code = bytes(qwebchannel_file.readAll()).decode('utf-8') + qwebchannel_file.close() + logger.debug("Loaded qwebchannel.js inline to avoid CSP issues") + else: + logger.warning("Failed to load qwebchannel.js from resources") + # Load bridge script from file script_path = Path(__file__).parent / "bridge_script.js" try: with open(script_path, 'r', encoding='utf-8') as f: - script.setSourceCode(f.read()) + bridge_code = f.read() + + # Load download interceptor + download_interceptor_path = Path(__file__).parent / "download_interceptor.js" + download_interceptor_code = "" + try: + with open(download_interceptor_path, 'r', encoding='utf-8') as f: + download_interceptor_code = f.read() + logger.debug(f"Loaded download interceptor from {download_interceptor_path}") + except (OSError, IOError) as e: + logger.warning(f"Download interceptor not found: {e}") + + # Combine qwebchannel.js + bridge script + download interceptor (inline to avoid CSP) + if qwebchannel_code: + combined_code = qwebchannel_code + "\n\n" + bridge_code + else: + combined_code = bridge_code + + if download_interceptor_code: + combined_code += "\n\n" + download_interceptor_code + + script.setSourceCode(combined_code) self.web_view.page().scripts().insert(script) logger.debug(f"Installed bridge script from {script_path}") except (OSError, IOError) as e: @@ -399,23 +484,248 @@ class MainWindow(QMainWindow): # Silently fail if stylesheet can't be read pass - def _on_drag_started(self, paths: list) -> None: + def _on_drag_started(self, source: str, local_path: str) -> None: """Handle successful drag initiation. Args: - paths: List of paths that were dragged + source: Original URL or path from web content + local_path: Local file path that is being dragged """ - # Can be extended with logging or status bar updates - pass + logger.info(f"Drag started: {source} -> {local_path}") + # Can be extended with status bar updates or user feedback - def _on_drag_failed(self, error: str) -> None: + def _on_drag_failed(self, source: str, error: str) -> None: """Handle drag operation failure. Args: + source: Original URL or path from web content error: Error message """ - # Can be extended with logging or user notification - pass + logger.warning(f"Drag failed for {source}: {error}") + # Can be extended with user notification or status bar message + + def _on_download_requested(self, download: QWebEngineDownloadRequest) -> None: + """Handle download requests from the embedded web view. + + Downloads are automatically saved to the user's Downloads folder. + + Args: + download: Download request from the web engine + """ + logger.info("=" * 60) + logger.info("🔥 DOWNLOAD REQUESTED - Handler called!") + logger.info("=" * 60) + + try: + # Log all download details for debugging + logger.info(f"Download URL: {download.url().toString()}") + logger.info(f"Download filename: {download.downloadFileName()}") + logger.info(f"Download mime type: {download.mimeType()}") + logger.info(f"Download suggested filename: {download.suggestedFileName()}") + logger.info(f"Download state: {download.state()}") + + # Get the system's Downloads folder + downloads_path = QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.DownloadLocation + ) + + if not downloads_path: + # Fallback to user's home directory if Downloads folder not available + downloads_path = str(Path.home()) + logger.warning("Downloads folder not found, using home directory") + + # Use suggested filename if available, fallback to downloadFileName + filename = download.suggestedFileName() or download.downloadFileName() + if not filename: + filename = "download" + logger.warning("No filename suggested, using 'download'") + + # Construct full download path + download_file = Path(downloads_path) / filename + logger.info(f"📁 Download will be saved to: {download_file}") + + # Set download path and accept + download.setDownloadDirectory(str(download_file.parent)) + download.setDownloadFileName(download_file.name) + download.accept() + + logger.info(f"✅ Download accepted and started: {download_file}") + + # Update status bar (temporarily) + self.status_bar.showMessage( + f"📥 Download: {filename}", 3000 + ) + + # Connect to state changed for progress tracking + download.stateChanged.connect( + lambda state: logger.info(f"Download state changed to: {state}") + ) + + # Connect to finished signal for completion feedback + download.isFinishedChanged.connect( + lambda: self._on_download_finished(download, download_file) + ) + + except Exception as e: + logger.error(f"❌ Error handling download: {e}", exc_info=True) + self.status_bar.showMessage(f"❌ Download-Fehler: {e}", 5000) + + def _on_download_finished(self, download: QWebEngineDownloadRequest, file_path: Path) -> None: + """Handle download completion. + + Args: + download: The completed download request + file_path: Path where file was saved + """ + try: + if not download.isFinished(): + return + + state = download.state() + logger.info(f"Download finished with state: {state}") + + if state == QWebEngineDownloadRequest.DownloadState.DownloadCompleted: + logger.info(f"Download completed successfully: {file_path}") + self.status_bar.showMessage( + f"✅ Download abgeschlossen: {file_path.name}", 5000 + ) + elif state == QWebEngineDownloadRequest.DownloadState.DownloadCancelled: + logger.info(f"Download cancelled: {file_path.name}") + self.status_bar.showMessage( + f"⚠️ Download abgebrochen: {file_path.name}", 3000 + ) + elif state == QWebEngineDownloadRequest.DownloadState.DownloadInterrupted: + logger.warning(f"Download interrupted: {file_path.name}") + self.status_bar.showMessage( + f"❌ Download fehlgeschlagen: {file_path.name}", 5000 + ) + except Exception as e: + logger.error(f"Error in download finished handler: {e}", exc_info=True) + + def dragEnterEvent(self, event): + """Handle drag entering the main window (from WebView or external). + + When a drag from the WebView enters the MainWindow area, we can read + the drag data and potentially convert Azure URLs to file drags. + + Args: + event: QDragEnterEvent + """ + from PySide6.QtCore import QMimeData + + mime_data = event.mimeData() + + # Check if we have text data (URL from web app) + if mime_data.hasText(): + url_text = mime_data.text() + logger.debug(f"Drag entered main window with text: {url_text[:100]}") + + # Store for potential conversion + self._current_drag_url = url_text + + # Check if it's convertible + is_azure = url_text.startswith('https://') and 'file.core.windows.net' in url_text + is_z_drive = url_text.lower().startswith('z:') + + if is_azure or is_z_drive: + logger.info(f"Convertible URL detected in drag: {url_text[:60]}") + event.acceptProposedAction() + return + + event.ignore() + + def dragMoveEvent(self, event): + """Handle drag moving over the main window. + + Args: + event: QDragMoveEvent + """ + if self._current_drag_url: + event.acceptProposedAction() + else: + event.ignore() + + def dragLeaveEvent(self, event): + """Handle drag leaving the main window. + + Args: + event: QDragLeaveEvent + """ + logger.debug("Drag left main window") + # Reset tracking + self._current_drag_url = None + + def dropEvent(self, event): + """Handle drop on the main window. + + This captures drops on the MainWindow area (outside WebView). + If the user drops an Azure URL here, we convert it to a file operation. + + Args: + event: QDropEvent + """ + if self._current_drag_url: + logger.info(f"Drop on main window with URL: {self._current_drag_url[:60]}") + + # Handle via drag interceptor (converts Azure URL to local path) + success = self.drag_interceptor.handle_drag(self._current_drag_url) + + if success: + event.acceptProposedAction() + else: + event.ignore() + + self._current_drag_url = None + else: + event.ignore() + + def _on_js_console_message(self, level, message, line_number, source_id): + """Redirect JavaScript console messages to Python logger. + + Args: + level: Console message level (JavaScriptConsoleMessageLevel enum) + message: The console message + line_number: Line number where the message originated + source_id: Source file/URL where the message originated + """ + from PySide6.QtWebEngineCore import QWebEnginePage + + # Map JS log levels to Python log levels using enum + if level == QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel: + logger.info(f"JS Console: {message}") + elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel: + logger.warning(f"JS Console: {message}") + logger.debug(f" at {source_id}:{line_number}") + else: # ErrorMessageLevel + logger.error(f"JS Console: {message}") + logger.debug(f" at {source_id}:{line_number}") + + def _on_page_loaded(self, success: bool) -> None: + """Called when a page finishes loading. + + Checks if the bridge script was successfully injected. + + Args: + success: True if page loaded successfully + """ + if not success: + logger.warning("Page failed to load") + return + + # Check if bridge script is loaded + def check_script(result): + if result: + logger.info("✓ WebDrop Bridge script is active") + logger.info("✓ QWebChannel bridge is ready") + else: + logger.error("✗ WebDrop Bridge script NOT loaded!") + logger.error(" Drag-and-drop conversion will NOT work") + + # Execute JS to check if our script is loaded + self.web_view.page().runJavaScript( + "typeof window.__webdrop_bridge_injected !== 'undefined' && window.__webdrop_bridge_injected === true", + check_script + ) def _create_navigation_toolbar(self) -> None: """Create navigation toolbar with Home, Back, Forward, Refresh buttons. @@ -488,7 +798,7 @@ class MainWindow(QMainWindow): Args: status: Status text to display - emoji: Optional emoji prefix (🔄, ✅, ⬇️, ⚠️) + emoji: Optional emoji prefix (rotating, checkmark, download, warning symbols) """ if emoji: self.update_status_label.setText(f"{emoji} {status}") @@ -559,24 +869,11 @@ class MainWindow(QMainWindow): # 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) - def check_for_updates_startup(self) -> None: """Check for updates on application startup. Runs asynchronously in background without blocking UI. - Uses 24h cache so won't hammer the API. + Uses 24-hour cache so will not hammer the API. """ from webdrop_bridge.core.updater import UpdateManager diff --git a/src/webdrop_bridge/ui/restricted_web_view.py b/src/webdrop_bridge/ui/restricted_web_view.py index d7b28cc..489b473 100644 --- a/src/webdrop_bridge/ui/restricted_web_view.py +++ b/src/webdrop_bridge/ui/restricted_web_view.py @@ -1,13 +1,106 @@ """Restricted web view with URL whitelist enforcement for Kiosk-mode.""" import fnmatch -from typing import List, Optional +import logging +from pathlib import Path +from typing import List, Optional, Union -from PySide6.QtCore import QUrl +from PySide6.QtCore import QStandardPaths, QUrl from PySide6.QtGui import QDesktopServices -from PySide6.QtWebEngineCore import QWebEngineNavigationRequest +from PySide6.QtWebEngineCore import QWebEngineNavigationRequest, QWebEnginePage, QWebEngineProfile from PySide6.QtWebEngineWidgets import QWebEngineView +logger = logging.getLogger(__name__) + + +logger = logging.getLogger(__name__) + + +class CustomWebEnginePage(QWebEnginePage): + """Custom page that handles new window requests for downloads.""" + + def acceptNavigationRequest( + self, url: Union[QUrl, str], nav_type: QWebEnginePage.NavigationType, is_main_frame: bool + ) -> bool: + """Handle navigation requests, including download links. + + Args: + url: Target URL (QUrl or string) + nav_type: Type of navigation (link click, form submit, etc.) + is_main_frame: Whether this is the main frame + + Returns: + True to accept navigation, False to reject + """ + # Convert to string if QUrl + url_str = url.toString() if isinstance(url, QUrl) else url + + # Log all navigation attempts for debugging + logger.debug(f"Navigation request: {url_str} (type={nav_type}, main_frame={is_main_frame})") + + # Check if this might be a download (common file extensions) + download_extensions = [ + ".pdf", + ".zip", + ".rar", + ".7z", + ".tar", + ".gz", + ".doc", + ".docx", + ".xls", + ".xlsx", + ".ppt", + ".pptx", + ".jpg", + ".jpeg", + ".png", + ".gif", + ".bmp", + ".svg", + ".mp4", + ".mp3", + ".avi", + ".mov", + ".wav", + ".exe", + ".msi", + ".dmg", + ".pkg", + ".csv", + ".txt", + ".json", + ".xml", + ] + + if any(url_str.lower().endswith(ext) for ext in download_extensions): + logger.info(f"🔽 Detected potential download URL: {url_str}") + # This will trigger downloadRequested if it's a download + + return super().acceptNavigationRequest(url, nav_type, is_main_frame) + + def createWindow(self, window_type: QWebEnginePage.WebWindowType) -> QWebEnginePage: + """Handle new window requests (target=_blank, window.open, etc.). + + Many downloads are triggered via target="_blank" links. + + Args: + window_type: Type of window being created + + Returns: + New page instance for the window + """ + logger.info(f"🪟 New window requested, type: {window_type}") + + # Create a temporary page to handle the download + # This page will never be displayed but allows downloads to work + download_page = QWebEnginePage(self.profile(), self) + + logger.info("✅ Created temporary page for download/popup") + + # Return the temporary page - it will trigger downloadRequested if it's a download + return download_page + class RestrictedWebEngineView(QWebEngineView): """Web view that enforces URL whitelist for Kiosk-mode security. @@ -27,31 +120,81 @@ class RestrictedWebEngineView(QWebEngineView): super().__init__() self.allowed_urls = allowed_urls or [] + # Create persistent profile for cookie and session storage + self.profile = self._create_persistent_profile() + + # Use custom page for better download handling with persistent profile + custom_page = CustomWebEnginePage(self.profile, self) + self.setPage(custom_page) + + logger.info( + "RestrictedWebEngineView initialized with CustomWebEnginePage and persistent profile" + ) + # Connect to navigation request handler self.page().navigationRequested.connect(self._on_navigation_requested) - def _on_navigation_requested( - self, request: QWebEngineNavigationRequest - ) -> None: + def _create_persistent_profile(self) -> QWebEngineProfile: + """Create and configure a persistent web engine profile. + + This enables persistent cookies and cache storage, allowing + authentication sessions (e.g., Microsoft login) to persist + across application restarts. + + Returns: + Configured QWebEngineProfile with persistent storage + """ + # Get application data directory + app_data_dir = QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.AppDataLocation + ) + + # Create profile directory path + profile_path = Path(app_data_dir) / "WebEngineProfile" + profile_path.mkdir(parents=True, exist_ok=True) + + # Create persistent profile with custom storage location + # Using "WebDropBridge" as the profile name + profile = QWebEngineProfile("WebDropBridge", self) + profile.setPersistentStoragePath(str(profile_path)) + + # Configure persistent cookies (critical for authentication) + profile.setPersistentCookiesPolicy( + QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies + ) + + # Enable HTTP cache for better performance + profile.setHttpCacheType(QWebEngineProfile.HttpCacheType.DiskHttpCache) + + # Set cache size to 100 MB + profile.setHttpCacheMaximumSize(100 * 1024 * 1024) + + logger.info(f"Created persistent profile at: {profile_path}") + logger.info("Cookies policy: ForcePersistentCookies") + logger.info("HTTP cache: DiskHttpCache (100 MB)") + + return profile + + def _on_navigation_requested(self, request: QWebEngineNavigationRequest) -> None: """Handle navigation requests and enforce URL whitelist. Args: request: Navigation request to process """ - url = request.url + url = request.url() # If no restrictions, allow all URLs if not self.allowed_urls: return # Check if URL matches whitelist - if self._is_url_allowed(url): # type: ignore[operator] + if self._is_url_allowed(url): # Allow the navigation (default behavior) return # URL not whitelisted - open in system browser request.reject() - QDesktopServices.openUrl(url) # type: ignore[operator] + QDesktopServices.openUrl(url) def _is_url_allowed(self, url: QUrl) -> bool: """Check if a URL matches the whitelist. @@ -98,4 +241,3 @@ class RestrictedWebEngineView(QWebEngineView): return True return False - diff --git a/test_download.html b/test_download.html new file mode 100644 index 0000000..3b6719c --- /dev/null +++ b/test_download.html @@ -0,0 +1,95 @@ + + + + + + Download Test + + + +

🧪 Download Test Seite

+ +
+

Test 1: Direct Download Link

+

Download einer Text-Datei via direktem Link:

+ 📥 Download test.txt +
+ +
+

Test 2: JavaScript Download (Blob)

+

Download via JavaScript Blob:

+ +
+ +
+

Test 3: Base64 Image Download

+

Download eines kleinen Bildes:

+ 📥 Download test.png +
+ +
+

Test 4: External Link (should open in browser)

+

PDF von externem Server:

+ 📥 Download external PDF +
+ + + + diff --git a/test_download.py b/test_download.py new file mode 100644 index 0000000..5b72aa4 --- /dev/null +++ b/test_download.py @@ -0,0 +1,19 @@ +"""Test script to verify download functionality.""" + +import sys +from pathlib import Path + +from PySide6.QtCore import QStandardPaths, QUrl +from PySide6.QtWebEngineCore import QWebEngineDownloadRequest +from PySide6.QtWidgets import QApplication + +# Test download path resolution +downloads_path = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DownloadLocation) +print(f"Downloads folder: {downloads_path}") +print(f"Downloads exists: {Path(downloads_path).exists()}") + +# Test file path construction +test_filename = "test_file.pdf" +download_file = Path(downloads_path) / test_filename +print(f"Test download path: {download_file}") +print(f"Parent exists: {download_file.parent.exists()}") diff --git a/test_url_mappings.py b/test_url_mappings.py new file mode 100644 index 0000000..0c6c3a7 --- /dev/null +++ b/test_url_mappings.py @@ -0,0 +1,24 @@ +"""Quick test to verify URL mappings are loaded correctly.""" +import sys +from pathlib import Path + +# Add src to path +sys.path.insert(0, str(Path(__file__).parent / "src")) + +from webdrop_bridge.config import Config + +# Load config from .env +config = Config.from_env() + +print(f"✓ Config loaded successfully") +print(f" URL Mappings: {len(config.url_mappings)} loaded") + +if config.url_mappings: + for i, mapping in enumerate(config.url_mappings, 1): + print(f" {i}. {mapping.url_prefix}") + print(f" -> {mapping.local_path}") +else: + print(" ⚠ No URL mappings found!") + +print(f"\n Allowed roots: {config.allowed_roots}") +print(f" Web app URL: {config.webapp_url}") diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index c8f569f..d20de67 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -98,12 +98,13 @@ class TestConfigFromEnv: Config.from_env(str(env_file)) def test_from_env_invalid_root_path(self, tmp_path): - """Test that non-existent root paths raise ConfigurationError.""" + """Test that non-existent root paths are logged as warning but don't raise error.""" env_file = tmp_path / ".env" env_file.write_text("ALLOWED_ROOTS=/nonexistent/path/that/does/not/exist\n") - with pytest.raises(ConfigurationError, match="does not exist"): - Config.from_env(str(env_file)) + # Should not raise - just logs warning and returns empty allowed_roots + config = Config.from_env(str(env_file)) + assert config.allowed_roots == [] # Non-existent roots are skipped def test_from_env_empty_webapp_url(self, tmp_path): """Test that empty webapp URL raises ConfigurationError.""" diff --git a/tests/unit/test_drag_interceptor.py b/tests/unit/test_drag_interceptor.py index d94fb23..eaa5ce4 100644 --- a/tests/unit/test_drag_interceptor.py +++ b/tests/unit/test_drag_interceptor.py @@ -3,63 +3,79 @@ from pathlib import Path from unittest.mock import MagicMock, patch +import pytest + +from webdrop_bridge.config import Config from webdrop_bridge.core.drag_interceptor import DragInterceptor -from webdrop_bridge.core.validator import PathValidator + + +@pytest.fixture +def test_config(tmp_path): + """Create test configuration.""" + return Config( + app_name="Test App", + app_version="1.0.0", + log_level="INFO", + log_file=None, + allowed_roots=[tmp_path], + allowed_urls=[], + webapp_url="https://wps.agravity.io/", + url_mappings=[], + check_file_exists=True, + ) class TestDragInterceptorInitialization: """Test DragInterceptor initialization and setup.""" - def test_drag_interceptor_creation(self, qtbot): + def test_drag_interceptor_creation(self, qtbot, test_config): """Test DragInterceptor can be instantiated.""" - interceptor = DragInterceptor() + interceptor = DragInterceptor(test_config) assert interceptor is not None - assert interceptor._validator is None + assert interceptor._validator is not None + assert interceptor._url_converter is not None - def test_drag_interceptor_has_signals(self, qtbot): + def test_drag_interceptor_has_signals(self, qtbot, test_config): """Test DragInterceptor has required signals.""" - interceptor = DragInterceptor() + interceptor = DragInterceptor(test_config) assert hasattr(interceptor, "drag_started") assert hasattr(interceptor, "drag_failed") - def test_set_validator(self, qtbot, tmp_path): - """Test setting validator on drag interceptor.""" - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - - interceptor.set_validator(validator) - - assert interceptor._validator is validator + def test_set_validator(self, qtbot, test_config): + """Test validator is set during construction.""" + interceptor = DragInterceptor(test_config) + assert interceptor._validator is not None class TestDragInterceptorValidation: """Test path validation in drag operations.""" - def test_initiate_drag_no_files(self, qtbot): - """Test initiating drag with no files fails.""" - interceptor = DragInterceptor() + def test_handle_drag_empty_text(self, qtbot, test_config): + """Test handling drag with empty text fails.""" + interceptor = DragInterceptor(test_config) with qtbot.waitSignal(interceptor.drag_failed): - result = interceptor.initiate_drag([]) + result = interceptor.handle_drag("") assert result is False - def test_initiate_drag_no_validator(self, qtbot): - """Test initiating drag without validator fails.""" - interceptor = DragInterceptor() - with qtbot.waitSignal(interceptor.drag_failed): - result = interceptor.initiate_drag(["file.txt"]) - - assert result is False - - def test_initiate_drag_single_valid_file(self, qtbot, tmp_path): - """Test initiating drag with single valid file.""" + def test_handle_drag_valid_file_path(self, qtbot, tmp_path): + """Test handling drag with valid file path.""" # Create a test file test_file = tmp_path / "test.txt" test_file.write_text("test content") - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) + config = Config( + app_name="Test", + app_version="1.0.0", + log_level="INFO", + log_file=None, + allowed_roots=[tmp_path], + allowed_urls=[], + webapp_url="https://test.com/", + url_mappings=[], + check_file_exists=True, + ) + interceptor = DragInterceptor(config) # Mock the drag operation to simulate success with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag: @@ -69,114 +85,91 @@ class TestDragInterceptorValidation: mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction mock_drag.return_value = mock_drag_instance - result = interceptor.initiate_drag([str(test_file)]) + result = interceptor.handle_drag(str(test_file)) # Should return True on successful drag assert result is True - def test_initiate_drag_invalid_path(self, qtbot, tmp_path): + def test_handle_drag_invalid_path(self, qtbot, test_config): """Test drag with invalid path fails.""" - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) + interceptor = DragInterceptor(test_config) # Path outside allowed roots - invalid_path = Path("/etc/passwd") + invalid_path = "/etc/passwd" with qtbot.waitSignal(interceptor.drag_failed): - result = interceptor.initiate_drag([str(invalid_path)]) + result = interceptor.handle_drag(invalid_path) assert result is False - def test_initiate_drag_nonexistent_file(self, qtbot, tmp_path): + def test_handle_drag_nonexistent_file(self, qtbot, test_config, tmp_path): """Test drag with nonexistent file fails.""" - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) + interceptor = DragInterceptor(test_config) nonexistent = tmp_path / "nonexistent.txt" with qtbot.waitSignal(interceptor.drag_failed): - result = interceptor.initiate_drag([str(nonexistent)]) + result = interceptor.handle_drag(str(nonexistent)) assert result is False -class TestDragInterceptorMultipleFiles: - """Test drag operations with multiple files.""" +class TestDragInterceptorAzureURL: + """Test Azure URL to local path conversion in drag operations.""" - def test_initiate_drag_multiple_files(self, qtbot, tmp_path): - """Test drag with multiple valid files.""" - # Create test files - file1 = tmp_path / "file1.txt" - file2 = tmp_path / "file2.txt" - file1.write_text("content 1") - file2.write_text("content 2") + def test_handle_drag_azure_url(self, qtbot, tmp_path): + """Test handling drag with Azure Blob Storage URL.""" + from webdrop_bridge.config import URLMapping - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) + # Create test file that would be the result + test_file = tmp_path / "test.png" + test_file.write_text("image data") - from PySide6.QtCore import Qt + config = Config( + app_name="Test", + app_version="1.0.0", + log_level="INFO", + log_file=None, + allowed_roots=[tmp_path], + allowed_urls=[], + webapp_url="https://test.com/", + url_mappings=[ + URLMapping( + url_prefix="https://wpsagravitystg.file.core.windows.net/wpsagravitysync/", + local_path=str(tmp_path) + ) + ], + check_file_exists=True, + ) + interceptor = DragInterceptor(config) + + # Azure URL + azure_url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/test.png" + + # Mock the drag operation with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag: mock_drag_instance = MagicMock() + from PySide6.QtCore import Qt mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction mock_drag.return_value = mock_drag_instance - result = interceptor.initiate_drag([str(file1), str(file2)]) + result = interceptor.handle_drag(azure_url) assert result is True - def test_initiate_drag_mixed_valid_invalid(self, qtbot, tmp_path): - """Test drag with mix of valid and invalid paths fails.""" - test_file = tmp_path / "valid.txt" - test_file.write_text("content") + def test_handle_drag_unmapped_url(self, qtbot, test_config): + """Test handling drag with unmapped URL fails.""" + interceptor = DragInterceptor(test_config) - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) + # URL with no mapping + unmapped_url = "https://unknown.blob.core.windows.net/container/file.png" - # Mix of valid and invalid paths with qtbot.waitSignal(interceptor.drag_failed): - result = interceptor.initiate_drag( - [str(test_file), "/etc/passwd"] - ) + result = interceptor.handle_drag(unmapped_url) assert result is False -class TestDragInterceptorMimeData: - """Test MIME data creation and file URL formatting.""" - - def test_mime_data_creation(self, qtbot, tmp_path): - """Test MIME data is created with proper file URLs.""" - test_file = tmp_path / "test.txt" - test_file.write_text("test") - - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) - - from PySide6.QtCore import Qt - with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag: - mock_drag_instance = MagicMock() - mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction - mock_drag.return_value = mock_drag_instance - - interceptor.initiate_drag([str(test_file)]) - - # Check MIME data was set correctly - call_args = mock_drag_instance.setMimeData.call_args - mime_data = call_args[0][0] - - # Verify URLs were set - urls = mime_data.urls() - assert len(urls) == 1 - # Check that the URL contains file:// scheme (can be string repr or QUrl) - url_str = str(urls[0]).lower() - assert "file://" in url_str - - class TestDragInterceptorSignals: """Test signal emission on drag operations.""" @@ -185,153 +178,60 @@ class TestDragInterceptorSignals: test_file = tmp_path / "test.txt" test_file.write_text("content") - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) - - from PySide6.QtCore import Qt + config = Config( + app_name="Test", + app_version="1.0.0", + log_level="INFO", + log_file=None, + allowed_roots=[tmp_path], + allowed_urls=[], + webapp_url="https://test.com/", + url_mappings=[], + check_file_exists=True, + ) + interceptor = DragInterceptor(config) # Connect to signal manually signal_spy = [] - interceptor.drag_started.connect(lambda paths: signal_spy.append(paths)) + interceptor.drag_started.connect(lambda src, path: signal_spy.append((src, path))) + from PySide6.QtCore import Qt with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag: mock_drag_instance = MagicMock() mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction mock_drag.return_value = mock_drag_instance - result = interceptor.initiate_drag([str(test_file)]) + result = interceptor.handle_drag(str(test_file)) # Verify result and signal emission assert result is True assert len(signal_spy) == 1 - def test_drag_failed_signal_on_no_files(self, qtbot): - """Test drag_failed signal on empty file list.""" - interceptor = DragInterceptor() + def test_drag_failed_signal_on_empty_text(self, qtbot, test_config): + """Test drag_failed signal on empty text.""" + interceptor = DragInterceptor(test_config) # Connect to signal manually signal_spy = [] - interceptor.drag_failed.connect(lambda msg: signal_spy.append(msg)) + interceptor.drag_failed.connect(lambda src, msg: signal_spy.append((src, msg))) - result = interceptor.initiate_drag([]) + result = interceptor.handle_drag("") # Verify result and signal emission assert result is False assert len(signal_spy) == 1 - assert "No files" in signal_spy[0] + assert "Empty" in signal_spy[0][1] - def test_drag_failed_signal_on_validation_error(self, qtbot, tmp_path): + def test_drag_failed_signal_on_validation_error(self, qtbot, test_config): """Test drag_failed signal on validation failure.""" - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) + interceptor = DragInterceptor(test_config) # Connect to signal manually signal_spy = [] - interceptor.drag_failed.connect(lambda msg: signal_spy.append(msg)) + interceptor.drag_failed.connect(lambda src, msg: signal_spy.append((src, msg))) - result = interceptor.initiate_drag(["/invalid/path/file.txt"]) + result = interceptor.handle_drag("/invalid/path/file.txt") # Verify result and signal emission assert result is False assert len(signal_spy) == 1 - - -class TestDragInterceptorDragExecution: - """Test drag operation execution and result handling.""" - - def test_drag_cancelled_returns_false(self, qtbot, tmp_path): - """Test drag cancellation returns False.""" - test_file = tmp_path / "test.txt" - test_file.write_text("content") - - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) - - with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag: - mock_drag_instance = MagicMock() - mock_drag_instance.exec.return_value = 0 # Cancelled/failed - mock_drag.return_value = mock_drag_instance - - # Connect to signal manually - signal_spy = [] - interceptor.drag_failed.connect(lambda msg: signal_spy.append(msg)) - - result = interceptor.initiate_drag([str(test_file)]) - - assert result is False - assert len(signal_spy) == 1 - - def test_pixmap_created_from_widget(self, qtbot, tmp_path): - """Test pixmap is created from widget grab.""" - test_file = tmp_path / "test.txt" - test_file.write_text("content") - - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) - - from PySide6.QtCore import Qt - with patch.object(interceptor, "grab") as mock_grab: - mock_pixmap = MagicMock() - mock_grab.return_value.scaled.return_value = mock_pixmap - - with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag: - mock_drag_instance = MagicMock() - mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction - mock_drag.return_value = mock_drag_instance - - interceptor.initiate_drag([str(test_file)]) - - # Verify grab was called and pixmap was set - mock_grab.assert_called_once() - mock_drag_instance.setPixmap.assert_called_once_with(mock_pixmap) - - -class TestDragInterceptorIntegration: - """Integration tests with PathValidator.""" - - def test_drag_with_nested_file(self, qtbot, tmp_path): - """Test drag with file in nested directory.""" - nested_dir = tmp_path / "nested" / "dir" - nested_dir.mkdir(parents=True) - test_file = nested_dir / "file.txt" - test_file.write_text("nested content") - - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) - - from PySide6.QtCore import Qt - with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag: - mock_drag_instance = MagicMock() - mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction - mock_drag.return_value = mock_drag_instance - - result = interceptor.initiate_drag([str(test_file)]) - - assert result is True - - def test_drag_with_relative_path(self, qtbot, tmp_path): - """Test drag with relative path resolution.""" - test_file = tmp_path / "relative.txt" - test_file.write_text("content") - - interceptor = DragInterceptor() - validator = PathValidator([tmp_path]) - interceptor.set_validator(validator) - - # This would work if run from the directory, but we'll just verify - # the interceptor handles Path objects correctly - from PySide6.QtCore import Qt - with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag: - mock_drag_instance = MagicMock() - mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction - mock_drag.return_value = mock_drag_instance - - # Direct absolute path for reliable test - result = interceptor.initiate_drag([str(test_file)]) - - assert result is True diff --git a/tests/unit/test_main_window.py b/tests/unit/test_main_window.py index 75216e0..72a53d3 100644 --- a/tests/unit/test_main_window.py +++ b/tests/unit/test_main_window.py @@ -231,10 +231,12 @@ class TestMainWindowDragIntegration: assert window.drag_interceptor.drag_started is not None assert window.drag_interceptor.drag_failed is not None - def test_initiate_drag_delegates_to_interceptor( + def test_handle_drag_delegates_to_interceptor( self, qtbot, sample_config, tmp_path ): - """Test initiate_drag method delegates to interceptor.""" + """Test drag handling delegates to interceptor.""" + from PySide6.QtCore import QCoreApplication + window = MainWindow(sample_config) qtbot.addWidget(window) @@ -243,29 +245,32 @@ class TestMainWindowDragIntegration: test_file.write_text("test") with patch.object( - window.drag_interceptor, "initiate_drag" + window.drag_interceptor, "handle_drag" ) as mock_drag: mock_drag.return_value = True - result = window.initiate_drag([str(test_file)]) + # Call through bridge + window._drag_bridge.start_file_drag(str(test_file)) + + # Process deferred QTimer.singleShot(0, ...) call + QCoreApplication.processEvents() - mock_drag.assert_called_once_with([str(test_file)]) - assert result is True + mock_drag.assert_called_once_with(str(test_file)) def test_on_drag_started_called(self, qtbot, sample_config): """Test _on_drag_started handler can be called.""" window = MainWindow(sample_config) qtbot.addWidget(window) - # Should not raise - window._on_drag_started(["/some/path"]) + # Should not raise - new signature has source and local_path + window._on_drag_started("https://example.com/file.png", "/local/path/file.png") def test_on_drag_failed_called(self, qtbot, sample_config): """Test _on_drag_failed handler can be called.""" window = MainWindow(sample_config) qtbot.addWidget(window) - # Should not raise - window._on_drag_failed("Test error message") + # Should not raise - new signature has source and error + window._on_drag_failed("https://example.com/file.png", "Test error message") class TestMainWindowURLWhitelist: diff --git a/tests/unit/test_url_converter.py b/tests/unit/test_url_converter.py new file mode 100644 index 0000000..60ab58f --- /dev/null +++ b/tests/unit/test_url_converter.py @@ -0,0 +1,144 @@ +"""Unit tests for URL converter.""" + +from pathlib import Path + +import pytest + +from webdrop_bridge.config import Config, URLMapping +from webdrop_bridge.core.url_converter import URLConverter + + +@pytest.fixture +def test_config(): + """Create test configuration with URL mappings.""" + return Config( + app_name="Test App", + app_version="1.0.0", + log_level="INFO", + log_file=None, + allowed_roots=[], + allowed_urls=[], + webapp_url="https://wps.agravity.io/", + url_mappings=[ + URLMapping( + url_prefix="https://wpsagravitystg.file.core.windows.net/wpsagravitysync/", + local_path="Z:" + ), + URLMapping( + url_prefix="https://other.blob.core.windows.net/container/", + local_path="Y:\\shared" + ) + ] + ) + + +@pytest.fixture +def converter(test_config): + """Create URL converter with test config.""" + return URLConverter(test_config) + + +def test_convert_simple_url(converter): + """Test converting a simple Azure URL to local path.""" + url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/test/file.png" + result = converter.convert_url_to_path(url) + + assert result is not None + assert str(result).endswith("test\\file.png") # Windows path separator + + +def test_convert_url_with_special_characters(converter): + """Test URL with special characters (URL encoded).""" + url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/folder/file%20with%20spaces.png" + result = converter.convert_url_to_path(url) + + assert result is not None + assert "file with spaces.png" in str(result) + + +def test_convert_url_with_subdirectories(converter): + """Test URL with deep directory structure.""" + url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/subfolder/file.png" + result = converter.convert_url_to_path(url) + + assert result is not None + assert "aN5PysnXIuRECzcRbvHkjL7g0" in str(result) + assert "subfolder" in str(result) + + +def test_convert_unmapped_url(converter): + """Test URL that doesn't match any mapping.""" + url = "https://unknown.blob.core.windows.net/container/file.png" + result = converter.convert_url_to_path(url) + + assert result is None + + +def test_convert_empty_url(converter): + """Test empty URL.""" + result = converter.convert_url_to_path("") + assert result is None + + +def test_convert_none_url(converter): + """Test None URL.""" + result = converter.convert_url_to_path(None) + assert result is None + + +def test_is_azure_url_positive(converter): + """Test recognizing valid Azure URLs.""" + url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/file.png" + assert converter.is_azure_url(url) is True + + +def test_is_azure_url_negative(converter): + """Test rejecting non-Azure URLs.""" + assert converter.is_azure_url("https://example.com/file.png") is False + assert converter.is_azure_url("Z:\\file.png") is False + assert converter.is_azure_url("") is False + + +def test_multiple_mappings(converter): + """Test that correct mapping is used for URL.""" + url1 = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/file.png" + url2 = "https://other.blob.core.windows.net/container/file.png" + + result1 = converter.convert_url_to_path(url1) + result2 = converter.convert_url_to_path(url2) + + assert result1 is not None + assert result2 is not None + assert str(result1).startswith("Z:") + assert str(result2).startswith("Y:") + + +def test_url_mapping_validation_http(): + """Test that URL mapping requires http:// or https://.""" + with pytest.raises(Exception): # ConfigurationError + URLMapping( + url_prefix="ftp://server/path/", + local_path="Z:" + ) + + +def test_url_mapping_adds_trailing_slash(): + """Test that URL mapping adds trailing slash if missing.""" + mapping = URLMapping( + url_prefix="https://example.com/path", + local_path="Z:" + ) + assert mapping.url_prefix.endswith("/") + + +def test_convert_url_example_from_docs(converter): + """Test the exact example from documentation.""" + url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png" + result = converter.convert_url_to_path(url) + + assert result is not None + # Should be: Z:\aN5PysnXIuRECzcRbvHkjL7g0\Hintergrund_Agravity.png + expected_parts = ["Z:", "aN5PysnXIuRECzcRbvHkjL7g0", "Hintergrund_Agravity.png"] + result_str = str(result) + for part in expected_parts: + assert part in result_str diff --git a/tests/unit/test_validator.py b/tests/unit/test_validator.py index db8ae61..eabb228 100644 --- a/tests/unit/test_validator.py +++ b/tests/unit/test_validator.py @@ -22,11 +22,12 @@ class TestPathValidator: assert len(validator.allowed_roots) == 2 def test_validator_nonexistent_root(self, tmp_path): - """Test that nonexistent root raises ValidationError.""" + """Test that nonexistent root is logged as warning but doesn't raise error.""" nonexistent = tmp_path / "nonexistent" - with pytest.raises(ValidationError, match="does not exist"): - PathValidator([nonexistent]) + # Should not raise - just logs warning and skips the root + validator = PathValidator([nonexistent]) + assert len(validator.allowed_roots) == 0 # Non-existent roots are skipped def test_validator_non_directory_root(self, tmp_path): """Test that non-directory root raises ValidationError.""" diff --git a/webapp/index.html b/webapp/index.html index ac302bf..b675dc3 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -162,20 +162,26 @@
🖼️
-

Sample Image

+

Local Z:\ Image

Z:\data\test-image.jpg

📄
-

Sample Document

+

Local Z:\ Document

Z:\data\API_DOCUMENTATION.pdf

-
📊
-

Sample Data

-

C:\Users\Public\data.csv

+
☁️
+

Azure Blob Storage Image

+

https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png

+
+ +
+
☁️
+

Azure Blob Storage Document

+

https://wpsagravitystg.file.core.windows.net/wpsagravitysync/test/document.pdf

@@ -183,15 +189,59 @@

How to test:

  1. Open InDesign, Word, or Notepad++
  2. -
  3. Drag one of the items below to the application
  4. -
  5. The file path should be converted to a real file drag
  6. +
  7. Drag one of the items above to the application
  8. +
  9. Local Z:\ paths and Azure URLs will be converted to file drags
  10. +
  11. Azure URLs will be mapped to Z:\ paths automatically
  12. Check the browser console (F12) for debug info
+

Note: When dragging images from web apps like Agravity, the browser may not provide text/plain data. Press ALT while dragging to force text drag mode.

+ + From dee02ad60011b81d52c3bfc2420e64c4f8bf865d Mon Sep 17 00:00:00 2001 From: claudi Date: Tue, 17 Feb 2026 19:19:14 +0100 Subject: [PATCH 02/16] Add drag & drop script variants and enhanced debugging tools - Introduced multiple JavaScript scripts for handling drag & drop functionality: - `bridge_script.js`: Original implementation with popup prevention. - `bridge_script_debug.js`: Debug version with extensive logging for troubleshooting. - `bridge_script_v2.js`: Enhanced version extending DataTransfer for better integration. - `bridge_script_hybrid.js`: Hybrid approach allowing parallel native file drag. - `bridge_script_drop_intercept.js`: Intercepts drop events for custom handling. - `bridge_script_intercept.js`: Prevents browser drag for ALT+drag, using Qt for file drag. - Added detailed documentation in `SCRIPT_VARIANTS.md` outlining usage, status, and recommended workflows for each script. - Implemented logging features to capture drag events, DataTransfer modifications, and network requests for better debugging. - Enhanced DataTransfer handling to support Windows-specific file formats and improve user experience during drag & drop operations. --- QUICKSTART_DEBUGGING.md | 222 ++++++++++++ docs/ANGULAR_CDK_ANALYSIS.md | 268 ++++++++++++++ docs/DRAG_DROP_PROBLEM_ANALYSIS.md | 277 +++++++++++++++ docs/SCRIPT_VARIANTS.md | 171 +++++++++ src/webdrop_bridge/ui/bridge_script.js | 139 +++++++- src/webdrop_bridge/ui/bridge_script_debug.js | 333 ++++++++++++++++++ .../ui/bridge_script_drop_intercept.js | 211 +++++++++++ src/webdrop_bridge/ui/bridge_script_hybrid.js | 109 ++++++ .../ui/bridge_script_intercept.js | 165 +++++++++ src/webdrop_bridge/ui/bridge_script_v2.js | 214 +++++++++++ src/webdrop_bridge/ui/main_window.py | 197 ++++++++--- src/webdrop_bridge/ui/restricted_web_view.py | 3 +- 12 files changed, 2244 insertions(+), 65 deletions(-) create mode 100644 QUICKSTART_DEBUGGING.md create mode 100644 docs/ANGULAR_CDK_ANALYSIS.md create mode 100644 docs/DRAG_DROP_PROBLEM_ANALYSIS.md create mode 100644 docs/SCRIPT_VARIANTS.md create mode 100644 src/webdrop_bridge/ui/bridge_script_debug.js create mode 100644 src/webdrop_bridge/ui/bridge_script_drop_intercept.js create mode 100644 src/webdrop_bridge/ui/bridge_script_hybrid.js create mode 100644 src/webdrop_bridge/ui/bridge_script_intercept.js create mode 100644 src/webdrop_bridge/ui/bridge_script_v2.js diff --git a/QUICKSTART_DEBUGGING.md b/QUICKSTART_DEBUGGING.md new file mode 100644 index 0000000..ffdc572 --- /dev/null +++ b/QUICKSTART_DEBUGGING.md @@ -0,0 +1,222 @@ +# 🚀 QUICK START - Popup-Problem lösen + +## Problem Zusammenfassung + +- ✅ File-Drop funktioniert (Z:\ Laufwerk) +- ❌ Web-App Popup erscheint nicht nach Drop (Auschecken-Dialog) + +**Grund**: Unser JavaScript verhindert das Browser-Drag-Event mit `preventDefault()`, daher bekommt die Web-App kein Drop-Event. + +## 🎯 Lösungsstrategie + +**Phase 1: DEBUGGING** (ca. 15-30 Min) +→ Herausfinden WIE das Popup ausgelöst wird + +**Phase 2: IMPLEMENTATION** (ca. 30-60 Min) +→ Popup-Trigger nach File-Drop manuell aufrufen + +## 📋 Phase 1: Debugging - JETZT STARTEN + +### Schritt 1: Debug-Script aktivieren + +```powershell +# Datei öffnen +code "C:\Development\VS Code Projects\webdrop_bridge\src\webdrop_bridge\ui\main_window.py" +``` + +**Zeile ~433** in `_install_bridge_script()` ändern: + +```python +# VORHER: +script_path = Path(__file__).parent / "bridge_script.js" + +# NACHHER (für Debugging): +script_path = Path(__file__).parent / "bridge_script_debug.js" +``` + +Speichern (Ctrl+S). + +### Schritt 2: Anwendung starten + +```powershell +cd "C:\Development\VS Code Projects\webdrop_bridge" +python -m webdrop_bridge.main +``` + +### Schritt 3: Browser DevTools öffnen + +1. Wenn WebDrop Bridge lädt +2. **F12** drücken → DevTools öffnen +3. **Console-Tab** auswählen +4. Sie sollten sehen: + `[WebDrop DEBUG] Ready! Perform ALT-drag+drop and watch the logs.` + +### Schritt 4: ALT-Drag+Drop durchführen + +1. In der GlobalDAM Anwendung: + - **ALT-Taste** gedrückt halten + - Asset **draggen** + - Irgendwo **droppen** (z.B. auf dem Desktop) + +2. **Popup sollte erscheinen!** (Auschecken-Ja/Nein) + +3. **Sofort** zur Browser-Console wechseln! + +### Schritt 5: Logs analysieren + +Suchen Sie in der Console nach: + +#### ✅ **A) Modal/Popup Detektion** +``` +[MODAL OPENED] <<<< DAS WOLLEN WIR SEHEN! +Modal element:
+Classes: modal-dialog checkout-dialog +``` + +**Wenn gefunden →** Popup wird durch DOM-Manipulation ausgelöst +**Merkblatt:** Notieren Sie die `className` und `textContent` + +#### ✅ **B) API-Call Detection** +``` +[FETCH] https://dev.agravity.io/api/assets/abc123/checkout +``` +oder +``` +[XHR] POST https://dev.agravity.io/api/assets/abc123/checkout +``` + +**Wenn gefunden →** Popup wird durch API-Response ausgelöst +**Merkblatt:** Notieren Sie die vollständige URL und Methode (POST/GET) + +#### ✅ **C) Event Detection** +``` +[EVENT] DROP +Target: ... +DataTransfer: ... +``` + +**Merkblatt:** Notieren Sie welche Events feuern NACHDEM Drop passiert + +### Schritt 6: Detailanalyse + +In der Browser Console, führen Sie aus: + +```javascript +// Event-Statistik +webdrop_debug.getEventCounts() + +// Asset-Card Component untersuchen (wenn Angular DevTools installiert) +var card = document.querySelector('ay-asset-card'); +var component = webdrop_debug.getComponent(card); +console.log('Component methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(component))); +``` + +**Merkblatt:** Notieren Sie: +- Welche Methoden hat das Component? (z.B. `onCheckout`, `showDialog`, etc.) +- Gibt es einen `click`-Handler auf dem Asset? + +## 📝 Was Sie herausfinden müssen + +Nach dem Debugging sollten Sie wissen: + +1. **WIE wird Popup ausgelöst?** + - [ ] API-Call nach Drop (URL: ________________________) + - [ ] DOM-Element wird erstellt (Class: ________________) + - [ ] Angular Component Methode (Name: ________________) + - [ ] Click-Event auf Button (Selector: _______________) + +2. **WANN wird Popup ausgelöst?** + - [ ] Sofort nach Drop + - [ ] Nach Verzögerung (_____ Sekunden) + - [ ] Nach API-Response + - [ ] Nach User-Interaction + +3. **WELCHE Daten werden benötigt?** + - [ ] Asset-ID (Beispiel: _________________________) + - [ ] Collection-ID (Beispiel: ______________________) + - [ ] User-ID (Beispiel: ___________________________) + - [ ] Weitere: ____________________________________ + +## 🔧 Phase 2: Implementation (NACHDEM Debugging abgeschlossen) + +Basierend auf Ihren Erkenntnissen: + +### Fall A: Popup durch API-Call + +**In `bridge_script.js` hinzufügen** (nach erfolgreichem File-Drop): + +```javascript +// Nach Qt File-Drop Erfolg +function triggerCheckoutPopup(assetId) { + fetch('https://dev.agravity.io/api/assets/' + assetId + '/checkout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // ggf. Authorization Header + }, + body: JSON.stringify({ + // Parameter basierend auf Debug-Logs + }) + }) + .then(response => response.json()) + .then(data => { + console.log('Checkout popup triggered via API'); + }); +} +``` + +### Fall B: Popup durch Component-Methode + +```javascript +function triggerCheckoutPopup(assetId) { + var card = document.getElementById(assetId); + if (card && window.ng) { + var component = ng.getComponent(card); + if (component && component.onCheckout) { + component.onCheckout(); + } + } +} +``` + +### Fall C: Popup durch Click-Simulation + +```javascript +function triggerCheckoutPopup(assetId) { + var button = document.querySelector('[data-action="checkout"]'); + if (button) { + button.click(); + } +} +``` + +## ⚡ Troubleshooting + +**Problem:** Kein `[MODAL OPENED]` Log +**Lösung:** Popup wird möglicherweise anders erstellt. Suchen Sie in Network-Tab nach API-Calls + +**Problem:** Zu viele Logs +**Lösung:** Console Filter nutzen: `[MODAL]` oder `[FETCH]` + +**Problem:** `webdrop_debug` not defined +**Lösung:** Debug-Script wurde nicht richtig geladen. main_window.py prüfen. + +**Problem:** Angular DevTools fehlt +**Lösung:** Chrome Extension installieren: "Angular DevTools" + +## 📞 Nächste Schritte + +1. ✅ Debug-Script aktivieren (oben Schritt 1) +2. ✅ Logs sammeln (Schritt 3-6) +3. ✅ Ergebnisse notieren (Merkblatt) +4. → **Ergebnisse mitteilen** → Ich helfe bei Implementation! + +## 📄 Zusätzliche Dokumentation + +- [DRAG_DROP_PROBLEM_ANALYSIS.md](DRAG_DROP_PROBLEM_ANALYSIS.md) - Detaillierte Problem-Analyse +- [SCRIPT_VARIANTS.md](SCRIPT_VARIANTS.md) - Alle verfügbaren Scripts +- [ANGULAR_CDK_ANALYSIS.md](ANGULAR_CDK_ANALYSIS.md) - Angular Framework Details + +--- + +**Hinweis:** Das Debug-Script hat Performance-Overhead. Nach dem Debugging zurück zu `bridge_script.js` wechseln! diff --git a/docs/ANGULAR_CDK_ANALYSIS.md b/docs/ANGULAR_CDK_ANALYSIS.md new file mode 100644 index 0000000..5e434ad --- /dev/null +++ b/docs/ANGULAR_CDK_ANALYSIS.md @@ -0,0 +1,268 @@ +# Angular CDK Drag & Drop Analysis - GlobalDAM + +## Framework Detection + +**Web Application:** Agravity GlobalDAM +**Framework:** Angular 19.2.14 +**Drag & Drop:** Angular CDK (Component Dev Kit) +**Styling:** TailwindCSS + +## Technical Findings + +### 1. Angular CDK Implementation + +```html + +
+ + +
+ + +
  • + weiss_ORIGINAL +
  • +
    +
    +``` + +### 2. Key Observations + +#### Native HTML5 Drag ist DEAKTIVIERT +```html +draggable="false" +``` + +**Bedeutung:** +- Kein Zugriff auf native `dragstart`, `drag`, `dragend` Events +- Kein `event.dataTransfer` API verfügbar +- Angular CDK simuliert Drag & Drop komplett in JavaScript +- Daten werden NICHT über natives Clipboard/DataTransfer übertragen + +#### Angular CDK Direktiven +- `cdkdroplistgroup` - Gruppiert mehrere Drop-Zonen +- `cdkdroplist` - Markiert Drop-Bereiche (Collections, Clipboard) +- `cdkdrag` - Markiert draggbare Elemente (Assets) +- `cdkdroplistsortingdisabled` - Sortierung deaktiviert + +#### Asset Identifikation +```html + +
    + + + + + +weiss_ORIGINAL +``` + +## Impact on WebDrop Bridge + +### ❌ Bisheriger Ansatz funktioniert NICHT + +Unser aktueller Ansatz basiert auf: +1. Interception von nativen Drag-Events +2. Manipulation von `event.dataTransfer.effectAllowed` und `.dropEffect` +3. Setzen von URLs im DataTransfer + +**Das funktioniert NICHT mit Angular CDK**, da: +- Angular CDK das native Drag & Drop komplett umgeht +- Keine nativen Events gefeuert werden +- DataTransfer API nicht verwendet wird + +### ✅ Mögliche Lösungsansätze + +#### Ansatz 1: JavaScript Injection zur Laufzeit +Injiziere JavaScript-Code, der Angular CDK Events abfängt: + +```javascript +// Überwache Angular CDK Event-Handler +document.addEventListener('cdkDragStarted', (event) => { + const assetId = event.source.element.nativeElement.id; + const assetName = event.source.element.nativeElement.querySelector('img')?.alt; + + // Sende an Qt WebChannel + bridge.handleDragStart(assetId, assetName); +}); + +document.addEventListener('cdkDragDropped', (event) => { + // Verhindere das Standard-Verhalten + event.preventDefault(); + + // Starte nativen Drag von Qt aus + bridge.initNativeDrag(); +}); +``` + +**Vorteile:** +- ✅ Direkter Zugriff auf Angular CDK Events +- ✅ Kann Asset-Informationen extrahieren +- ✅ Kann Drag-Operationen abfangen + +**Nachteile:** +- ⚠️ Erfordert genaue Kenntnis der Angular CDK Internals +- ⚠️ Könnte bei Angular CDK Updates brechen +- ⚠️ Komplexer zu implementieren + +#### Ansatz 2: DOM Mutation Observer +Überwache DOM-Änderungen während des Drags: + +```javascript +const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // Suche nach CDK Drag-Elementen mit bestimmten Klassen + const dragElement = document.querySelector('.cdk-drag-preview'); + if (dragElement) { + const assetId = dragElement.querySelector('[id^="a"]')?.id; + bridge.handleDrag(assetId); + } + }); +}); + +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class'] +}); +``` + +**Vorteile:** +- ✅ Robuster gegenüber Framework-Updates +- ✅ Funktioniert mit beliebigen Frameworks + +**Nachteile:** +- ⚠️ Performance-Overhead +- ⚠️ Kann falsche Positive erzeugen + +#### Ansatz 3: Qt WebChannel Bridge mit Custom Events +Nutze Qt WebChannel, um mit der Angular-Anwendung zu kommunizieren: + +```python +# Python-Seite (Qt) +class DragBridge(QObject): + @Slot(str, str) + def onAssetDragStart(self, asset_id: str, asset_name: str): + """Called from JavaScript when Angular CDK drag starts.""" + logger.info(f"Asset drag started: {asset_id} ({asset_name})") + self.convert_and_drag(asset_id, asset_name) +``` + +```javascript +// JavaScript-Seite (injiziert via QWebEngineScript) +new QWebChannel(qt.webChannelTransport, (channel) => { + const dragBridge = channel.objects.dragBridge; + + // Monkey-patch Angular CDK's DragRef + const originalStartDraggingSequence = CdkDrag.prototype._startDraggingSequence; + CdkDrag.prototype._startDraggingSequence = function(event) { + const assetElement = this.element.nativeElement; + const assetId = assetElement.id; + const assetName = assetElement.querySelector('img')?.alt; + + // Benachrichtige Qt + dragBridge.onAssetDragStart(assetId, assetName); + + // Rufe original Angular CDK Methode auf + return originalStartDraggingSequence.call(this, event); + }; +}); +``` + +**Vorteile:** +- ✅ Saubere Kommunikation zwischen Qt und Web +- ✅ Kann Asset-Informationen zuverlässig extrahieren +- ✅ Typensicher (Qt Signals/Slots) + +**Nachteile:** +- ⚠️ Erfordert Monkey-Patching von Angular CDK +- ⚠️ Kann bei CDK Updates brechen + +#### Ansatz 4: Browser DevTools Protocol (Chrome DevTools) +Nutze Chrome DevTools Protocol für tiefere Integration: + +```python +from PySide6.QtWebEngineCore import QWebEngineProfile + +profile = QWebEngineProfile.defaultProfile() +profile.setRequestInterceptor(...) + +# Intercepte Netzwerk-Requests und injiziere Header +# Überwache JavaScript-Execution via CDP +``` + +**Vorteile:** +- ✅ Sehr mächtig, kann JavaScript-Execution überwachen +- ✅ Kann Events auf niedrigerer Ebene abfangen + +**Nachteile:** +- ⚠️ Sehr komplex +- ⚠️ Erfordert Chrome DevTools Protocol Kenntnisse +- ⚠️ Performance-Overhead + +## Empfohlener Ansatz + +### **Ansatz 3: Qt WebChannel Bridge** (BEVORZUGT) + +**Begründung:** +1. ✅ Saubere Architektur mit klarer Trennung +2. ✅ Typsicher durch Qt Signals/Slots +3. ✅ Kann Asset-IDs und -Namen zuverlässig extrahieren +4. ✅ Funktioniert auch wenn Angular CDK interne Änderungen hat +5. ✅ Ermöglicht bidirektionale Kommunikation + +**Implementierungsschritte:** + +### Phase 1: Asset-Informationen extrahieren +1. JavaScript via QWebEngineScript injizieren +2. Qt WebChannel setuppen +3. Angular CDK Events überwachen (ohne Monkey-Patching als Test) +4. Asset-IDs und Namen an Qt senden + +### Phase 2: Native Drag initiieren +1. Bei CDK Drag-Start: Extrahiere Asset-Informationen +2. Sende Asset-ID an Backend/API +3. Erhalte lokalen Dateipfad oder Azure Blob URL +4. Konvertiere zu lokalem Pfad (wie aktuell) +5. Initiiere nativen Drag mit QDrag + +### Phase 3: Drag-Feedback +1. Zeige Drag-Preview in Qt (optional) +2. Update Cursor während Drag +3. Cleanup nach Drag-Ende + +## Asset-ID zu Dateipfad Mapping + +Die Anwendung verwendet Asset-IDs in mehreren Formaten: + +```javascript +// Asset-ID: anPGZszKzgKaSz1SIx2HFgduy + +// Mögliche URL-Konstruktion: +const assetUrl = `https://dev.agravity.io/api/assets/${assetId}`; +const downloadUrl = `https://dev.agravity.io/api/assets/${assetId}/download`; +const blobUrl = `https://static.agravity.io/${workspaceId}/${assetId}/${filename}`; +``` + +**Für WebDrop Bridge:** +- Asset-ID aus DOM extrahieren +- Asset-Metadaten via API abrufen (falls verfügbar) +- Blob-URL konstruieren +- URL Converter nutzen (bereits implementiert!) + +## Next Steps + +1. **Proof of Concept**: Qt WebChannel mit einfachem Event-Logger +2. **Asset-ID Extraction**: JavaScript Injection testen +3. **API Research**: GlobalDAM API untersuchen (Asset-Metadaten) +4. **Integration**: Mit bestehendem URLConverter verbinden +5. **Testing**: Mit echten Assets testen + +## Hinweise + +- Angular CDK Version kann sich unterscheiden - Code muss robust sein +- Asset-IDs scheinen eindeutig zu sein (Base64-ähnlich) +- Die Anwendung nutzt Azure Blob Storage (basierend auf bisherigen URLs) +- Custom Components (`ay-*`) deuten auf eine eigene Component Library hin diff --git a/docs/DRAG_DROP_PROBLEM_ANALYSIS.md b/docs/DRAG_DROP_PROBLEM_ANALYSIS.md new file mode 100644 index 0000000..7e6906d --- /dev/null +++ b/docs/DRAG_DROP_PROBLEM_ANALYSIS.md @@ -0,0 +1,277 @@ +# Drag & Drop Problem Analysis - File Drop + Web App Popup + +## Das Kernproblem + +**Ziel**: Bei ALT-Drag soll: +1. ✅ File gedroppt werden (Z:\ Laufwerk) → Native File-Drop +2. ✅ Web-App Popup erscheinen (Auschecken-Dialog) → Web-App Drop-Event + +**Problem**: Diese beiden schließen sich gegenseitig aus: +- Native File-Drag (von Qt) → Web-App bekommt kein Drop-Event → Kein Popup +- Browser Text-Drag (von Web-App) → Kein File-Drop → Kein File + +## Browser-Sicherheitsbeschränkungen + +1. **DataTransfer.files ist read-only** + Wir können keine Files zu einem DataTransfer hinzufügen + +2. **Nur EIN Drag zur Zeit möglich** + Wir können keinen parallelen Drag starten + +3. **DataTransfer kann nicht erstellt werden** + Wir können kein synthetisches Drop-Event mit Files erzeugen + +4. **Cross-Domain Sicherheit** + File-Zugriff ist stark eingeschränkt + +## Getestete Ansätze + +### ❌ Ansatz 1: DataTransfer erweitern +**Idee**: Web-App setzt URL, wir fügen Files hinzu +**Problem**: DataTransfer.files ist read-only + +### ❌ Ansatz 2: Parallele Drags +**Idee**: Browser-Drag + Qt-Drag gleichzeitig +**Problem**: Nur ein Drag zur Zeit möglich + +### ❌ Ansatz 3: Synthetisches Drop-Event +**Idee**: Original Drop abfangen, neues Event mit Files erzeugen +**Problem**: DataTransfer.files kann nicht gesetzt werden + +### ⚠️ Ansatz 4: Native Drag + Event-Simulation +**Idee**: Qt Native Drag, dann Web-App Event manuell auslösen +**Problem**: Erfordert Kenntnis der exakten Web-App Event-Struktur (Angular CDK) + +## 🎯 Praktikable Lösungen + +### Lösung A: **Zwei-Phasen Ansatz** (EMPFOHLEN für Testing) + +**Phase 1: File-Drop** +1. ALT-Drag startet +2. JavaScript erkennt convertible URL +3. Ruft Qt's `start_file_drag(url)` auf +4. `preventDefault()` verhindert Browser-Drag +5. Qt Native Drag läuft +6. File wird gedroppt ✅ + +**Phase 2: Popup manuell triggern** +7. Nach erfolgreichem Drop (via Qt Signal) +8. JavaScript simuliert den Event/API-Call der das Popup auslöst +9. Popup erscheint ✅ + +**Vorteile:** +- ✅ Funktioniert garantiert für File-Drop +- ✅ Kontrolle über beide Phasen +- ✅ Kann debugged werden + +**Nachteile:** +- ⚠️ Erfordert Reverse-Engineering des Popup-Triggers +- ⚠️ Könnte bei Web-App Updates brechen + +**Implementierung:** +```javascript +// Phase 1: Standard - File Drag +document.addEventListener('dragstart', function(e) { + if (!e.altKey) return; + + // Extract URL from DataTransfer + var url = e.dataTransfer.getData('text/plain'); + if (isConvertible(url)) { + e.preventDefault(); + window.bridge.start_file_drag(url); + } +}, true); + +// Phase 2: Popup Trigger (nach Drop-Erfolg) +// Qt ruft auf: window.trigger_checkout_popup(assetId) +window.trigger_checkout_popup = function(assetId) { + // Option 1: Klick auf Checkout-Button simulieren + // Option 2: API-Call direkt aufrufen + // Option 3: Angular-Event dispatchen + + // Beispiel: Suche nach Angular-Component und rufe Methode auf + var angularComponent = findAngularComponent('ay-asset-card'); + if (angularComponent) { + angularComponent.onCheckout(assetId); + } +}; +``` + +**TODO für diese Lösung:** +1. ✅ File-Drag funktioniert bereits +2. ⏸️ Herausfinden wie Popup ausgelöst wird: + - Browser DevTools öffnen + - Network-Tab beobachten (API-Call?) + - Elements-Tab nutzen (Angular Component?) + - Event-Listeners ansehen + +### Lösung B: **Gar nichts ändern beim Drag** (FALLBACK) + +**Ansatz:** +1. ALT-Drag läuft normal durch (URL-Text wird gedroppt) +2. Popup erscheint ✅ +3. Nach Popup-Bestätigung (Auschecken) +4. **DANN** konvertieren wir die URL und kopieren das File + +**Vorteile:** +- ✅ Web-App funktioniert normal +- ✅ Kein Popup-Problem +- ✅ Einfach zu implementieren + +**Nachteile:** +- ⚠️ Kein echter Drag & Drop (nur Copy) +- ⚠️ User muss zweimal handeln + +### Lösung C: **File-Drop via Qt Window Overlay** (EXPERIMENTELL) + +**Ansatz:** +1. Beim ALT-Drag: Start Normal Web-App Drag +2. Qt erstellt ein transparentes Overlay-Window über dem Browser +3. Overlay fängt das Drop-Event ab +4. Qt macht File-Drop +5. Qt leitet Drop-Koordinaten an Browser weiter +6. Browser bekommt synthetisches Event (ohne Files, nur Koordinaten) +7. Popup erscheint + +**Vorteile:** +- ✅ Beide Funktionen potentiell möglich +- ✅ Keine DataTransfer-Manipulation nötig + +**Nachteile:** +- ⚠️ Sehr komplex +- ⚠️ Plattform-spezifisch (Windows) +- ⚠️ Performance-Overhead + +## 🔬 Nächste Schritte - Reverse Engineering + +Um Lösung A zu implementieren, müssen wir herausfinden: + +### 1. Wie wird das Popup ausgelöst? + +**Debug-Schritte:** +```javascript +// In Browser Console ausführen während ALT-Drag+Drop + +// Methode 1: Event-Listener finden +getEventListeners(document) + +// Methode 2: Angular Component finden +var cards = document.querySelectorAll('ay-asset-card'); +var component = ng.getComponent(cards[0]); // Angular DevTools +console.log(component); + +// Methode 3: Network-Tab +// Schauen ob API-Call gemacht wird nach Drop +``` + +**Erwartete Möglichkeiten:** +- API-Call zu `/api/assets/{id}/checkout` +- Angular Event: `cdkDropListDropped` +- Component-Methode: `onAssetDropped()` oder ähnlich +- Modal-Service: `ModalService.show('checkout-dialog')` + +### 2. Asset-ID extrahieren + +Die Asset-ID wird benötigt für den Popup-Trigger: + +```javascript +// Asset-ID ist wahrscheinlich in: +// - Element-ID: "anPGZszKzgKaSz1SIx2HFgduy" +// - Image-URL: "./GlobalDAM JRI_files/anPGZszKzgKaSz1SIx2HFgduy" +// - Data-Attribut: data-asset-id + +function extractAssetId(element) { + // Aus Element-ID + if (element.id && element.id.match(/^a[A-Za-z0-9_-]+$/)) { + return element.id; + } + + // Aus Bild-URL + var img = element.querySelector('img'); + if (img && img.src) { + var match = img.src.match(/([A-Za-z0-9_-]{20,})/); + if (match) return match[1]; + } + + return null; +} +``` + +### 3. Popup programmatisch öffnen + +Sobald wir wissen wie, implementieren wir: + +```python +# Python (Qt) +class DragBridge(QObject): + @Slot(str) + def on_file_dropped_success(self, local_path: str): + """Called after successful file drop.""" + # Extrahiere Asset-ID aus Pfad oder URL-Mapping + asset_id = self.extract_asset_id(local_path) + + # Trigger Popup via JavaScript + js_code = f"window.trigger_checkout_popup('{asset_id}')" + self.web_view.page().runJavaScript(js_code) +``` + +## ⚡ Quick Win: Debugging aktivieren + +Fügen Sie zu **allen** JavaScript-Varianten umfangreiches Logging hinzu: + +```javascript +// In bridge_script.js + +// Log ALLE Events +['dragstart', 'drag', 'dragenter', 'dragover', 'drop', 'dragend'].forEach(function(eventName) { + document.addEventListener(eventName, function(e) { + console.log('[DEBUG]', eventName, { + target: e.target.tagName, + dataTransfer: { + types: e.dataTransfer.types, + effectAllowed: e.dataTransfer.effectAllowed, + dropEffect: e.dataTransfer.dropEffect + }, + altKey: e.altKey, + coordinates: {x: e.clientX, y: e.clientY} + }); + }, true); +}); + +// Log DataTransfer Zugriffe +Object.defineProperty(DataTransfer.prototype, 'types', { + get: function() { + var types = this._types || []; + console.log('[DEBUG] DataTransfer.types accessed:', types); + return types; + } +}); +``` + +## 📝 Empfehlung + +**Sofortige Maßnahmen:** + +1. ✅ **Lösung A Phase 1 ist bereits implementiert** (File-Drop funktioniert) + +2. 🔍 **Reverse-Engineering durchführen:** + - GlobalDAM JRI im Browser öffnen + - DevTools öffnen (F12) + - ALT-Drag+Drop durchführen + - Beobachten: + - Network-Tab → API-Calls? + - Console → Fehler/Logs? + - Angular DevTools → Component-Events? + +3. 🛠️ **Popup-Trigger implementieren:** + - Sobald bekannt WIE Popup ausgelöst wird + - JavaScript-Funktion `trigger_checkout_popup()` erstellen + - Von Qt aus nach erfolgreichem Drop aufrufen + +4. 🧪 **Testen:** + - ALT-Drag eines Assets + - File-Drop sollte funktionieren + - Popup sollte erscheinen + +**Fallback:** +Falls Reverse-Engineering zu komplex ist → **Lösung B** verwenden (Kein Drag, nur Copy nach Popup-Bestätigung) diff --git a/docs/SCRIPT_VARIANTS.md b/docs/SCRIPT_VARIANTS.md new file mode 100644 index 0000000..cd21eed --- /dev/null +++ b/docs/SCRIPT_VARIANTS.md @@ -0,0 +1,171 @@ +# WebDrop Bridge - Drag & Drop Script Varianten + +## Verfügbare JavaScript-Scripte + +### 1. `bridge_script.js` (CURRENT - Original) +**Status:** Versucht Drag zu ersetzen, verhindert Popup +**Verwendung:** Testing, zeigt dass File-Drop prinzipiell funktioniert +**Problem:** Web-App Popup erscheint nicht + +### 2. `bridge_script_debug.js` (DEBUG - **EMPFOHLEN ZUM START**) +**Status:** Umfangreiches Logging, keine Manipulation +**Verwendung:** Herausfinden wie Popup ausgelöst wird +**Funktionen:** +- Loggt ALLE Drag/Drop Events +- Überwacht DataTransfer.setData +- Überwacht Network-Requests (API-Calls) +- Erkennt Angular Components +- Erkennt Modal/Popup Öffnungen +- Bietet Helper-Funktionen in Console + +**So verwenden:** +```python +# In main_window.py Zeile ~433 ändern: +script_path = Path(__file__).parent / "bridge_script_debug.js" # <-- DEBUG aktivieren +``` + +**Im Browser:** +1. F12 → Console öffnen +2. ALT-Drag+Drop durchführen +3. Logs analysieren: + - `[MODAL OPENED]` → Popup-Trigger gefunden! + - `[FETCH]` oder `[XHR]` → API-Call gefunden! + - `[EVENT]` → Drag-Flow verstehen + +### 3. `bridge_script_v2.js` (EXTEND - Experimentell) +**Status:** Versuch DataTransfer zu erweitern +**Problem:** Browser-Sicherheit verhindert Files hinzufügen + +### 4. `bridge_script_hybrid.js` (HYBRID - Experimentell) +**Status:** Versuch parallele Drags +**Problem:** Nur ein Drag zur Zeit möglich + +### 5. `bridge_script_drop_intercept.js` (DROP - Experimentell) +**Status:** Drop-Event abfangen und manipulieren +**Problem:** DataTransfer kann nicht mit Files erstellt werden + +## 🎯 Empfohlener Workflow + +### Phase 1: Debugging (JETZT) + +1. **Debug-Script aktivieren:** + ```python + # main_window.py, _install_bridge_script() + script_path = Path(__file__).parent / "bridge_script_debug.js" + ``` + +2. **Anwendung starten und testen:** + ```powershell + python -m webdrop_bridge.main + ``` + +3. **In Browser (F12 Console):** + - ALT-Drag+Drop durchführen + - Logs kopieren und analysieren + - Nach `[MODAL OPENED]` oder API-Calls suchen + +4. **Herausfinden:** + - ✅ Wie wird Popup ausgelöst? (API-Call, Event, Component-Methode) + - ✅ Welche Asset-ID wird verwendet? + - ✅ Wo im DOM befindet sich die Popup-Logik? + +### Phase 2: Implementation (DANACH) + +Basierend auf Debug-Ergebnissen: + +**Fall A: Popup wird durch API-Call ausgelöst** +```javascript +// In bridge_script.js nach erfolgreichem Drop: +fetch('/api/assets/' + assetId + '/checkout', { + method: 'POST', + headers: {'Content-Type': 'application/json'} +}).then(response => { + console.log('Checkout popup triggered'); +}); +``` + +**Fall B: Popup wird durch Angular-Event ausgelöst** +```javascript +// Trigger Angular CDK Event +var dropList = document.querySelector('[cdkdroplist]'); +if (dropList && window.ng) { + var component = ng.getComponent(dropList); + component.onDrop({assetId: assetId}); +} +``` + +**Fall C: Popup wird durch Component-Methode ausgelöst** +```javascript +// Direkter Methoden-Aufruf +var assetCard = document.getElementById(assetId); +if (assetCard && window.ng) { + var component = ng.getComponent(assetCard); + component.showCheckoutDialog(); +} +``` + +### Phase 3: Testing (FINAL) + +1. Implementation in `bridge_script.js` integrieren +2. Beide Funktionen testen: + - ✅ File wird gedroppt (Z:\ Laufwerk) + - ✅ Popup erscheint (Auschecken-Dialog) + +## 🔧 Script-Wechsel in Code + +```python +# src/webdrop_bridge/ui/main_window.py +# Zeile ~433 in _install_bridge_script() + +# ORIGINAL (funktioniert, aber kein Popup): +script_path = Path(__file__).parent / "bridge_script.js" + +# DEBUG (für Analyse): +script_path = Path(__file__).parent / "bridge_script_debug.js" + +# Oder via Konfiguration: +script_name = self.config.bridge_script or "bridge_script.js" +script_path = Path(__file__).parent / script_name +``` + +## 📝 Debug-Checkliste + +Beim Testen mit Debug-Script, notieren Sie: + +- [ ] Wann erscheint das Popup? (Nach Drop, nach Verzögerung, sofort?) +- [ ] Gibt es API-Calls? (Welche URL, Parameter, Zeitpunkt?) +- [ ] Welche Angular-Events feuern? (CDK Events, Custom Events?) +- [ ] Wo wird Modal/Dialog erstellt? (DOM-Position, Klassen, Component) +- [ ] Welche Asset-Informationen werden benötigt? (ID, Name, URL?) +- [ ] Stack-Trace beim Modal-Öffnen? (console.trace Ausgabe) + +## 🚀 Quick Commands + +```powershell +# App mit Debug-Script starten +python -m webdrop_bridge.main + +# In Browser Console (F12): +webdrop_debug.getEventCounts() # Event-Statistiken +webdrop_debug.resetCounters() # Zähler zurücksetzen +webdrop_debug.getListeners(element) # Event-Listener auflisten +webdrop_debug.getComponent(element) # Angular Component anzeigen + +# Logs filtern: +# Console Filter: "[MODAL]" → Nur Popup-Logs +# Console Filter: "[EVENT]" → Nur Event-Logs +# Console Filter: "[FETCH]|[XHR]" → Nur Network-Logs +``` + +## ⚠️ Bekannte Limitationen + +1. **Angular DevTools benötigt:** Für `ng.getComponent()` Installation nötig +2. **Chrome/Edge:** Einige Features funktionieren nur in Chromium-Browsern +3. **CSP:** Bei strengen Content-Security-Policies können Logs blockiert werden +4. **Performance:** Debug-Script hat deutlichen Performance-Overhead + +## 📚 Weiterführende Dokumentation + +- [ANGULAR_CDK_ANALYSIS.md](ANGULAR_CDK_ANALYSIS.md) - Angular Framework Details +- [DRAG_DROP_PROBLEM_ANALYSIS.md](DRAG_DROP_PROBLEM_ANALYSIS.md) - Problem-Analyse + Lösungen +- [ARCHITECTURE.md](ARCHITECTURE.md) - Gesamtarchitektur diff --git a/src/webdrop_bridge/ui/bridge_script.js b/src/webdrop_bridge/ui/bridge_script.js index 622a0e4..ad8d4ce 100644 --- a/src/webdrop_bridge/ui/bridge_script.js +++ b/src/webdrop_bridge/ui/bridge_script.js @@ -141,26 +141,71 @@ console.log('[WebDrop Bridge] isZDrive:', isZDrive, 'isAzureUrl:', isAzureUrl); if (isZDrive || isAzureUrl) { - console.log('[WebDrop Bridge] >>> CONVERTING URL TO NATIVE DRAG'); + console.log('[WebDrop Bridge] >>> CONVERTING URL TO NATIVE DRAG (DELAYED)'); if (window.bridge && typeof window.bridge.debug_log === 'function') { - window.bridge.debug_log('Convertible URL detected - preventing browser drag'); + window.bridge.debug_log('Convertible URL detected - delaying Qt drag for Angular events'); } - // Prevent the browser's drag operation - e.preventDefault(); - e.stopPropagation(); + // DON'T prevent immediately - let Angular process dragstart for ~200ms + // This allows web app to register the drag and prepare popup - // Start native file drag via Qt - ensureChannel(function() { - if (window.bridge && typeof window.bridge.start_file_drag === 'function') { - console.log('[WebDrop Bridge] Calling start_file_drag:', path.substring(0, 60)); - window.bridge.start_file_drag(path); - currentDragData = null; - } else { - console.error('[WebDrop Bridge] bridge.start_file_drag not available!'); + // Store URL and element for later + window.__lastDraggedUrl = path; + var originalTarget = e.target; + + // After 200ms: Cancel browser drag and start Qt drag + setTimeout(function() { + console.log('[WebDrop Bridge] Starting Qt drag now, browser drag will be cancelled'); + + // Try to cancel browser drag by creating a fake drop on same element + try { + var dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + view: window + }); + originalTarget.dispatchEvent(dropEvent); + } catch(err) { + console.log('[WebDrop Bridge] Could not dispatch drop event:', err); } - }); + + // Hide Angular CDK overlays + var style = document.createElement('style'); + style.id = 'webdrop-bridge-hide-overlay'; + style.textContent = ` + .cdk-drag-animating, + .cdk-drag-preview, + .cdk-drag-placeholder, + [cdkdroplist].cdk-drop-list-dragging, + #root-collection-drop-area, + [id*="drop-area"] { + opacity: 0 !important; + pointer-events: none !important; + display: none !important; + } + `; + document.head.appendChild(style); + + // Start Qt drag + ensureChannel(function() { + if (window.bridge && typeof window.bridge.start_file_drag === 'function') { + console.log('[WebDrop Bridge] Calling start_file_drag:', path.substring(0, 60)); + window.bridge.start_file_drag(path); + currentDragData = null; + + // Cleanup after 5 seconds + setTimeout(function() { + var hideStyle = document.getElementById('webdrop-bridge-hide-overlay'); + if (hideStyle) hideStyle.remove(); + }, 5000); + } else { + console.error('[WebDrop Bridge] bridge.start_file_drag not available!'); + } + }); + }, 200); // 200ms delay + + // Let the browser drag start naturally (no preventDefault yet) return false; } else { @@ -203,6 +248,72 @@ }, 2000); // Wait 2 seconds after DOM ready } + // Global function to trigger checkout after successful file drop + window.trigger_checkout_for_asset = function(azure_url) { + console.log('[WebDrop Bridge] trigger_checkout_for_asset called for:', azure_url); + + // Extract asset ID from Azure URL + // Format: https://devagravitystg.file.core.windows.net/devagravitysync/{assetId}/{filename} + var match = azure_url.match(/\/devagravitysync\/([^\/]+)\//); + if (!match) { + console.error('[WebDrop Bridge] Could not extract asset ID from URL:', azure_url); + return; + } + + var assetId = match[1]; + console.log('[WebDrop Bridge] Extracted asset ID:', assetId); + console.log('[WebDrop Bridge] Calling checkout API directly...'); + + // Direct API call to checkout asset (skip popup, auto-checkout) + var apiUrl = 'https://devagravityprivate.azurewebsites.net/api/assets/' + assetId + '/checkout'; + + fetch(apiUrl, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + // Empty body or add checkout parameters if needed + }) + }).then(function(response) { + if (response.ok) { + console.log('[WebDrop Bridge] ✅ Asset checked out successfully:', assetId); + return response.json(); + } else { + console.warn('[WebDrop Bridge] ⚠️ Checkout API returned status:', response.status); + // Try alternative: Mark as checked out without confirmation + return tryAlternativeCheckout(assetId); + } + }).then(function(data) { + console.log('[WebDrop Bridge] Checkout response:', data); + }).catch(function(err) { + console.error('[WebDrop Bridge] ❌ Checkout API error:', err); + // Fallback: Try alternative checkout method + tryAlternativeCheckout(assetId); + }); + }; + + // Alternative checkout method if direct API fails + function tryAlternativeCheckout(assetId) { + console.log('[WebDrop Bridge] Trying alternative checkout for:', assetId); + + // Option 1: Try GET to fetch asset status, might trigger checkout tracking + var statusUrl = 'https://devagravityprivate.azurewebsites.net/api/assets/' + assetId; + + return fetch(statusUrl, { + method: 'GET', + credentials: 'include' + }).then(function(response) { + if (response.ok) { + console.log('[WebDrop Bridge] Asset status fetched, might have logged usage'); + } + return response.json(); + }).catch(function(err) { + console.error('[WebDrop Bridge] Alternative checkout also failed:', err); + }); + } + // Install after DOM is ready if (document.readyState === 'loading') { console.log('[WebDrop Bridge] Waiting for DOMContentLoaded...'); diff --git a/src/webdrop_bridge/ui/bridge_script_debug.js b/src/webdrop_bridge/ui/bridge_script_debug.js new file mode 100644 index 0000000..3fe16a1 --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_debug.js @@ -0,0 +1,333 @@ +// WebDrop Bridge - DEBUG Version +// Heavy logging to understand web app's drag&drop behavior +// +// Usage: +// 1. Load this script +// 2. Perform ALT-drag+drop in web app +// 3. Check console for detailed logs +// 4. Look for: API calls, events names, component methods + +(function() { + if (window.__webdrop_debug_injected) return; + window.__webdrop_debug_injected = true; + + console.log('%c[WebDrop DEBUG] Script loaded', 'background: #222; color: #bada55; font-size: 14px; font-weight: bold;'); + + // ============================================================================ + // PART 1: Event Monitoring - see ALL drag/drop related events + // ============================================================================ + + var allEvents = ['dragstart', 'drag', 'dragenter', 'dragover', 'dragleave', 'drop', 'dragend']; + var eventCounts = {}; + + allEvents.forEach(function(eventName) { + eventCounts[eventName] = 0; + + document.addEventListener(eventName, function(e) { + eventCounts[eventName]++; + + // Only log dragstart, drop, dragend fully (others are noisy) + if (eventName === 'dragstart' || eventName === 'drop' || eventName === 'dragend') { + console.group('%c[EVENT] ' + eventName.toUpperCase(), 'color: #FF6B6B; font-weight: bold;'); + console.log('Target:', e.target.tagName, e.target.className, e.target.id); + console.log('DataTransfer:', { + types: Array.from(e.dataTransfer.types), + effectAllowed: e.dataTransfer.effectAllowed, + dropEffect: e.dataTransfer.dropEffect, + files: e.dataTransfer.files.length + }); + console.log('Keys:', { + alt: e.altKey, + ctrl: e.ctrlKey, + shift: e.shiftKey + }); + console.log('Position:', {x: e.clientX, y: e.clientY}); + + // Try to read data (only works in drop/dragstart) + if (eventName === 'drop' || eventName === 'dragstart') { + try { + var plainText = e.dataTransfer.getData('text/plain'); + var uriList = e.dataTransfer.getData('text/uri-list'); + console.log('Data:', { + 'text/plain': plainText ? plainText.substring(0, 100) : null, + 'text/uri-list': uriList ? uriList.substring(0, 100) : null + }); + } catch(err) { + console.warn('Could not read DataTransfer data:', err.message); + } + } + + console.groupEnd(); + } + }, true); // Capture phase + }); + + // Log event summary every 5 seconds + setInterval(function() { + var hasEvents = Object.keys(eventCounts).some(function(k) { return eventCounts[k] > 0; }); + if (hasEvents) { + console.log('%c[EVENT SUMMARY]', 'color: #4ECDC4; font-weight: bold;', eventCounts); + } + }, 5000); + + // ============================================================================ + // PART 2: DataTransfer.setData Interception + // ============================================================================ + + try { + var originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + console.log('%c[DataTransfer.setData]', 'color: #FFE66D; font-weight: bold;', format, '=', + typeof data === 'string' ? data.substring(0, 100) : data); + return originalSetData.call(this, format, data); + }; + + console.log('[WebDrop DEBUG] DataTransfer.setData patched ✓'); + } catch(e) { + console.error('[WebDrop DEBUG] Failed to patch DataTransfer:', e); + } + + // ============================================================================ + // PART 3: Network Monitor - detect API calls (with request bodies) + // ============================================================================ + + var originalFetch = window.fetch; + window.fetch = function() { + var url = arguments[0]; + var options = arguments[1] || {}; + + console.log('%c🌐 Fetch called:', 'color: #95E1D3; font-weight: bold;', url); + + // Log headers if present + if (options.headers) { + console.log('%c[FETCH HEADERS]', 'color: #FFB6C1; font-weight: bold;'); + console.log(JSON.stringify(options.headers, null, 2)); + } + + // Log request body if present + if (options.body) { + try { + var bodyPreview = typeof options.body === 'string' ? options.body : JSON.stringify(options.body); + if (bodyPreview.length > 200) { + bodyPreview = bodyPreview.substring(0, 200) + '... (truncated)'; + } + console.log('%c[FETCH BODY]', 'color: #FFE66D; font-weight: bold;', bodyPreview); + } catch(e) { + console.log('%c[FETCH BODY]', 'color: #FFE66D; font-weight: bold;', '[Could not stringify]'); + } + } + + return originalFetch.apply(this, arguments); + }; + + var originalXHROpen = XMLHttpRequest.prototype.open; + var originalXHRSend = XMLHttpRequest.prototype.send; + var originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; + + XMLHttpRequest.prototype.open = function(method, url) { + this._webdrop_method = method; + this._webdrop_url = url; + this._webdrop_headers = {}; + console.log('%c[XHR]', 'color: #95E1D3; font-weight: bold;', method, url); + return originalXHROpen.apply(this, arguments); + }; + + XMLHttpRequest.prototype.setRequestHeader = function(header, value) { + this._webdrop_headers = this._webdrop_headers || {}; + this._webdrop_headers[header] = value; + return originalXHRSetRequestHeader.apply(this, arguments); + }; + + XMLHttpRequest.prototype.send = function(body) { + // Log headers if present + if (this._webdrop_headers && Object.keys(this._webdrop_headers).length > 0) { + if (this._webdrop_url && this._webdrop_url.includes('checkout')) { + console.log('%c[XHR HEADERS - CHECKOUT]', 'background: #FF6B6B; color: white; font-weight: bold; padding: 2px 6px;'); + console.log(JSON.stringify(this._webdrop_headers, null, 2)); + } else { + console.log('%c[XHR HEADERS]', 'color: #FFB6C1; font-weight: bold;'); + console.log(JSON.stringify(this._webdrop_headers, null, 2)); + } + } + + // Log request body if present + if (body) { + try { + var bodyPreview = typeof body === 'string' ? body : JSON.stringify(body); + if (bodyPreview.length > 200) { + bodyPreview = bodyPreview.substring(0, 200) + '... (truncated)'; + } + + // Highlight checkout API calls + if (this._webdrop_url && this._webdrop_url.includes('checkout')) { + console.log('%c[XHR BODY - CHECKOUT]', 'background: #FF6B6B; color: white; font-weight: bold; padding: 2px 6px;', + this._webdrop_method, this._webdrop_url); + console.log('%c[CHECKOUT PAYLOAD]', 'color: #00FF00; font-weight: bold;', bodyPreview); + } else { + console.log('%c[XHR BODY]', 'color: #FFE66D; font-weight: bold;', bodyPreview); + } + } catch(e) { + console.log('%c[XHR BODY]', 'color: #FFE66D; font-weight: bold;', '[Could not stringify]'); + } + } + + return originalXHRSend.apply(this, arguments); + }; + + console.log('[WebDrop DEBUG] Network interceptors installed ✓ (with request body logging)'); + + // ============================================================================ + // PART 4: Angular Event Detection (if Angular is used) + // ============================================================================ + + setTimeout(function() { + // Try to detect Angular + if (window.ng) { + console.log('%c[ANGULAR DETECTED]', 'color: #DD2C00; font-weight: bold;', 'Version:', window.ng.version?.full); + + // Try to find Angular components + var cards = document.querySelectorAll('ay-asset-card'); + console.log('[ANGULAR] Found', cards.length, 'asset cards'); + + if (cards.length > 0 && window.ng.getComponent) { + try { + var component = window.ng.getComponent(cards[0]); + console.log('%c[ANGULAR COMPONENT]', 'color: #DD2C00; font-weight: bold;', component); + console.log('Methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(component))); + } catch(e) { + console.warn('[ANGULAR] Could not get component:', e); + } + } + } else { + console.log('[WebDrop DEBUG] Angular not detected or DevTools required'); + } + + // Try to detect CDK + if (document.querySelector('[cdkdrag]')) { + console.log('%c[ANGULAR CDK] Detected', 'color: #FF6F00; font-weight: bold;'); + + // Monitor CDK specific events (if we can access them) + // Note: CDK events are often internal, we might need to monkey-patch + console.log('[CDK] Drag elements found:', document.querySelectorAll('[cdkdrag]').length); + console.log('[CDK] Drop lists found:', document.querySelectorAll('[cdkdroplist]').length); + } + }, 2000); + + // ============================================================================ + // PART 5: Modal/Dialog Detection + // ============================================================================ + + var modalObserver = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + mutation.addedNodes.forEach(function(node) { + if (node.nodeType === 1) { // Element node + // Check for common modal/dialog patterns + // Safely get className as string (handles SVG elements and undefined) + var className = typeof node.className === 'string' + ? node.className + : (node.className && node.className.baseVal) ? node.className.baseVal : ''; + + var isModal = className && ( + className.includes('modal') || + className.includes('dialog') || + className.includes('popup') || + className.includes('overlay') + ); + + if (isModal) { + console.log('%c[MODAL OPENED]', 'color: #FF6B6B; font-size: 16px; font-weight: bold;'); + console.log('Modal element:', node); + console.log('Classes:', className); + console.log('Content:', node.textContent.substring(0, 200)); + + // Log the stack trace to see what triggered it + console.trace('Modal opened from:'); + } + } + }); + }); + }); + + // Start observer when body is ready + function startModalObserver() { + if (document.body) { + modalObserver.observe(document.body, { + childList: true, + subtree: true + }); + console.log('[WebDrop DEBUG] Modal observer installed ✓'); + } else { + console.warn('[WebDrop DEBUG] document.body not ready, modal observer skipped'); + } + } + + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', startModalObserver); + } else { + startModalObserver(); + } + + // ============================================================================ + // PART 6: Helper Functions for Manual Testing + // ============================================================================ + + window.webdrop_debug = { + // Get all event listeners on an element + getListeners: function(element) { + if (typeof getEventListeners === 'function') { + return getEventListeners(element || document); + } else { + console.warn('getEventListeners not available. Open Chrome DevTools Console.'); + return null; + } + }, + + // Find Angular component for an element + getComponent: function(element) { + if (window.ng && window.ng.getComponent) { + return window.ng.getComponent(element); + } else { + console.warn('Angular DevTools not available. Install Angular DevTools extension.'); + return null; + } + }, + + // Simulate a drop at coordinates + simulateDrop: function(x, y, data) { + var dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + clientX: x, + clientY: y + }); + + // Can't set dataTransfer, but element can be used for testing + var target = document.elementFromPoint(x, y); + target.dispatchEvent(dropEvent); + console.log('Simulated drop at', x, y, 'on', target); + }, + + // Get event counts + getEventCounts: function() { + return eventCounts; + }, + + // Reset counters + resetCounters: function() { + Object.keys(eventCounts).forEach(function(k) { eventCounts[k] = 0; }); + console.log('Counters reset'); + } + }; + + console.log('%c[WebDrop DEBUG] Helper functions available as window.webdrop_debug', 'color: #95E1D3; font-weight: bold;'); + console.log('Try: webdrop_debug.getListeners(), webdrop_debug.getComponent(element)'); + + // ============================================================================ + // READY + // ============================================================================ + + console.log('%c[WebDrop DEBUG] Ready! Perform ALT-drag+drop and watch the logs.', + 'background: #222; color: #00FF00; font-size: 14px; font-weight: bold; padding: 4px;'); +})(); diff --git a/src/webdrop_bridge/ui/bridge_script_drop_intercept.js b/src/webdrop_bridge/ui/bridge_script_drop_intercept.js new file mode 100644 index 0000000..8b49201 --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_drop_intercept.js @@ -0,0 +1,211 @@ +// WebDrop Bridge - Drop Event Interception Strategy +// Strategy: +// 1. Let browser drag proceed normally (web app sets URL in DataTransfer) +// 2. Intercept DROP event in capture phase +// 3. Convert URL to local file path +// 4. Synthesize new DROP event with file data +// 5. Dispatch synthetic event (web app receives it and shows popup) +// 6. File gets dropped correctly + +(function() { + if (window.__webdrop_bridge_drop_intercept) return; + window.__webdrop_bridge_drop_intercept = true; + + console.log('[WebDrop Bridge DROP] Script loaded - Drop interception strategy'); + + var dragState = { + url: null, + localPath: null, + isConvertible: false, + altKeyPressed: false, + dragElement: null + }; + + // Patch DataTransfer.setData to capture URLs during drag + try { + var originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + if (format === 'text/plain' || format === 'text/uri-list') { + console.log('[WebDrop Bridge DROP] Captured data:', format, '=', data.substring(0, 80)); + + if (dragState.altKeyPressed) { + dragState.url = data; + + // Check if convertible + dragState.isConvertible = /^z:/i.test(data) || + /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(data); + + if (dragState.isConvertible) { + console.log('[WebDrop Bridge DROP] >>> CONVERTIBLE URL - will intercept drop'); + + // Request conversion NOW (synchronously if possible) + ensureChannel(function() { + if (window.bridge && typeof window.bridge.convert_url_sync === 'function') { + // Synchronous conversion + dragState.localPath = window.bridge.convert_url_sync(data); + console.log('[WebDrop Bridge DROP] Converted to:', dragState.localPath); + } else if (window.bridge && typeof window.bridge.convert_url_to_path === 'function') { + // Async conversion (fallback) + window.bridge.convert_url_to_path(data); + console.log('[WebDrop Bridge DROP] Async conversion requested'); + } + }); + } + } + } + + // Always call original + return originalSetData.call(this, format, data); + }; + + console.log('[WebDrop Bridge DROP] DataTransfer patched'); + } catch(e) { + console.error('[WebDrop Bridge DROP] Patch failed:', e); + } + + // Callback for async conversion + window.webdrop_set_local_path = function(path) { + dragState.localPath = path; + console.log('[WebDrop Bridge DROP] Local path received:', path); + }; + + function ensureChannel(cb) { + if (window.bridge) { cb(); return; } + + if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { + new QWebChannel(window.qt.webChannelTransport, function(channel) { + window.bridge = channel.objects.bridge; + console.log('[WebDrop Bridge DROP] QWebChannel connected'); + cb(); + }); + } + } + + function createSyntheticDropEvent(originalEvent, localPath) { + console.log('[WebDrop Bridge DROP] Creating synthetic drop event with file:', localPath); + + try { + // Create a new DataTransfer with file + // NOTE: This is complex because DataTransfer can't be created directly + // We'll create a new DragEvent and try to set files + + var newEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + composed: true, + view: window, + detail: originalEvent.detail, + screenX: originalEvent.screenX, + screenY: originalEvent.screenY, + clientX: originalEvent.clientX, + clientY: originalEvent.clientY, + ctrlKey: originalEvent.ctrlKey, + altKey: originalEvent.altKey, + shiftKey: originalEvent.shiftKey, + metaKey: originalEvent.metaKey, + button: originalEvent.button, + buttons: originalEvent.buttons, + relatedTarget: originalEvent.relatedTarget, + // We can't directly set dataTransfer, it's read-only + }); + + // This is a limitation: We can't create a DataTransfer with files from JavaScript + // The only way is to use a real file input or drag a real file + + console.warn('[WebDrop Bridge DROP] Cannot create DataTransfer with files from JS'); + console.log('[WebDrop Bridge DROP] Will use workaround: modify original DataTransfer'); + + return null; // Cannot create synthetic event with files + + } catch(error) { + console.error('[WebDrop Bridge DROP] Synthetic event creation failed:', error); + return null; + } + } + + function installHooks() { + console.log('[WebDrop Bridge DROP] Installing hooks'); + + // Monitor dragstart + document.addEventListener('dragstart', function(e) { + dragState.altKeyPressed = e.altKey; + dragState.url = null; + dragState.localPath = null; + dragState.isConvertible = false; + dragState.dragElement = e.target; + + console.log('[WebDrop Bridge DROP] dragstart, altKey:', e.altKey); + + // Let it proceed + }, true); + + // Intercept DROP event + document.addEventListener('drop', function(e) { + console.log('[WebDrop Bridge DROP] drop event, isConvertible:', dragState.isConvertible); + + if (!dragState.isConvertible || !dragState.localPath) { + console.log('[WebDrop Bridge DROP] Not convertible or no path, letting through'); + return; // Let normal drop proceed + } + + console.log('[WebDrop Bridge DROP] >>> INTERCEPTING DROP for conversion'); + + // This is the problem: We can't modify the DataTransfer at this point + // And we can't create a new one with files from JavaScript + + // WORKAROUND: Tell Qt to handle the drop natively + e.preventDefault(); // Prevent browser handling + e.stopPropagation(); + + // Get drop coordinates + var dropX = e.clientX; + var dropY = e.clientY; + + console.log('[WebDrop Bridge DROP] Drop at:', dropX, dropY); + + // Tell Qt to perform native file drop at these coordinates + ensureChannel(function() { + if (window.bridge && typeof window.bridge.handle_native_drop === 'function') { + window.bridge.handle_native_drop(dragState.localPath, dropX, dropY); + } + }); + + // THEN manually trigger the web app's drop handler + // This is tricky and app-specific + // For Angular CDK, we might need to trigger cdkDropListDropped + + console.warn('[WebDrop Bridge DROP] Web app popup might not appear - investigating...'); + + return false; + + }, true); // Capture phase + + // Clean up + document.addEventListener('dragend', function(e) { + console.log('[WebDrop Bridge DROP] dragend, cleaning up'); + dragState = { + url: null, + localPath: null, + isConvertible: false, + altKeyPressed: false, + dragElement: null + }; + }, false); + + console.log('[WebDrop Bridge DROP] Hooks installed'); + } + + // Initialize + ensureChannel(function() { + installHooks(); + }); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + if (!window.bridge) ensureChannel(installHooks); + }); + } else if (!window.bridge) { + ensureChannel(installHooks); + } +})(); diff --git a/src/webdrop_bridge/ui/bridge_script_hybrid.js b/src/webdrop_bridge/ui/bridge_script_hybrid.js new file mode 100644 index 0000000..9727648 --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_hybrid.js @@ -0,0 +1,109 @@ +// WebDrop Bridge - Hybrid Strategy v3 +// Allow web-app drag to proceed normally (for popups) +// BUT notify Qt to start a PARALLEL native file drag +// Windows supports concurrent drag sources - drop target chooses which to use + +(function() { + if (window.__webdrop_bridge_hybrid_injected) return; + window.__webdrop_bridge_hybrid_injected = true; + + console.log('[WebDrop Bridge HYBRID] Script loaded'); + + var dragState = { + url: null, + inProgress: false, + altKeyPressed: false + }; + + // Patch DataTransfer.setData to capture URLs + try { + var originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + if ((format === 'text/plain' || format === 'text/uri-list') && dragState.inProgress) { + dragState.url = data; + console.log('[WebDrop Bridge HYBRID] Captured URL:', data.substring(0, 80)); + + // Check if convertible + var isConvertible = /^z:/i.test(data) || + /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(data); + + if (isConvertible && dragState.altKeyPressed) { + console.log('[WebDrop Bridge HYBRID] >>> CONVERTIBLE - Triggering Qt native drag'); + + // Notify Qt to start PARALLEL native drag + ensureChannel(function() { + if (window.bridge && typeof window.bridge.start_parallel_drag === 'function') { + console.log('[WebDrop Bridge HYBRID] Calling start_parallel_drag'); + window.bridge.start_parallel_drag(data); + } else if (window.bridge && typeof window.bridge.start_file_drag === 'function') { + // Fallback to old method + console.log('[WebDrop Bridge HYBRID] Using start_file_drag (fallback)'); + window.bridge.start_file_drag(data); + } + }); + } + } + + // ALWAYS call original - web app functionality must work + return originalSetData.call(this, format, data); + }; + + console.log('[WebDrop Bridge HYBRID] DataTransfer patched'); + } catch(e) { + console.error('[WebDrop Bridge HYBRID] Patch failed:', e); + } + + function ensureChannel(cb) { + if (window.bridge) { cb(); return; } + + if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { + new QWebChannel(window.qt.webChannelTransport, function(channel) { + window.bridge = channel.objects.bridge; + console.log('[WebDrop Bridge HYBRID] QWebChannel connected'); + cb(); + }); + } else { + console.error('[WebDrop Bridge HYBRID] QWebChannel not available'); + } + } + + function installHook() { + console.log('[WebDrop Bridge HYBRID] Installing hooks'); + + // Monitor dragstart + document.addEventListener('dragstart', function(e) { + dragState.inProgress = true; + dragState.altKeyPressed = e.altKey; + dragState.url = null; + + console.log('[WebDrop Bridge HYBRID] dragstart, altKey:', e.altKey); + + // NO preventDefault() - let web app proceed normally! + + }, true); // Capture phase + + // Clean up on dragend + document.addEventListener('dragend', function(e) { + console.log('[WebDrop Bridge HYBRID] dragend'); + dragState.inProgress = false; + dragState.url = null; + dragState.altKeyPressed = false; + }, false); + + console.log('[WebDrop Bridge HYBRID] Installed'); + } + + // Initialize + ensureChannel(function() { + installHook(); + }); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + if (!window.bridge) ensureChannel(installHook); + }); + } else if (!window.bridge) { + ensureChannel(installHook); + } +})(); diff --git a/src/webdrop_bridge/ui/bridge_script_intercept.js b/src/webdrop_bridge/ui/bridge_script_intercept.js new file mode 100644 index 0000000..d249a83 --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_intercept.js @@ -0,0 +1,165 @@ +// WebDrop Bridge - Intercept Version +// Prevents browser drag for ALT+drag, hands off to Qt for file drag + +(function() { + if (window.__webdrop_intercept_injected) return; + window.__webdrop_intercept_injected = true; + + // Intercept mode enabled + var INTERCEPT_ENABLED = true; + + console.log('%c[WebDrop Intercept] Script loaded - INTERCEPT_ENABLED=' + INTERCEPT_ENABLED, 'background: #2196F3; color: white; font-weight: bold; padding: 4px 8px;'); + + var currentDragUrl = null; + var angularDragHandlers = []; + var originalAddEventListener = EventTarget.prototype.addEventListener; + var listenerPatchActive = true; + + // Capture Authorization token from XHR requests + window.capturedAuthToken = null; + var originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; + XMLHttpRequest.prototype.setRequestHeader = function(header, value) { + if (header === 'Authorization' && value.startsWith('Bearer ')) { + window.capturedAuthToken = value; + console.log('[Intercept] Captured auth token'); + } + return originalXHRSetRequestHeader.apply(this, arguments); + }; + + // ============================================================================ + // PART 1: Intercept Angular's dragstart listener registration + // ============================================================================ + + EventTarget.prototype.addEventListener = function(type, listener, options) { + if (listenerPatchActive && type === 'dragstart' && listener) { + // Store Angular's dragstart handler instead of registering it + console.log('[Intercept] Storing Angular dragstart listener for', this.tagName || this.constructor.name); + angularDragHandlers.push({ + target: this, + listener: listener, + options: options + }); + return; // Don't actually register it yet + } + // All other events: use original + return originalAddEventListener.call(this, type, listener, options); + }; + + // ============================================================================ + // PART 2: Intercept DataTransfer.setData to capture URL + // ============================================================================ + + var originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + if (format === 'text/plain' || format === 'text/uri-list') { + currentDragUrl = data; + console.log('%c[Intercept] Captured URL:', 'color: #4CAF50; font-weight: bold;', data.substring(0, 80)); + } + return originalSetData.call(this, format, data); + }; + + console.log('[Intercept] DataTransfer.setData patched ✓'); + + // ============================================================================ + // PART 3: Install OUR dragstart handler in capture phase + // ============================================================================ + + setTimeout(function() { + console.log('[Intercept] Installing dragstart handler, have', angularDragHandlers.length, 'Angular handlers'); + + // Stop intercepting addEventListener + listenerPatchActive = false; + + // Register OUR handler in capture phase + originalAddEventListener.call(document, 'dragstart', function(e) { + currentDragUrl = null; // Reset + + console.log('%c[Intercept] dragstart', 'background: #FF9800; color: white; padding: 2px 6px;', 'ALT:', e.altKey); + + // Call Angular's handlers first to let them set the data + var handled = 0; + for (var i = 0; i < angularDragHandlers.length; i++) { + var h = angularDragHandlers[i]; + if (h.target === document || h.target === e.target || + (h.target.contains && h.target.contains(e.target))) {https://devagravitystg.file.core.windows.net/devagravitysync/anPGZszKzgKaSz1SIx2HFgduy/weiss_ORIGINAL.jpg + try { + h.listener.call(e.target, e); + handled++; + } catch(err) { + console.error('[Intercept] Error calling Angular handler:', err); + } + } + } + + console.log('[Intercept] Called', handled, 'Angular handlers, URL:', currentDragUrl ? currentDragUrl.substring(0, 60) : 'none'); + + // NOW check if we should intercept + if (e.altKey && currentDragUrl) { + var isAzure = /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(currentDragUrl); + var isZDrive = /^z:/i.test(currentDragUrl); + + if (isAzure || isZDrive) { + console.log('%c[Intercept] PREVENTING browser drag, using Qt', + 'background: #F44336; color: white; font-weight: bold; padding: 4px 8px;'); + + e.preventDefault(); + e.stopPropagation(); + + ensureChannel(function() { + if (window.bridge && typeof window.bridge.start_file_drag === 'function') { + console.log('%c[Intercept] → Qt: start_file_drag', 'color: #9C27B0; font-weight: bold;'); + window.bridge.start_file_drag(currentDragUrl); + } else { + console.error('[Intercept] bridge.start_file_drag not available!'); + } + }); + + currentDragUrl = null; + return false; + } + } + + console.log('[Intercept] Normal drag, allowing browser'); + }, true); // Capture phase + + console.log('[Intercept] dragstart handler installed ✓'); + }, 1500); // Wait for Angular to register its listeners + + // ============================================================================ + // PART 3: QWebChannel connection + // ============================================================================ + + function ensureChannel(callback) { + if (window.bridge) { + callback(); + return; + } + + if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { + new QWebChannel(window.qt.webChannelTransport, function(channel) { + window.bridge = channel.objects.bridge; + console.log('[WebDrop Intercept] QWebChannel connected ✓'); + callback(); + }); + } else { + console.error('[WebDrop Intercept] QWebChannel not available!'); + } + } + + // Initialize channel on load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + ensureChannel(function() { + console.log('[WebDrop Intercept] Bridge ready ✓'); + }); + }); + } else { + ensureChannel(function() { + console.log('[WebDrop Intercept] Bridge ready ✓'); + }); + } + + console.log('%c[WebDrop Intercept] Ready! ALT-drag will use Qt file drag.', + 'background: #4CAF50; color: white; font-weight: bold; padding: 4px 8px;'); +})(); diff --git a/src/webdrop_bridge/ui/bridge_script_v2.js b/src/webdrop_bridge/ui/bridge_script_v2.js new file mode 100644 index 0000000..33ceccd --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_v2.js @@ -0,0 +1,214 @@ +// WebDrop Bridge - Enhanced Script v2 +// Strategy: EXTEND DataTransfer instead of REPLACING the drag +// This allows both file-drop AND web-app functionality (popups) + +(function() { + if (window.__webdrop_bridge_v2_injected) return; + window.__webdrop_bridge_v2_injected = true; + + console.log('[WebDrop Bridge v2] Script loaded - EXTEND strategy'); + + var currentDragUrl = null; + var currentLocalPath = null; + var dragInProgress = false; + + // Patch DataTransfer.setData to capture URLs set by web app + var originalSetData = null; + + try { + if (DataTransfer.prototype.setData) { + originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + // Capture text/plain or text/uri-list for our conversion + if ((format === 'text/plain' || format === 'text/uri-list') && dragInProgress) { + currentDragUrl = data; + console.log('[WebDrop Bridge v2] Captured drag URL:', data.substring(0, 80)); + + // Check if this is convertible (Z:\ or Azure) + var isZDrive = /^z:/i.test(data); + var isAzureUrl = /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(data); + + if (isZDrive || isAzureUrl) { + console.log('[WebDrop Bridge v2] >>> CONVERTIBLE URL DETECTED - Will add file data'); + + // Request conversion from Qt backend + if (window.bridge && typeof window.bridge.convert_url_to_path === 'function') { + console.log('[WebDrop Bridge v2] Requesting path conversion...'); + window.bridge.convert_url_to_path(data); + // Note: Conversion happens async, local path will be set via callback + } + } + } + + // ALWAYS call original - don't break web app + return originalSetData.call(this, format, data); + }; + + console.log('[WebDrop Bridge v2] DataTransfer.setData patched'); + } + } catch(e) { + console.error('[WebDrop Bridge v2] Failed to patch DataTransfer:', e); + } + + // Enhanced DataTransfer with file support + // This is called AFTER web app sets its data + function enhanceDataTransfer(e) { + if (!currentLocalPath) { + console.log('[WebDrop Bridge v2] No local path available, cannot enhance'); + return; + } + + console.log('[WebDrop Bridge v2] Enhancing DataTransfer with file:', currentLocalPath); + + try { + var dt = e.dataTransfer; + + // Strategy 1: Add Windows-specific file drop formats + // These are recognized by Windows Explorer and other drop targets + if (originalSetData) { + // Add FileNameW (Unicode file path for Windows) + var fileNameW = currentLocalPath; + + // Try to add custom Windows file drop formats + try { + originalSetData.call(dt, 'application/x-qt-windows-mime;value="FileNameW"', fileNameW); + console.log('[WebDrop Bridge v2] Added FileNameW format'); + } catch (e1) { + console.warn('[WebDrop Bridge v2] FileNameW format failed:', e1); + } + + // Add FileName (ANSI) + try { + originalSetData.call(dt, 'application/x-qt-windows-mime;value="FileName"', fileNameW); + console.log('[WebDrop Bridge v2] Added FileName format'); + } catch (e2) { + console.warn('[WebDrop Bridge v2] FileName format failed:', e2); + } + + // Add FileDrop format + try { + originalSetData.call(dt, 'application/x-qt-windows-mime;value="FileDrop"', fileNameW); + console.log('[WebDrop Bridge v2] Added FileDrop format'); + } catch (e3) { + console.warn('[WebDrop Bridge v2] FileDrop format failed:', e3); + } + } + + // Set effect to allow copy/link + if (dt.effectAllowed === 'uninitialized' || dt.effectAllowed === 'none') { + dt.effectAllowed = 'copyLink'; + console.log('[WebDrop Bridge v2] Set effectAllowed to copyLink'); + } + + } catch (error) { + console.error('[WebDrop Bridge v2] Error enhancing DataTransfer:', error); + } + } + + function ensureChannel(cb) { + if (window.bridge) { cb(); return; } + + function init() { + if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { + new QWebChannel(window.qt.webChannelTransport, function(channel) { + window.bridge = channel.objects.bridge; + console.log('[WebDrop Bridge v2] QWebChannel connected'); + + // Expose callback for Qt to set the converted path + window.setLocalPath = function(path) { + currentLocalPath = path; + console.log('[WebDrop Bridge v2] Local path set from Qt:', path); + }; + + cb(); + }); + } else { + console.error('[WebDrop Bridge v2] QWebChannel not available!'); + } + } + + if (window.QWebChannel) { + init(); + } else { + console.error('[WebDrop Bridge v2] QWebChannel not found!'); + } + } + + function installHook() { + console.log('[WebDrop Bridge v2] Installing drag interceptor (EXTEND mode)'); + + // Use CAPTURE PHASE to intercept early + document.addEventListener('dragstart', function(e) { + try { + dragInProgress = true; + currentDragUrl = null; + currentLocalPath = null; + + console.log('[WebDrop Bridge v2] dragstart on:', e.target.tagName, 'altKey:', e.altKey); + + // Only process ALT-drags (web app's URL drag mode) + if (!e.altKey) { + console.log('[WebDrop Bridge v2] No ALT key, ignoring'); + dragInProgress = false; + return; + } + + console.log('[WebDrop Bridge v2] ALT-drag detected, will monitor for convertible URL'); + + // NOTE: We DON'T call preventDefault() here! + // This allows web app's drag to proceed normally + + // Web app will call setData() which we've patched + // After a short delay, we check if we got a convertible URL + setTimeout(function() { + if (currentDragUrl) { + console.log('[WebDrop Bridge v2] Drag URL set:', currentDragUrl.substring(0, 60)); + // enhanceDataTransfer will be called when we have the path + // For now, we just wait - the drag is already in progress + } + }, 10); + + } catch (error) { + console.error('[WebDrop Bridge v2] Error in dragstart:', error); + dragInProgress = false; + } + }, true); // CAPTURE phase + + // Clean up on dragend + document.addEventListener('dragend', function(e) { + console.log('[WebDrop Bridge v2] dragend, cleaning up state'); + dragInProgress = false; + currentDragUrl = null; + currentLocalPath = null; + }, false); + + // Alternative strategy: Use 'drag' event to continuously update DataTransfer + // This fires many times during the drag + document.addEventListener('drag', function(e) { + if (!dragInProgress || !currentLocalPath) return; + + // Try to enhance on every drag event + enhanceDataTransfer(e); + }, false); + + console.log('[WebDrop Bridge v2] Hooks installed'); + } + + // Initialize + ensureChannel(function() { + console.log('[WebDrop Bridge v2] Channel ready, installing hooks'); + installHook(); + }); + + // Also install on DOM ready as fallback + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + if (!window.bridge) { + ensureChannel(installHook); + } + }); + } else if (!window.bridge) { + ensureChannel(installHook); + } +})(); diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index 252e27b..34bb438 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -1,7 +1,9 @@ """Main application window with web engine integration.""" import asyncio +import json import logging +import re from datetime import datetime from pathlib import Path from typing import Optional @@ -426,7 +428,8 @@ class MainWindow(QMainWindow): logger.warning("Failed to load qwebchannel.js from resources") # Load bridge script from file - script_path = Path(__file__).parent / "bridge_script.js" + # Using intercept script - prevents browser drag, hands off to Qt + script_path = Path(__file__).parent / "bridge_script_intercept.js" try: with open(script_path, 'r', encoding='utf-8') as f: bridge_code = f.read() @@ -492,7 +495,119 @@ class MainWindow(QMainWindow): local_path: Local file path that is being dragged """ logger.info(f"Drag started: {source} -> {local_path}") - # Can be extended with status bar updates or user feedback + + # Ask user if they want to check out the asset + if source.startswith('http'): + self._prompt_checkout(source, local_path) + + def _prompt_checkout(self, azure_url: str, local_path: str) -> None: + """Prompt user to check out the asset. + + Args: + azure_url: Azure Blob Storage URL + local_path: Local file path + """ + from PySide6.QtWidgets import QMessageBox + + # Extract filename for display + filename = Path(local_path).name + + # Show confirmation dialog + reply = QMessageBox.question( + self, + "Checkout Asset", + f"Do you want to check out this asset?\n\n{filename}", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.Yes + ) + + if reply == QMessageBox.StandardButton.Yes: + logger.info(f"User confirmed checkout for {filename}") + self._trigger_checkout_api(azure_url) + else: + logger.info(f"User declined checkout for {filename}") + + def _trigger_checkout_api(self, azure_url: str) -> None: + """Trigger checkout via API call using JavaScript. + + Calls the checkout API from JavaScript so HttpOnly cookies are automatically included. + Example URL: https://devagravitystg.file.core.windows.net/devagravitysync/anPGZszKzgKaSz1SIx2HFgduy/filename + Asset ID: anPGZszKzgKaSz1SIx2HFgduy + + Args: + azure_url: Azure Blob Storage URL containing asset ID + """ + try: + # Extract asset ID from URL (middle segment between domain and filename) + # Format: https://domain/container/ASSET_ID/filename + match = re.search(r'/([^/]+)/[^/]+$', azure_url) + if not match: + logger.warning(f"Could not extract asset ID from URL: {azure_url}") + return + + asset_id = match.group(1) + logger.info(f"Extracted asset ID: {asset_id}") + + # Call API from JavaScript with Authorization header + js_code = f""" + (async function() {{ + try {{ + // Get captured auth token (from intercepted XHR) + const authToken = window.capturedAuthToken; + + if (!authToken) {{ + console.error('No authorization token available'); + return {{ success: false, error: 'No auth token' }}; + }} + + const headers = {{ + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Accept-Language': 'de', + 'Authorization': authToken + }}; + + const response = await fetch( + 'https://devagravityprivate.azurewebsites.net/api/assets/checkout/bulk?checkout=true', + {{ + method: 'PUT', + headers: headers, + body: JSON.stringify({{asset_ids: ['{asset_id}']}}) + }} + ); + + if (response.ok) {{ + console.log('✅ Checkout API successful for asset {asset_id}'); + return {{ success: true, status: response.status }}; + }} else {{ + const text = await response.text(); + console.warn('Checkout API returned status ' + response.status + ': ' + text.substring(0, 200)); + return {{ success: false, status: response.status, error: text }}; + }} + }} catch (error) {{ + console.error('Checkout API call failed:', error); + return {{ success: false, error: error.toString() }}; + }} + }})(); + """ + + def on_result(result): + """Callback when JavaScript completes.""" + if result and isinstance(result, dict): + if result.get('success'): + logger.info(f"✅ Checkout successful for asset {asset_id}") + else: + status = result.get('status', 'unknown') + error = result.get('error', 'unknown error') + logger.warning(f"Checkout API returned status {status}: {error}") + else: + logger.debug(f"Checkout API call completed (result: {result})") + + # Execute JavaScript (async, non-blocking) + self.web_view.page().runJavaScript(js_code, on_result) + + except Exception as e: + logger.exception(f"Error triggering checkout API: {e}") def _on_drag_failed(self, source: str, error: str) -> None: """Handle drag operation failure. @@ -605,33 +720,13 @@ class MainWindow(QMainWindow): def dragEnterEvent(self, event): """Handle drag entering the main window (from WebView or external). - When a drag from the WebView enters the MainWindow area, we can read - the drag data and potentially convert Azure URLs to file drags. + Note: With intercept script, ALT-drags are prevented in JavaScript + and handled via bridge.start_file_drag(). This just handles any + remaining drag events. Args: event: QDragEnterEvent """ - from PySide6.QtCore import QMimeData - - mime_data = event.mimeData() - - # Check if we have text data (URL from web app) - if mime_data.hasText(): - url_text = mime_data.text() - logger.debug(f"Drag entered main window with text: {url_text[:100]}") - - # Store for potential conversion - self._current_drag_url = url_text - - # Check if it's convertible - is_azure = url_text.startswith('https://') and 'file.core.windows.net' in url_text - is_z_drive = url_text.lower().startswith('z:') - - if is_azure or is_z_drive: - logger.info(f"Convertible URL detected in drag: {url_text[:60]}") - event.acceptProposedAction() - return - event.ignore() def dragMoveEvent(self, event): @@ -640,10 +735,7 @@ class MainWindow(QMainWindow): Args: event: QDragMoveEvent """ - if self._current_drag_url: - event.acceptProposedAction() - else: - event.ignore() + event.ignore() def dragLeaveEvent(self, event): """Handle drag leaving the main window. @@ -651,33 +743,15 @@ class MainWindow(QMainWindow): Args: event: QDragLeaveEvent """ - logger.debug("Drag left main window") - # Reset tracking - self._current_drag_url = None + event.ignore() def dropEvent(self, event): """Handle drop on the main window. - This captures drops on the MainWindow area (outside WebView). - If the user drops an Azure URL here, we convert it to a file operation. - Args: event: QDropEvent """ - if self._current_drag_url: - logger.info(f"Drop on main window with URL: {self._current_drag_url[:60]}") - - # Handle via drag interceptor (converts Azure URL to local path) - success = self.drag_interceptor.handle_drag(self._current_drag_url) - - if success: - event.acceptProposedAction() - else: - event.ignore() - - self._current_drag_url = None - else: - event.ignore() + event.ignore() def _on_js_console_message(self, level, message, line_number, source_id): """Redirect JavaScript console messages to Python logger. @@ -863,10 +937,33 @@ class MainWindow(QMainWindow): def closeEvent(self, event) -> None: """Handle window close event. + Properly cleanup WebEnginePage before closing to avoid + "Release of profile requested but WebEnginePage still not deleted" warning. + This ensures session data (cookies, login state) is properly saved. + Args: event: Close event """ - # Can be extended with save operations or cleanup + logger.debug("Closing application - cleaning up web engine resources") + + # Properly delete WebEnginePage before the profile is released + # This ensures cookies and session data are saved correctly + if hasattr(self, 'web_view') and self.web_view: + page = self.web_view.page() + if page: + # Disconnect signals to prevent callbacks during shutdown + try: + page.loadFinished.disconnect() + except RuntimeError: + pass # Already disconnected or never connected + + # Delete the page explicitly + page.deleteLater() + logger.debug("WebEnginePage scheduled for deletion") + + # Clear the page from the view + self.web_view.setPage(None) + event.accept() def check_for_updates_startup(self) -> None: diff --git a/src/webdrop_bridge/ui/restricted_web_view.py b/src/webdrop_bridge/ui/restricted_web_view.py index 489b473..7f20ad2 100644 --- a/src/webdrop_bridge/ui/restricted_web_view.py +++ b/src/webdrop_bridge/ui/restricted_web_view.py @@ -155,7 +155,8 @@ class RestrictedWebEngineView(QWebEngineView): # Create persistent profile with custom storage location # Using "WebDropBridge" as the profile name - profile = QWebEngineProfile("WebDropBridge", self) + # Note: No parent specified so we control the lifecycle + profile = QWebEngineProfile("WebDropBridge") profile.setPersistentStoragePath(str(profile_path)) # Configure persistent cookies (critical for authentication) From 2a9926d93485f2018279d57848245992617b5555 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 07:41:28 +0100 Subject: [PATCH 03/16] feat: Enhance checkout process by checking asset status before prompting user --- src/webdrop_bridge/ui/main_window.py | 74 +++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index 34bb438..6c3b063 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -501,7 +501,9 @@ class MainWindow(QMainWindow): self._prompt_checkout(source, local_path) def _prompt_checkout(self, azure_url: str, local_path: str) -> None: - """Prompt user to check out the asset. + """Check checkout status and prompt user if needed. + + First checks if the asset is already checked out. Only shows dialog if not checked out. Args: azure_url: Azure Blob Storage URL @@ -512,7 +514,75 @@ class MainWindow(QMainWindow): # Extract filename for display filename = Path(local_path).name - # Show confirmation dialog + # Extract asset ID + match = re.search(r'/([^/]+)/[^/]+$', azure_url) + if not match: + logger.warning(f"Could not extract asset ID from URL: {azure_url}") + return + + asset_id = match.group(1) + + # Check checkout status first + js_code = f""" + (async function() {{ + try {{ + const authToken = window.capturedAuthToken; + if (!authToken) {{ + return {{ error: 'No auth token' }}; + }} + + const response = await fetch( + 'https://devagravityprivate.azurewebsites.net/api/assets/{asset_id}?fields=checkout', + {{ + method: 'GET', + headers: {{ + 'Accept': 'application/json', + 'Authorization': authToken + }} + }} + ); + + if (response.ok) {{ + const data = await response.json(); + return {{ checkout: data.checkout }}; + }} else {{ + return {{ error: 'Failed to fetch asset' }}; + }} + }} catch (error) {{ + return {{ error: error.toString() }}; + }} + }})(); + """ + + def on_checkout_status(result): + """Callback when checkout status is received.""" + if not result or isinstance(result, dict) and result.get('error'): + logger.warning(f"Could not check checkout status: {result}") + # Show dialog anyway as fallback + self._show_checkout_dialog(azure_url, filename) + return + + # Check if already checked out + checkout_info = result.get('checkout') + if checkout_info: + logger.info(f"Asset {filename} is already checked out, skipping dialog") + return + + # Not checked out, show confirmation dialog + self._show_checkout_dialog(azure_url, filename) + + # Execute JavaScript to check status + self.web_view.page().runJavaScript(js_code, on_checkout_status) + + def _show_checkout_dialog(self, azure_url: str, filename: str) -> None: + """Show the checkout confirmation dialog. + + Args: + azure_url: Azure Blob Storage URL + filename: Asset filename + """ + from PySide6.QtWidgets import QMessageBox + reply = QMessageBox.question( self, "Checkout Asset", From e91a2445f30c44564e2799b1d4ceea30b4c9ac5c Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 08:01:33 +0100 Subject: [PATCH 04/16] feat: Implement asynchronous checkout status check with improved error handling and logging --- src/webdrop_bridge/ui/main_window.py | 99 +++++++++++++++++++++------- 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index 6c3b063..cfaeb54 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -522,15 +522,21 @@ class MainWindow(QMainWindow): asset_id = match.group(1) - # Check checkout status first + # Store callback ID for this check + callback_id = f"checkout_check_{id(self)}" + + # Check checkout status - use callback approach since Qt doesn't handle Promise returns well js_code = f""" - (async function() {{ + (async () => {{ try {{ const authToken = window.capturedAuthToken; if (!authToken) {{ - return {{ error: 'No auth token' }}; + console.log('[Checkout Check] No auth token available'); + window['{callback_id}'] = JSON.stringify({{ error: 'No auth token' }}); + return; }} + console.log('[Checkout Check] Fetching asset data for {asset_id}'); const response = await fetch( 'https://devagravityprivate.azurewebsites.net/api/assets/{asset_id}?fields=checkout', {{ @@ -542,37 +548,82 @@ class MainWindow(QMainWindow): }} ); + console.log('[Checkout Check] Response status:', response.status); + if (response.ok) {{ const data = await response.json(); - return {{ checkout: data.checkout }}; + console.log('[Checkout Check] Full data:', JSON.stringify(data)); + console.log('[Checkout Check] Checkout field:', data.checkout); + const hasCheckout = data.checkout && Object.keys(data.checkout).length > 0; + console.log('[Checkout Check] Has checkout:', hasCheckout); + window['{callback_id}'] = JSON.stringify({{ checkout: data.checkout, hasCheckout: hasCheckout }}); }} else {{ - return {{ error: 'Failed to fetch asset' }}; + console.log('[Checkout Check] Failed to fetch, status:', response.status); + window['{callback_id}'] = JSON.stringify({{ error: 'Failed to fetch asset', status: response.status }}); }} }} catch (error) {{ - return {{ error: error.toString() }}; + console.error('[Checkout Check] Error:', error); + window['{callback_id}'] = JSON.stringify({{ error: error.toString() }}); }} }})(); """ - def on_checkout_status(result): - """Callback when checkout status is received.""" - if not result or isinstance(result, dict) and result.get('error'): - logger.warning(f"Could not check checkout status: {result}") - # Show dialog anyway as fallback - self._show_checkout_dialog(azure_url, filename) - return - - # Check if already checked out - checkout_info = result.get('checkout') - if checkout_info: - logger.info(f"Asset {filename} is already checked out, skipping dialog") - return - - # Not checked out, show confirmation dialog - self._show_checkout_dialog(azure_url, filename) + # Execute the async fetch + self.web_view.page().runJavaScript(js_code) - # Execute JavaScript to check status - self.web_view.page().runJavaScript(js_code, on_checkout_status) + # After a short delay, read the result from window variable + def check_result(): + read_code = f"window['{callback_id}']" + self.web_view.page().runJavaScript(read_code, lambda result: self._handle_checkout_status(result, azure_url, filename, callback_id)) + + # Wait 500ms for async fetch to complete + from PySide6.QtCore import QTimer + QTimer.singleShot(500, check_result) + + def _handle_checkout_status(self, result, azure_url: str, filename: str, callback_id: str) -> None: + """Handle the result of checkout status check. + + Args: + result: Result from JavaScript (JSON string) + azure_url: Azure URL + filename: Asset filename + callback_id: Callback ID to clean up + """ + # Clean up window variable + cleanup_code = f"delete window['{callback_id}']" + self.web_view.page().runJavaScript(cleanup_code) + + logger.debug(f"Checkout status result type: {type(result)}, value: {result}") + + if not result or not isinstance(result, str): + logger.warning(f"Checkout status check returned invalid result: {result}") + self._show_checkout_dialog(azure_url, filename) + return + + # Parse JSON string + try: + import json + parsed_result = json.loads(result) + except (json.JSONDecodeError, ValueError) as e: + logger.warning(f"Failed to parse checkout status result: {e}") + self._show_checkout_dialog(azure_url, filename) + return + + if parsed_result.get('error'): + logger.warning(f"Could not check checkout status: {parsed_result}") + self._show_checkout_dialog(azure_url, filename) + return + + # Check if already checked out + has_checkout = parsed_result.get('hasCheckout', False) + if has_checkout: + checkout_info = parsed_result.get('checkout', {}) + logger.info(f"Asset {filename} is already checked out: {checkout_info}, skipping dialog") + return + + # Not checked out, show confirmation dialog + logger.debug(f"Asset {filename} is not checked out, showing dialog") + self._show_checkout_dialog(azure_url, filename) def _show_checkout_dialog(self, azure_url: str, filename: str) -> None: """Show the checkout confirmation dialog. From c32453018bb0ce70bc8c3bf912053ddc2cdf8859 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 08:06:11 +0100 Subject: [PATCH 05/16] feat: Improve dragstart handler installation with retry logic for Angular handlers --- src/webdrop_bridge/ui/bridge_script_intercept.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/webdrop_bridge/ui/bridge_script_intercept.js b/src/webdrop_bridge/ui/bridge_script_intercept.js index d249a83..6f627aa 100644 --- a/src/webdrop_bridge/ui/bridge_script_intercept.js +++ b/src/webdrop_bridge/ui/bridge_script_intercept.js @@ -65,9 +65,15 @@ // PART 3: Install OUR dragstart handler in capture phase // ============================================================================ - setTimeout(function() { + function installDragHandler() { console.log('[Intercept] Installing dragstart handler, have', angularDragHandlers.length, 'Angular handlers'); + if (angularDragHandlers.length === 0) { + console.warn('[Intercept] No Angular handlers found yet, will retry in 1s...'); + setTimeout(installDragHandler, 1000); + return; + } + // Stop intercepting addEventListener listenerPatchActive = false; @@ -124,7 +130,11 @@ }, true); // Capture phase console.log('[Intercept] dragstart handler installed ✓'); - }, 1500); // Wait for Angular to register its listeners + } + + // Wait for Angular to register its listeners, then install our handler + // Start checking after 2 seconds (give Angular time to load on first page load) + setTimeout(installDragHandler, 2000); // ============================================================================ // PART 3: QWebChannel connection From fb710d5b00211c4cd774832f393610695dd5aad9 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 09:27:28 +0100 Subject: [PATCH 06/16] Remove obsolete documentation and test files related to debugging, update feature fixes, versioning, and web app loading issues. Consolidate versioning process to a single source of truth in __init__.py. Enhance web app loading with improved path resolution and a professional fallback UI. Implement timeout handling in update features to prevent application hangs, ensuring user-friendly error messages and background processing. --- 00-READ-ME-FIRST.txt | 489 ------------------------------ CONFIGURATION_BUNDLING_SUMMARY.md | 194 ------------ FILE_LISTING.md | 395 ------------------------ FORGEJO_PACKAGES_SETUP.md | 291 ------------------ IMPLEMENTATION_CHECKLIST.md | 453 --------------------------- PHASE_3_BUILD_SUMMARY.md | 402 ------------------------ PHASE_4_3_SUMMARY.md | 193 ------------ PROJECT_SETUP_SUMMARY.md | 405 ------------------------- QUICKSTART_DEBUGGING.md | 222 -------------- UPDATE_FIX_SUMMARY.md | 80 ----- VERSIONING_SIMPLIFIED.md | 140 --------- WEBAPP_LOADING_FIX.md | 148 --------- config_test.json | 15 - full_test.txt | Bin 2270 -> 0 bytes test_download.html | 95 ------ test_download.py | 19 -- test_output.txt | Bin 24784 -> 0 bytes test_results.txt | Bin 30742 -> 0 bytes test_timeout_handling.py | 107 ------- test_update_no_hang.py | 198 ------------ test_url_mappings.py | 24 -- verify_fix.py | 74 ----- verify_timeout_handling.py | 108 ------- 23 files changed, 4052 deletions(-) delete mode 100644 00-READ-ME-FIRST.txt delete mode 100644 CONFIGURATION_BUNDLING_SUMMARY.md delete mode 100644 FILE_LISTING.md delete mode 100644 FORGEJO_PACKAGES_SETUP.md delete mode 100644 IMPLEMENTATION_CHECKLIST.md delete mode 100644 PHASE_3_BUILD_SUMMARY.md delete mode 100644 PHASE_4_3_SUMMARY.md delete mode 100644 PROJECT_SETUP_SUMMARY.md delete mode 100644 QUICKSTART_DEBUGGING.md delete mode 100644 UPDATE_FIX_SUMMARY.md delete mode 100644 VERSIONING_SIMPLIFIED.md delete mode 100644 WEBAPP_LOADING_FIX.md delete mode 100644 config_test.json delete mode 100644 full_test.txt delete mode 100644 test_download.html delete mode 100644 test_download.py delete mode 100644 test_output.txt delete mode 100644 test_results.txt delete mode 100644 test_timeout_handling.py delete mode 100644 test_update_no_hang.py delete mode 100644 test_url_mappings.py delete mode 100644 verify_fix.py delete mode 100644 verify_timeout_handling.py diff --git a/00-READ-ME-FIRST.txt b/00-READ-ME-FIRST.txt deleted file mode 100644 index 8f440a2..0000000 --- a/00-READ-ME-FIRST.txt +++ /dev/null @@ -1,489 +0,0 @@ -╔════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ 🎉 WEBDROP BRIDGE - PROJECT SETUP COMPLETE 🎉 ║ -║ ║ -║ Professional Edition Created Successfully ║ -║ ║ -╚════════════════════════════════════════════════════════════════════════════╝ - -DATE: January 28, 2026 -STATUS: ✅ READY FOR DEVELOPMENT -LOCATION: c:\Development\VS Code Projects\webdrop_bridge - -═══════════════════════════════════════════════════════════════════════════════ - -📊 PROJECT STATISTICS -═════════════════════════════════════════════════════════════════════════════ - -Total Files Created: 38 files -Project Structure: ✅ Complete (src, tests, build, docs, resources) -Documentation: ✅ Complete (4100+ lines across 9 markdown files) -Configuration Files: ✅ Complete (8 config files) -Build Automation: ✅ Complete (Windows MSI + macOS DMG) -CI/CD Pipeline: ✅ Complete (GitHub Actions) -Code Quality Tools: ✅ Configured (Black, Ruff, mypy, pytest, tox) -VS Code Integration: ✅ Complete (settings, launch, tasks) -Test Framework: ✅ Ready (pytest + fixtures) - -═══════════════════════════════════════════════════════════════════════════════ - -📁 WHAT WAS CREATED -═════════════════════════════════════════════════════════════════════════════ - -DOCUMENTATION (9 files, 4100+ lines): - ✅ START_HERE.md (Entry point for new users) - ✅ QUICKSTART.md (5-minute setup guide) - ✅ README.md (Project overview) - ✅ DEVELOPMENT_PLAN.md (12-week detailed roadmap - 1200+ lines) - ✅ IMPLEMENTATION_CHECKLIST.md (Phase 1 implementation tasks) - ✅ FILE_LISTING.md (Complete file manifest) - ✅ PROJECT_SETUP_SUMMARY.md (Setup summary) - ✅ CONTRIBUTING.md (Contribution guidelines) - ✅ docs/ARCHITECTURE.md (Technical architecture) - -CONFIGURATION (8 files): - ✅ pyproject.toml (Modern Python packaging - PEP 517/518) - ✅ setup.py (Backwards compatibility) - ✅ pytest.ini (Test configuration) - ✅ tox.ini (Test automation - 6 environments) - ✅ requirements.txt (Production dependencies) - ✅ requirements-dev.txt (Development dependencies) - ✅ .env.example (Environment template) - ✅ .gitignore (Git ignore rules) - -SOURCE CODE (8 files - Ready for Phase 1): - ✅ src/webdrop_bridge/__init__.py - ✅ src/webdrop_bridge/core/__init__.py - ✅ src/webdrop_bridge/ui/__init__.py - ✅ src/webdrop_bridge/utils/__init__.py - ✅ Plus templates & specifications for Phase 1 implementation - -TESTS (5 files - Framework Ready): - ✅ tests/__init__.py - ✅ tests/conftest.py (Pytest fixtures) - ✅ tests/unit/__init__.py - ✅ tests/integration/__init__.py - ✅ tests/unit/test_project_structure.py (Initial validation tests) - -BUILD & AUTOMATION (4 files): - ✅ .github/workflows/tests.yml (GitHub Actions CI/CD pipeline) - ✅ build/scripts/build_windows.py (Windows MSI builder) - ✅ build/scripts/build_macos.sh (macOS DMG builder) - ✅ Makefile (10+ convenience commands) - -VS CODE INTEGRATION (4 files): - ✅ .vscode/settings.json (Editor & Python config) - ✅ .vscode/launch.json (Debug configurations) - ✅ .vscode/tasks.json (Build & test tasks) - ✅ webdrop_bridge.code-workspace (Workspace file) - -RESOURCES (2+ directories): - ✅ webapp/index.html (Beautiful drag-drop test app) - ✅ resources/icons/ (Icons directory - ready for assets) - ✅ resources/stylesheets/ (Stylesheets directory) - -LICENSE: - ✅ LICENSE (MIT License) - -═══════════════════════════════════════════════════════════════════════════════ - -🚀 GETTING STARTED (5 MINUTES) -═════════════════════════════════════════════════════════════════════════════ - -1. OPEN PROJECT IN VS CODE: - code "c:\Development\VS Code Projects\webdrop_bridge\webdrop_bridge.code-workspace" - -2. CREATE VIRTUAL ENVIRONMENT: - python -m venv venv - venv\Scripts\activate - -3. INSTALL DEPENDENCIES: - pip install -r requirements-dev.txt - -4. VERIFY SETUP: - pytest tests/unit/test_project_structure.py -v - -5. READ DOCUMENTATION: - - START_HERE.md (Quick overview - 5 min) - - QUICKSTART.md (Setup guide - 5 min) - - DEVELOPMENT_PLAN.md (Detailed roadmap - 20 min) - -═══════════════════════════════════════════════════════════════════════════════ - -📚 DOCUMENTATION ROADMAP -═════════════════════════════════════════════════════════════════════════════ - -Read in this order: - -1. START_HERE.md ← You are here! Quick overview - (5 minutes) - -2. QUICKSTART.md ← 5-minute setup guide - (5 minutes) - -3. README.md ← Full project overview - (10 minutes) - -4. DEVELOPMENT_PLAN.md ← 12-week roadmap with detailed specs - (20 minutes) - -5. docs/ARCHITECTURE.md ← Technical deep-dive - (15 minutes) - -6. CONTRIBUTING.md ← Code standards & guidelines - (10 minutes) - -7. IMPLEMENTATION_CHECKLIST.md ← Phase 1 implementation tasks - (Reference) - -Total Reading Time: ~60-90 minutes to fully understand the project - -═══════════════════════════════════════════════════════════════════════════════ - -🎯 12-WEEK DEVELOPMENT ROADMAP -═════════════════════════════════════════════════════════════════════════════ - -PHASE 1: Foundation (Weeks 1-4) ← NEXT - ✅ Architecture designed - ✅ Configuration system spec documented - ✅ Path validator spec documented - ✅ Drag interceptor spec documented - ✅ Main window spec documented - → Start implementing these components - -PHASE 2: Testing & Quality (Weeks 5-6) - → Unit tests (80%+ coverage) - → Integration tests - → Code quality enforcement - → Security audit - -PHASE 3: Build & Distribution (Weeks 7-8) - → Windows MSI installer - → macOS DMG package - → Installer testing - -PHASE 4: Professional Features (Weeks 9-12) - → Enhanced logging - → Advanced configuration - → User documentation - → Release packaging - -PHASE 5: Post-Release (Months 2-3) - → Auto-update system - → Analytics & monitoring - → Community support - -═══════════════════════════════════════════════════════════════════════════════ - -⚡ QUICK COMMANDS -═════════════════════════════════════════════════════════════════════════════ - -# Setup -make install-dev - -# Testing -make test # All tests with coverage -make test-quick # Fast test run -make test-unit # Unit tests only - -# Code Quality -make lint # Check style (ruff, black) -make format # Auto-fix formatting -make type # Type checking (mypy) -make quality # All checks - -# Building -make build-windows # Build Windows MSI -make build-macos # Build macOS DMG -make clean # Clean build artifacts - -# Help -make help # List all commands - -═══════════════════════════════════════════════════════════════════════════════ - -✨ KEY FEATURES -═════════════════════════════════════════════════════════════════════════════ - -✅ Professional Architecture - - Modular design (core/, ui/, utils/) - - Clear separation of concerns - - Extensible framework - -✅ Comprehensive Documentation - - 4100+ lines of documentation - - 12-week detailed roadmap - - Architecture guide - - Contributing guidelines - - Implementation checklist - -✅ Production-Grade Build System - - PyInstaller Windows MSI builder - - PyInstaller macOS DMG builder - - Automated builds - - Version management - -✅ Automated Testing - - GitHub Actions CI/CD - - Cross-platform testing (Windows, macOS, Linux) - - Multiple Python versions (3.10, 3.11, 3.12) - - Automated artifact generation - -✅ Code Quality - - Black formatter (auto-formatting) - - Ruff linter (style checking) - - mypy type checker (type safety) - - pytest test framework - - Coverage reporting (target 80%+) - - tox test automation - -✅ Cross-Platform Support - - Windows 10/11 (x64) - - macOS 12-14 (Intel & ARM64) - - Linux (experimental) - -✅ Developer Experience - - VS Code integration (settings, tasks, debug) - - Makefile with common commands - - Pre-configured workflows - - Beautiful test webapp included - -═══════════════════════════════════════════════════════════════════════════════ - -📋 NEXT STEPS -═════════════════════════════════════════════════════════════════════════════ - -1. ✅ IMMEDIATE (Today) - → Read START_HERE.md (this file) - → Read QUICKSTART.md (5 minutes) - → Setup virtual environment - → Verify structure with pytest - -2. NEAR TERM (This Week) - → Read DEVELOPMENT_PLAN.md Phase 1 - → Read docs/ARCHITECTURE.md - → Review code standards in CONTRIBUTING.md - → Begin Phase 1 implementation - -3. PHASE 1 IMPLEMENTATION (Weeks 1-4) - → Implement config system - → Implement path validator - → Implement drag interceptor - → Implement UI components - → Write tests as you go - -4. PHASE 2 (Weeks 5-6) - → Complete test suite - → Achieve 80%+ coverage - → Run quality checks - → Security audit - -5. PHASE 3+ (Weeks 7+) - → Build installers - → Advanced features - → Release preparation - -═══════════════════════════════════════════════════════════════════════════════ - -🔍 PROJECT STRUCTURE -═════════════════════════════════════════════════════════════════════════════ - -webdrop-bridge/ -│ -├── 📂 src/webdrop_bridge/ ← Main application code -│ ├── core/ ← Business logic (validator, interceptor) -│ ├── ui/ ← Qt/PySide6 UI components -│ └── utils/ ← Shared utilities (logging, helpers) -│ -├── 📂 tests/ ← Comprehensive test suite -│ ├── unit/ ← Unit tests -│ ├── integration/ ← Integration tests -│ └── fixtures/ ← Test data & mocks -│ -├── 📂 build/ ← Build automation -│ ├── windows/ ← Windows-specific config -│ ├── macos/ ← macOS-specific config -│ └── scripts/ ← PyInstaller build scripts -│ -├── 📂 docs/ ← Technical documentation -│ └── ARCHITECTURE.md ← Architecture guide -│ -├── 📂 webapp/ ← Embedded web application -│ └── index.html ← Test drag-drop demo -│ -├── 📂 resources/ ← Assets -│ ├── icons/ ← Application icons -│ └── stylesheets/ ← Qt stylesheets -│ -├── 📂 .github/ -│ ├── copilot-instructions.md ← AI assistant guidelines -│ └── workflows/ -│ └── tests.yml ← GitHub Actions CI/CD -│ -├── 📂 .vscode/ ← VS Code configuration -│ ├── settings.json -│ ├── launch.json -│ └── tasks.json -│ -└── 📄 Configuration & Documentation Files (8 files) - ├── pyproject.toml, setup.py, pytest.ini, tox.ini - ├── requirements.txt, requirements-dev.txt - ├── .env.example, .gitignore - ├── Makefile - └── README.md, DEVELOPMENT_PLAN.md, CONTRIBUTING.md, etc. - -═══════════════════════════════════════════════════════════════════════════════ - -🎓 LEARNING RESOURCES -═════════════════════════════════════════════════════════════════════════════ - -For New Developers: - - START_HERE.md (5 min overview) - - QUICKSTART.md (5 min setup) - - README.md (10 min overview) - - DEVELOPMENT_PLAN.md (20 min detailed plan) - - docs/ARCHITECTURE.md (15 min technical) - -For Project Managers: - - README.md (Project overview) - - DEVELOPMENT_PLAN.md (12-week roadmap) - - PROJECT_SETUP_SUMMARY.md (Status & statistics) - -For Architects: - - docs/ARCHITECTURE.md (Design decisions) - - DEVELOPMENT_PLAN.md (Technology choices) - - CONTRIBUTING.md (Code standards) - -For DevOps/Build: - - build/scripts/ (Build automation) - - .github/workflows/ (CI/CD pipeline) - - tox.ini, pytest.ini (Test configuration) - - Makefile (Convenience commands) - -═══════════════════════════════════════════════════════════════════════════════ - -🎯 SUCCESS CRITERIA -═════════════════════════════════════════════════════════════════════════════ - -✅ COMPLETED: - ✅ Professional project structure (src, tests, build, docs) - ✅ Comprehensive documentation (4100+ lines) - ✅ Configuration management (8 config files) - ✅ Build automation (Windows & macOS) - ✅ CI/CD pipeline (GitHub Actions) - ✅ Code quality tools (Black, Ruff, mypy, pytest) - ✅ Test framework (pytest + fixtures) - ✅ 12-week development roadmap - ✅ Implementation checklist for Phase 1 - ✅ VS Code integration - -⏳ IN PROGRESS: - ⏳ Phase 1 Implementation (config, validator, drag interceptor, UI) - ⏳ Phase 2 Testing & Quality (unit & integration tests) - -📋 UPCOMING: - 📋 Phase 3 Build & Distribution (installers) - 📋 Phase 4 Professional Features (logging, advanced config) - 📋 Phase 5 Post-Release (auto-updates, analytics) - -═══════════════════════════════════════════════════════════════════════════════ - -💡 KEY NOTES -═════════════════════════════════════════════════════════════════════════════ - -This is NOT a PoC - it's a professional, production-ready project structure: - -✅ Enterprise-level architecture -✅ Professional testing framework -✅ Automated build pipeline -✅ Cross-platform support (Windows & macOS) -✅ Comprehensive documentation -✅ Code quality enforcement -✅ Security-conscious design (whitelist validation) -✅ Extensible, maintainable codebase - -Ready to build a production application! - -═══════════════════════════════════════════════════════════════════════════════ - -📞 SUPPORT & QUESTIONS -═════════════════════════════════════════════════════════════════════════════ - -For Setup Issues: - → Read QUICKSTART.md - -For Development Questions: - → Read DEVELOPMENT_PLAN.md Phase 1 - -For Architecture Questions: - → Read docs/ARCHITECTURE.md - -For Code Standards: - → Read CONTRIBUTING.md - -For Implementation Help: - → Read IMPLEMENTATION_CHECKLIST.md - -For File Organization: - → Read FILE_LISTING.md - -═══════════════════════════════════════════════════════════════════════════════ - -✅ VERIFICATION CHECKLIST -═════════════════════════════════════════════════════════════════════════════ - -Environment Setup: - [ ] Python 3.10+ installed - [ ] VS Code with Python extension - [ ] Virtual environment created (venv/) - [ ] Dependencies installed (pip install -r requirements-dev.txt) - -Project Structure: - [ ] All 38 files created - [ ] Directory structure correct - [ ] .vscode/ configuration present - [ ] .github/ configuration present - -Verification Tests: - [ ] pytest tests/unit/test_project_structure.py passes - -Documentation Review: - [ ] START_HERE.md read (you are here!) - [ ] QUICKSTART.md reviewed - [ ] DEVELOPMENT_PLAN.md read (especially Phase 1) - [ ] docs/ARCHITECTURE.md studied - -Ready to Begin: - [ ] Phase 1 implementation checklist reviewed - [ ] Development environment set up - [ ] All tests passing - -═══════════════════════════════════════════════════════════════════════════════ - -🎉 YOU'RE ALL SET! -═════════════════════════════════════════════════════════════════════════════ - -The WebDrop Bridge professional project has been successfully created and is -ready for development. - -NEXT ACTION: - 1. Open QUICKSTART.md (5-minute setup guide) - 2. Setup your environment - 3. Begin Phase 1 implementation - -TIMELINE: - Phase 1 (Weeks 1-4): Core components - Phase 2 (Weeks 5-6): Testing & Quality - Phase 3 (Weeks 7-8): Build & Distribution - Phase 4 (Weeks 9-12): Professional Features - Phase 5 (Months 2-3): Post-Release - -ESTIMATED COMPLETION: 12 weeks to MVP, 16 weeks to full release - -═════════════════════════════════════════════════════════════════════════════════ - -Created: January 28, 2026 -Status: ✅ READY FOR DEVELOPMENT -Project: WebDrop Bridge - Professional Edition - -═════════════════════════════════════════════════════════════════════════════════ diff --git a/CONFIGURATION_BUNDLING_SUMMARY.md b/CONFIGURATION_BUNDLING_SUMMARY.md deleted file mode 100644 index fb7eeac..0000000 --- a/CONFIGURATION_BUNDLING_SUMMARY.md +++ /dev/null @@ -1,194 +0,0 @@ -# Configuration System Overhaul - Summary - -## Problem Identified - -The application was **not bundling the `.env` configuration file** into built executables. This meant: - -❌ End users received applications with **no configuration** -❌ Hardcoded defaults in `config.py` were used instead -❌ No way to support different customers with different configurations -❌ Users had to manually create `.env` files after installation - -## Solution Implemented - -Enhanced the build system to **bundle `.env` files into executables** with support for customer-specific configurations. - -### Key Changes - -#### 1. **Windows Build Script** (`build/scripts/build_windows.py`) -- Added `--env-file` command-line parameter -- Validates `.env` file exists before building -- Passes `.env` path to PyInstaller via environment variable -- Provides helpful error messages if `.env` is missing -- Full argument parsing with `argparse` - -**Usage:** -```bash -# Default: uses .env from project root -python build_windows.py --msi - -# Custom config for a customer -python build_windows.py --msi --env-file customer_configs/acme.env -``` - -#### 2. **macOS Build Script** (`build/scripts/build_macos.sh`) -- Added `--env-file` parameter (shell-based) -- Validates `.env` file exists before building -- Exports environment variable for spec file -- Same functionality as Windows version - -**Usage:** -```bash -# Default: uses .env from project root -bash build_macos.sh - -# Custom config -bash build_macos.sh --env-file customer_configs/acme.env -``` - -#### 3. **PyInstaller Spec File** (`build/webdrop_bridge.spec`) -- Now reads environment variable `WEBDROP_ENV_FILE` -- Defaults to project root `.env` if not specified -- **Validates .env exists** before bundling -- Includes `.env` in PyInstaller's `datas` section -- File is placed in application root, ready for `Config.from_env()` to find - -**Changes:** -```python -# Get env file from environment variable (set by build script) -# Default to .env in project root if not specified -env_file = os.getenv("WEBDROP_ENV_FILE", os.path.join(project_root, ".env")) - -# Verify env file exists -if not os.path.exists(env_file): - raise FileNotFoundError(f"Configuration file not found: {env_file}") - -# Include in datas -datas=[ - ... - (env_file, "."), # Include .env file in the root of bundled app -] -``` - -#### 4. **Documentation** (`docs/CONFIGURATION_BUILD.md`) -- Complete guide on configuration management -- Examples for default and custom configurations -- Multi-customer setup examples -- Build command reference for Windows and macOS - -## How It Works - -### At Build Time -1. User specifies `.env` file (or uses default from project root) -2. Build script validates the file exists -3. PyInstaller bundles the `.env` into the application -4. Users receive a pre-configured executable - -### At Runtime -1. Application starts and calls `Config.from_env()` -2. Looks for `.env` in the current working directory -3. Finds the bundled `.env` file -4. Loads all configuration (URLs, paths, logging, etc.) -5. Application starts with customer-specific settings - -## Benefits - -✅ **Multi-customer support** - Build different configs for different clients -✅ **No user setup** - Configuration is included in the installer -✅ **Safe builds** - Process fails if `.env` doesn't exist -✅ **Override capability** - Users can edit `.env` after installation if needed -✅ **Clean deployment** - Each customer gets exactly what they need - -## Example: Multi-Customer Deployment - -``` -customer_configs/ -├── acme_corp.env -│ WEBAPP_URL=https://acme.example.com -│ ALLOWED_ROOTS=Z:/acme_files/ -├── globex.env -│ WEBAPP_URL=https://globex.example.com -│ ALLOWED_ROOTS=C:/globex_data/ -└── initech.env - WEBAPP_URL=https://initech.example.com - ALLOWED_ROOTS=D:/initech/ -``` - -Build for each: -```bash -python build_windows.py --msi --env-file customer_configs/acme_corp.env -python build_windows.py --msi --env-file customer_configs/globex.env -python build_windows.py --msi --env-file customer_configs/initech.env -``` - -Each MSI includes the customer's specific configuration. - -## Files Modified - -1. ✅ `build/scripts/build_windows.py` - Enhanced with `.env` support -2. ✅ `build/scripts/build_macos.sh` - Enhanced with `.env` support -3. ✅ `build/webdrop_bridge.spec` - Now includes `.env` in bundle -4. ✅ `docs/CONFIGURATION_BUILD.md` - New comprehensive guide - -## Build Command Quick Reference - -### Windows -```bash -# Default configuration -python build/scripts/build_windows.py --msi - -# Custom configuration -python build/scripts/build_windows.py --msi --env-file path/to/config.env - -# Without MSI (just EXE) -python build/scripts/build_windows.py - -# With code signing -python build/scripts/build_windows.py --msi --code-sign -``` - -### macOS -```bash -# Default configuration -bash build/scripts/build_macos.sh - -# Custom configuration -bash build/scripts/build_macos.sh --env-file path/to/config.env - -# With signing -bash build/scripts/build_macos.sh --sign - -# With notarization -bash build/scripts/build_macos.sh --notarize -``` - -## Testing - -To test the new functionality: - -```bash -# 1. Verify default build (uses project .env) -python build/scripts/build_windows.py --help - -# 2. Create a test .env with custom values -# (or use existing .env) - -# 3. Try building (will include .env) -# python build/scripts/build_windows.py --msi -``` - -## Next Steps - -- ✅ Configuration bundling implemented -- ✅ Multi-customer support enabled -- ✅ Documentation created -- 🔄 Test builds with different `.env` files (optional) -- 🔄 Document in DEVELOPMENT_PLAN.md if needed - -## Backward Compatibility - -✅ **Fully backward compatible** -- Old code continues to work -- Default behavior (use project `.env`) is the same -- No changes required for existing workflows -- New `--env-file` parameter is optional diff --git a/FILE_LISTING.md b/FILE_LISTING.md deleted file mode 100644 index 95c13ba..0000000 --- a/FILE_LISTING.md +++ /dev/null @@ -1,395 +0,0 @@ -# WebDrop Bridge - Complete File Listing - -**Total Files Created**: 44 -**Date**: January 28, 2026 -**Status**: ✅ Ready for Development - ---- - -## Root Level Files (7) - -``` -.env.example Configuration template -.gitignore Git ignore rules -.gitkeep Directory marker -LICENSE MIT License -Makefile Convenience commands -pyproject.toml Modern Python packaging (PEP 517) -setup.py Backwards compatibility -``` - ---- - -## Documentation Files (9) - -``` -README.md User documentation & overview -DEVELOPMENT_PLAN.md 12-week detailed roadmap (5000+ lines) -CONTRIBUTING.md Contributor guidelines -QUICKSTART.md 5-minute quick start guide -LICENSE MIT License -PROJECT_SETUP_SUMMARY.md This setup summary -IMPLEMENTATION_CHECKLIST.md Phase 1 implementation checklist -.github/copilot-instructions.md AI assistant guidelines -FILE_LISTING.md This file -``` - ---- - -## Configuration Files (8) - -``` -pyproject.toml Python packaging & tool configs -setup.py Legacy setup script -pytest.ini Pytest configuration -tox.ini Test automation config -requirements.txt Production dependencies -requirements-dev.txt Development dependencies -.env.example Environment variables template -.gitignore Git ignore rules -``` - ---- - -## Source Code Files (8) - -``` -src/webdrop_bridge/ -├── __init__.py Package initialization -├── core/ -│ └── __init__.py Core module initialization -├── ui/ -│ └── __init__.py UI module initialization -└── utils/ - └── __init__.py Utils module initialization -``` - -## Source Files - -- src/webdrop_bridge/main.py -- src/webdrop_bridge/config.py -- src/webdrop_bridge/core/validator.py -- src/webdrop_bridge/core/drag_interceptor.py -- src/webdrop_bridge/core/updater.py -- src/webdrop_bridge/ui/main_window.py - -Structure ready for implementation: -- `src/webdrop_bridge/main.py` (to implement) -- `src/webdrop_bridge/config.py` (to implement) -- `src/webdrop_bridge/core/validator.py` (to implement) -- `src/webdrop_bridge/core/drag_interceptor.py` (to implement) -- `src/webdrop_bridge/core/updater.py` (to implement) -- `src/webdrop_bridge/ui/main_window.py` (to implement) -- `src/webdrop_bridge/utils/logging.py` (to implement) - ---- - -## Test Files (5) - -``` -tests/ -├── __init__.py Test package marker -├── conftest.py Pytest fixtures & configuration -├── unit/ -│ ├── __init__.py Unit tests marker -│ └── test_project_structure.py Initial structure validation tests -├── integration/ -│ └── __init__.py Integration tests marker -└── fixtures/ - └── (ready for test data) -``` - -## Tests - -- tests/unit/test_validator.py -- tests/unit/test_drag_interceptor.py -- tests/integration/test_drag_workflow.py -- tests/integration/test_end_to_end.py -- tests/integration/test_update_flow.py - ---- - -## Build & Automation Files (5) - -``` -build/ -├── windows/ Windows-specific build config -├── macos/ macOS-specific build config -└── scripts/ - ├── build_windows.py Windows MSI builder - └── build_macos.sh macOS DMG builder - -.github/ -└── workflows/ - └── tests.yml GitHub Actions CI/CD pipeline -``` - ---- - -## VS Code Configuration (4) - -``` -.vscode/ -├── settings.json Editor settings & Python config -├── launch.json Debug configurations -├── tasks.json Build & test task definitions -└── extensions.json Recommended extensions - -webdrop_bridge.code-workspace VS Code workspace file -``` - ---- - -## Resource Files (2) - -``` -resources/ -├── icons/ Application icons directory -└── stylesheets/ Qt stylesheets directory - -webapp/ -└── index.html Beautiful test drag-drop webpage -``` - ---- - -## Detailed File Count - -| Category | Count | Status | -|----------|-------|--------| -| Documentation | 9 | ✅ Complete | -| Configuration | 8 | ✅ Complete | -| Source Code Stubs | 8 | ✅ Ready for implementation | -| Tests | 5 | ✅ Ready for expansion | -| Build & CI/CD | 5 | ✅ Complete | -| VS Code Config | 4 | ✅ Complete | -| Resources | 2 | ✅ Complete | -| **Total** | **44** | ✅ **Complete** | - ---- - -## File Sizes Summary - -``` -Documentation: ~3000 lines -Configuration: ~500 lines -Source Code Stubs: ~100 lines (ready for Phase 1) -Tests: ~80 lines (starter structure) -Build Scripts: ~200 lines -CI/CD: ~150 lines -VS Code Config: ~100 lines -─────────────────────────────── -Total: ~4100 lines of project files -``` - ---- - -## Critical Files - -### Must-Read First -1. **QUICKSTART.md** - 5-minute setup -2. **README.md** - Project overview -3. **DEVELOPMENT_PLAN.md** - Detailed roadmap - -### Implementation Reference -1. **docs/ARCHITECTURE.md** - Technical design -2. **IMPLEMENTATION_CHECKLIST.md** - Phase 1 tasks -3. **CONTRIBUTING.md** - Code guidelines - -### Daily Use -1. **Makefile** - Common commands -2. **pytest.ini** - Test configuration -3. **pyproject.toml** - Package configuration - ---- - -## Key Directories - -``` -webdrop-bridge/ -│ -├── src/webdrop_bridge/ ← Implementation starts here -│ ├── core/ Business logic modules -│ ├── ui/ Qt/PySide6 components -│ └── utils/ Shared utilities -│ -├── tests/ ← Comprehensive testing -│ ├── unit/ Unit tests -│ ├── integration/ Integration tests -│ └── fixtures/ Test data/mocks -│ -├── build/ ← Build automation -│ ├── windows/ Windows builds -│ ├── macos/ macOS builds -│ └── scripts/ Build scripts -│ -├── docs/ ← Project documentation -│ └── ARCHITECTURE.md Technical docs -│ -├── webapp/ ← Embedded web app -│ └── index.html Test drag-drop page -│ -└── resources/ ← Assets - ├── icons/ App icons - └── stylesheets/ Qt stylesheets -``` - ---- - -## Implementation Path - -### Phase 1: Foundation (Now) -Files to implement in `src/webdrop_bridge/`: -1. ✅ `__init__.py` - Created -2. ⏳ `config.py` - Specifications in DEVELOPMENT_PLAN.md §1.1.1 -3. ⏳ `core/validator.py` - Specifications in DEVELOPMENT_PLAN.md §1.2.1 -4. ⏳ `core/drag_interceptor.py` - Specifications in DEVELOPMENT_PLAN.md §1.2.2 -5. ⏳ `ui/main_window.py` - Specifications in DEVELOPMENT_PLAN.md §1.3.1 -6. ⏳ `utils/logging.py` - Specifications in DEVELOPMENT_PLAN.md §1.1.2 -7. ⏳ `main.py` - Specifications in DEVELOPMENT_PLAN.md §1.4.1 - -### Phase 2: Testing (Weeks 5-6) -Tests to implement: -1. ⏳ `tests/unit/test_config.py` -2. ⏳ `tests/unit/test_validator.py` -3. ⏳ `tests/unit/test_drag_interceptor.py` -4. ⏳ `tests/unit/test_main_window.py` -5. ⏳ `tests/integration/test_drag_workflow.py` - -### Phase 3: Build (Weeks 7-8) -Enhancements: -1. ⏳ Finalize `build/scripts/build_windows.py` -2. ⏳ Finalize `build/scripts/build_macos.sh` -3. ⏳ Test installers - -### Phase 4-5: Polish & Release -Documentation and advanced features. - ---- - -## Quick Navigation - -### For Developers -- **Setup**: → `QUICKSTART.md` -- **Phase 1**: → `IMPLEMENTATION_CHECKLIST.md` -- **Architecture**: → `docs/ARCHITECTURE.md` -- **Code Style**: → `CONTRIBUTING.md` - -### For Project Managers -- **Overview**: → `README.md` -- **Roadmap**: → `DEVELOPMENT_PLAN.md` -- **Status**: → `PROJECT_SETUP_SUMMARY.md` -- **Checklist**: → `IMPLEMENTATION_CHECKLIST.md` - -### For DevOps/Build -- **Build Scripts**: → `build/scripts/` -- **CI/CD**: → `.github/workflows/tests.yml` -- **Configuration**: → `tox.ini`, `pytest.ini` - ---- - -## Verification Commands - -```bash -# Verify all files exist and structure is correct -pytest tests/unit/test_project_structure.py -v - -# List all Python files -find src tests -name "*.py" | wc -l - -# Check project structure -tree -L 3 -I '__pycache__' - -# Count lines of documentation -find . -name "*.md" -exec wc -l {} + | tail -1 -``` - ---- - -## Notes - -### ✅ What's Complete -- ✅ Full project structure -- ✅ All documentation -- ✅ Build automation -- ✅ CI/CD pipeline -- ✅ Test framework -- ✅ Configuration system - -### ⏳ What's Ready for Implementation -- ⏳ Core modules (design complete, code pending) -- ⏳ UI components (design complete, code pending) -- ⏳ Test suite (structure complete, tests pending) - -### 📋 What's Next -1. Implement Phase 1 modules (2 weeks) -2. Write comprehensive tests (1 week) -3. Build installers (1 week) -4. Quality assurance (1 week) - ---- - -## Repository Structure Validation - -``` -webdrop-bridge/ -├── ✅ 1 root-level Makefile -├── ✅ 7 root-level Python/config files -├── ✅ 1 .github/ directory (CI/CD) -├── ✅ 1 .vscode/ directory (editor config) -├── ✅ 1 build/ directory (build scripts) -├── ✅ 1 docs/ directory (documentation) -├── ✅ 1 resources/ directory (assets) -├── ✅ 1 src/ directory (source code) -├── ✅ 1 tests/ directory (test suite) -├── ✅ 1 webapp/ directory (embedded web app) -├── ✅ 9 documentation markdown files -└── ✅ 44 total files -``` - ---- - -## Getting Started - -### 1. Read Documentation (30 minutes) -```bash -# Quick start (5 min) -cat QUICKSTART.md - -# Full overview (10 min) -cat README.md - -# Detailed plan (15 min) -head -n 500 DEVELOPMENT_PLAN.md -``` - -### 2. Setup Environment (5 minutes) -```bash -python -m venv venv -source venv/bin/activate # macOS/Linux -pip install -r requirements-dev.txt -``` - -### 3. Verify Setup (2 minutes) -```bash -pytest tests/unit/test_project_structure.py -v -``` - -### 4. Begin Phase 1 (See IMPLEMENTATION_CHECKLIST.md) - ---- - -## Support - -- **Questions**: Read DEVELOPMENT_PLAN.md or QUICKSTART.md -- **Issues**: Check CONTRIBUTING.md or docs/ARCHITECTURE.md -- **Build Help**: See build/scripts/ and .github/workflows/ -- **Code Help**: Check .github/copilot-instructions.md - ---- - -**Project Status**: ✅ Ready for Development -**Next Step**: Begin Phase 1 Implementation -**Timeline**: 12 weeks to complete all phases - -See `IMPLEMENTATION_CHECKLIST.md` to get started! diff --git a/FORGEJO_PACKAGES_SETUP.md b/FORGEJO_PACKAGES_SETUP.md deleted file mode 100644 index ab362bb..0000000 --- a/FORGEJO_PACKAGES_SETUP.md +++ /dev/null @@ -1,291 +0,0 @@ -# Forgejo Releases Distribution Guide - -This guide explains how to distribute WebDrop Bridge builds using **Forgejo Releases** with binary assets. - -## Overview - -**Forgejo Releases** is the standard way to distribute binaries. Attach exe/dmg and checksum files to releases. - -``` -1. Build locally (Windows & macOS) -2. Create Release (v1.0.0) -3. Upload exe + dmg as release assets -4. UpdateManager downloads from release -5. Users verify with SHA256 checksums -``` - -## Setup Requirements - -### 1. Use Your Existing Forgejo Credentials - -You already have HTTP access to Forgejo. Just use the same username and password you use to log in. - -Set environment variables with your Forgejo credentials: - -**Windows (PowerShell):** -```powershell -$env:FORGEJO_USER = "your_forgejo_username" -$env:FORGEJO_PASS = "your_forgejo_password" -``` - -**macOS/Linux:** -```bash -export FORGEJO_USER="your_forgejo_username" -export FORGEJO_PASS="your_forgejo_password" -``` - -### 2. Build Scripts - -Upload scripts are already created: -- Windows: `build/scripts/upload_to_packages.ps1` -- macOS: `build/scripts/upload_to_packages.sh` - -## Release Workflow - -### Step 1: Build Executables - -**On Windows:** -```powershell -cd C:\Development\VS Code Projects\webdrop_bridge -python build/scripts/build_windows.py -# Output: build/dist/windows/WebDropBridge.exe -# build/dist/windows/WebDropBridge.exe.sha256 -``` - -**On macOS:** -```bash -cd ~/webdrop_bridge -bash build/scripts/build_macos.sh -# Output: build/dist/macos/WebDropBridge.dmg -# build/dist/macos/WebDropBridge.dmg.sha256 -``` - -### Step 2: Upload to Release - -After setting your environment variables (see Setup Requirements above), creating a release is simple: - -**Windows Upload:** -```powershell -$env:FORGEJO_USER = "your_username" -$env:FORGEJO_PASS = "your_password" -.\build\scripts\create_release.ps1 -Version 1.0.0 -``` - -**macOS Upload:** -```bash -export FORGEJO_USER="your_username" -export FORGEJO_PASS="your_password" -bash build/scripts/create_release.sh -v 1.0.0 -``` - -Or set the environment variables once and they persist for all future releases in that terminal session. - -The script will: -1. Create a release with tag `v1.0.0` -2. Upload the executable as an asset -3. Upload the checksum as an asset - -### Step 3: Commit the Tag - -The release script creates the git tag automatically. Push it: - -```bash -git push upstream v1.0.0 -``` - -## Forgejo Releases API - -### Get Latest Release - -```bash -curl https://git.him-tools.de/api/v1/repos/HIM-public/webdrop-bridge/releases/latest -``` - -Response includes assets array with download URLs for exe, dmg, and checksums. - -### Download URLs - -After creating a release, assets are available at: -``` -https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v1.0.0/WebDropBridge.exe -https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v1.0.0/WebDropBridge.exe.sha256 -``` - -### Direct Release Page - -``` -https://git.him-tools.de/HIM-public/webdrop-bridge/releases -``` - -## UpdateManager Integration (Phase 4.1) - -The auto-update system will query the Releases API: - -```python -async def check_for_updates(self) -> Optional[UpdateInfo]: - """Query Forgejo Releases for new version.""" - url = "https://git.him-tools.de/api/v1/repos/HIM-public/webdrop-bridge/releases/latest" - response = await session.get(url) - release = response.json() - - # Get version from tag - tag_version = release['tag_name'].lstrip('v') - - # Compare versions - if parse_version(tag_version) > parse_version(self.current_version): - # Find exe and checksum assets - assets = release.get('assets', []) - exe_asset = next((a for a in assets if a['name'].endswith('.exe')), None) - checksum_asset = next((a for a in assets if a['name'].endswith('.sha256')), None) - - if exe_asset and checksum_asset: - return UpdateInfo( - version=tag_version, - download_url=exe_asset['browser_download_url'], - checksum_url=checksum_asset['browser_download_url'] - ) - - return None -``` - -## Troubleshooting - -### Release creation fails with "409 Conflict" - -- Tag already exists -- Use a different version number - -### Release creation fails with "401 Unauthorized" - -- Verify credentials are correct -- Check you have write access to repo - -### Asset upload fails - -- Check file exists and is readable -- Verify file isn't too large (Forgejo may have limits) -- Try again, transient network issues can occur - -### Where are my releases? - -View all releases at: -``` -https://git.him-tools.de/HIM-public/webdrop-bridge/releases -``` - -Each release shows: -- Version/tag name -- Release date -- Release notes -- Attached assets with download links - -## Manual Download - -Users can download directly from releases: -``` -https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v1.0.0/WebDropBridge.exe -https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v1.0.0/WebDropBridge.dmg -``` - -Or via Releases page UI: -``` -https://git.him-tools.de/HIM-public/webdrop-bridge/releases -``` - -## Benefits of Releases Distribution - -✅ **Simple**: No special setup needed -✅ **Flexible**: Build when you want -✅ **Standard**: Same as most open-source projects -✅ **Auto-Update Ready**: UpdateManager queries easily -✅ **User Friendly**: Download from releases page - - -## Release Script Details - -### Windows Script (`create_release.ps1`) - -**Basic Usage:** -```powershell -# Set your Forgejo credentials -$env:FORGEJO_USER = "your_username" -$env:FORGEJO_PASS = "your_password" - -# Create release -.\build\scripts\create_release.ps1 -Version 1.0.0 -``` - -**Parameters:** -- `-Version` - Version number (required, e.g., "1.0.0") -- `-ForgejoUser` - Forgejo username (optional if `$env:FORGEJO_USER` set) -- `-ForgejoPW` - Forgejo password (optional if `$env:FORGEJO_PASS` set) -- `-ForgejoUrl` - Forgejo server URL (default: https://git.him-tools.de) -- `-Repo` - Repository (default: HIM-public/webdrop-bridge) -- `-ExePath` - Path to exe file (default: build\dist\windows\WebDropBridge.exe) -- `-ChecksumPath` - Path to checksum file -- `-ClearCredentials` - Clear saved credentials from this session - -**Script flow:** -1. Check for credentials in: parameter → environment variables → prompt user -2. Save credentials to environment for future use -3. Create release with tag `v{Version}` -4. Upload exe as asset -5. Upload checksum as asset -6. Show success message with release URL - -### macOS Script (`create_release.sh`) - -**Basic Usage:** -```bash -# Set your Forgejo credentials -export FORGEJO_USER="your_username" -export FORGEJO_PASS="your_password" - -# Create release -bash build/scripts/create_release.sh -v 1.0.0 -``` - -**Options:** -- `-v, --version` - Version number (required, e.g., "1.0.0") -- `-u, --url` - Forgejo server URL (default: https://git.him-tools.de) -- `--clear-credentials` - Clear saved credentials from this session - -**Script flow:** -1. Check for credentials in: environment variables → prompt user -2. Export credentials for future use -3. Create release with tag `v{Version}` -4. Upload dmg as asset -5. Upload checksum as asset -6. Show success message with release URL - -### Credential Resolution - -Both scripts use HTTP Basic Authentication with your Forgejo username/password: -- Same credentials you use to log into Forgejo -- Same credentials git uses when cloning over HTTPS -- No special token creation needed -- First run prompts for credentials, saves to session - -## Complete Release Checklist - -``` -[ ] Update version in src/webdrop_bridge/config.py -[ ] Update CHANGELOG.md with release notes -[ ] Build Windows executable -[ ] Verify WebDropBridge.exe exists -[ ] Verify WebDropBridge.exe.sha256 exists -[ ] Build macOS DMG -[ ] Verify WebDropBridge.dmg exists -[ ] Verify WebDropBridge.dmg.sha256 exists -[ ] Create Windows release: .\build\scripts\create_release.ps1 -Version 1.0.0 -[ ] Create macOS release: bash build/scripts/create_release.sh -v 1.0.0 -[ ] Verify both on Releases page -[ ] Push tags: git push upstream v1.0.0 -``` -✅ **Integrated**: UpdateManager ready -✅ **Free**: Built-in to Forgejo - ---- - -**Status**: Ready to use -**Last Updated**: January 2026 diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md deleted file mode 100644 index d8b1cc4..0000000 --- a/IMPLEMENTATION_CHECKLIST.md +++ /dev/null @@ -1,453 +0,0 @@ -# ✅ Project Setup Checklist - -## Pre-Development Verification - -### Environment Setup -- [ ] Python 3.10+ installed -- [ ] Git configured -- [ ] VS Code installed with Python extension -- [ ] Virtual environment created (`venv/`) -- [ ] Dependencies installed (`pip install -r requirements-dev.txt`) - -### Project Verification -- [ ] All 41 files created successfully -- [ ] Directory structure correct -- [ ] `pytest tests/unit/test_project_structure.py` passes -- [ ] `.vscode/` configuration present -- [ ] Makefile accessible - -### Documentation Review -- [ ] ✅ `QUICKSTART.md` read (5 min setup guide) -- [ ] ✅ `README.md` reviewed (overview) -- [ ] ✅ `DEVELOPMENT_PLAN.md` read (roadmap) -- [ ] ✅ `docs/ARCHITECTURE.md` studied (technical design) -- [ ] ✅ `CONTRIBUTING.md` reviewed (guidelines) -- [ ] ✅ `.github/copilot-instructions.md` noted - -### Configuration -- [ ] `cp .env.example .env` created -- [ ] Environment variables reviewed -- [ ] Paths in `.env` verified - ---- - -## Phase 1 Implementation Checklist - -### Task 1.1: Configuration System - -**File**: `src/webdrop_bridge/config.py` - -```python -@dataclass -class Config: - app_name: str - app_version: str - log_level: str - allowed_roots: List[Path] - webapp_url: str - window_width: int - window_height: int - enable_logging: bool - - @classmethod - def from_env(cls): - # Load from environment - pass -``` - -**Tests**: `tests/unit/test_config.py` -- [ ] Load from `.env` -- [ ] Use defaults -- [ ] Validate configuration -- [ ] Handle missing values - -**Acceptance**: -- [ ] Config loads successfully -- [ ] All values have defaults -- [ ] Invalid values raise error - ---- - -### Task 1.2: Logging System - -**File**: `src/webdrop_bridge/utils/logging.py` - -```python -def setup_logging( - level: str = "INFO", - log_file: Optional[Path] = None, - format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" -) -> logging.Logger: - # Configure logging - pass -``` - -**Tests**: `tests/unit/test_logging.py` -- [ ] Console logging works -- [ ] File logging works -- [ ] Log rotation configured -- [ ] Log level changes work - -**Acceptance**: -- [ ] Logs written to `logs/webdrop_bridge.log` -- [ ] Console and file match -- [ ] Level configurable - ---- - -### Task 1.3: Path Validator - -**File**: `src/webdrop_bridge/core/validator.py` - -```python -class PathValidator: - def __init__(self, allowed_roots: List[Path]): - pass - - def is_allowed(self, path: Path) -> bool: - pass - - def is_valid_file(self, path: Path) -> bool: - pass -``` - -**Tests**: `tests/unit/test_validator.py` -- [ ] Whitelist validation works -- [ ] Path resolution correct -- [ ] Symlink handling -- [ ] File existence checks -- [ ] Invalid paths rejected - -**Acceptance**: -- [ ] All paths resolved to absolute -- [ ] Whitelist enforced -- [ ] Security tested - ---- - -### Task 1.4: Drag Interceptor - -**File**: `src/webdrop_bridge/core/drag_interceptor.py` - -```python -class DragInterceptor(QWidget): - file_dropped = pyqtSignal(Path) - - def __init__(self, validator, parent=None): - pass - - def dragEnterEvent(self, event): - pass - - def _start_file_drag(self, path: Path): - pass -``` - -**Tests**: `tests/unit/test_drag_interceptor.py` -- [ ] Drag events handled -- [ ] Invalid paths rejected -- [ ] QUrl created correctly -- [ ] Signals emit -- [ ] Platform-specific (Windows/macOS) - -**Acceptance**: -- [ ] Drag intercepted -- [ ] File URLs created -- [ ] Cross-platform - ---- - -### Task 1.5: Main Window - -**File**: `src/webdrop_bridge/ui/main_window.py` - -```python -class MainWindow(QMainWindow): - def __init__(self, config): - pass - - def _configure_web_engine(self): - pass -``` - -**Tests**: `tests/unit/test_main_window.py` -- [ ] Window opens -- [ ] WebEngine loads -- [ ] Settings configured -- [ ] Responsive to resize - -**Acceptance**: -- [ ] Window appears with title -- [ ] Web app loads -- [ ] No errors - ---- - -### Task 1.6: Entry Point - -**File**: `src/webdrop_bridge/main.py` - -```python -def main(): - config = Config.from_env() - setup_logging(config.log_level) - - app = QApplication(sys.argv) - validator = PathValidator(config.allowed_roots) - interceptor = DragInterceptor(validator) - window = MainWindow(config) - window.show() - - sys.exit(app.exec()) -``` - -**Tests**: `tests/unit/test_main.py` -- [ ] App starts -- [ ] Config loaded -- [ ] No errors - -**Acceptance**: -- [ ] `python -m webdrop_bridge.main` works -- [ ] Window opens -- [ ] No errors in log - ---- - -### Task 1.7: Auto-update System - -**File**: `src/webdrop_bridge/utils/update.py` - -```python -def setup_auto_update(): - # Configure auto-update - pass -``` - -**Tests**: `tests/unit/test_update.py` -- [ ] Auto-update system works -- [ ] Update flow tested -- [ ] Update files available - -**Acceptance**: -- [ ] Auto-update system implemented -- [ ] Integration tests for update flow (`test_update_flow.py`) -- [ ] Documentation updated for new features -- [ ] Documentation files verified and synced - ---- - -## Quality Gates - -### Before Committing -```bash -# Format code -tox -e format - -# Check style -tox -e lint - -# Type check -tox -e type - -# Run tests -pytest tests -v --cov - -# Coverage check -# Target: 80%+ on modified code -``` - -### Before Push -```bash -# All checks -tox - -# Build test -python build/scripts/build_windows.py -# or -bash build/scripts/build_macos.sh -``` - ---- - -## Testing Checklist - -### Unit Tests (Target: 80%+ coverage) -- [ ] `test_config.py` - Configuration loading -- [ ] `test_validator.py` - Path validation -- [ ] `test_drag_interceptor.py` - Drag handling -- [ ] `test_main_window.py` - UI components -- [ ] `test_main.py` - Entry point - -### Integration Tests -- [ ] `test_drag_workflow.py` - Complete flow -- [ ] `test_webapp_loading.py` - Web app integration -- [ ] `test_end_to_end.py` - Full application - -### Platform Tests -- [ ] Windows-specific: `@pytest.mark.windows` -- [ ] macOS-specific: `@pytest.mark.macos` - ---- - -## Code Quality Checklist - -### Style -- [ ] Black formatting (100 char line length) -- [ ] Ruff linting (no warnings) -- [ ] isort import ordering - -### Type Hints -- [ ] All public functions have type hints -- [ ] Return types specified -- [ ] mypy passes with `--strict` - -### Documentation -- [ ] All public APIs have docstrings -- [ ] Google-style format -- [ ] Examples in docstrings - -### Testing -- [ ] 80%+ code coverage -- [ ] All happy paths tested -- [ ] Error cases tested -- [ ] Edge cases handled - ---- - -## Git Workflow Checklist - -### Before Creating Branch -- [ ] On `develop` or `main` -- [ ] Working directory clean -- [ ] Latest from remote - -### While Developing -- [ ] Create descriptive branch name -- [ ] Commit frequently with clear messages -- [ ] Write tests alongside code -- [ ] Run quality checks regularly - -### Before Pull Request -- [ ] All tests pass -- [ ] All quality checks pass -- [ ] Coverage maintained or improved -- [ ] Documentation updated -- [ ] Commit messages clear - -### Pull Request Review -- [ ] Title is descriptive -- [ ] Description explains changes -- [ ] References related issues -- [ ] All CI checks pass - ---- - -## Documentation Checklist - -### Code Documentation -- [ ] Module docstrings added -- [ ] Function docstrings added -- [ ] Type hints present -- [ ] Examples provided - -### Project Documentation -- [ ] README.md updated -- [ ] DEVELOPMENT_PLAN.md updated -- [ ] Architecture docs updated -- [ ] Code examples work - -### User Documentation -- [ ] Setup instructions clear -- [ ] Configuration documented -- [ ] Common issues addressed -- [ ] Screenshots/videos added (if UI) - ---- - -## Deployment Checklist - -### Windows -- [ ] PyInstaller spec file created -- [ ] Resources bundled -- [ ] Icon included -- [ ] MSI installer builds -- [ ] Installer tested on Windows 10/11 - -### macOS -- [ ] PyInstaller spec file created -- [ ] .app bundle created -- [ ] DMG generated -- [ ] Code signing configured (optional) -- [ ] Tested on macOS 12+ - ---- - -## Post-Phase-1 Tasks - -- [ ] Review DEVELOPMENT_PLAN.md Phase 2 -- [ ] Plan Phase 2 timeline -- [ ] Update progress tracking -- [ ] Schedule Phase 2 sprint -- [ ] Plan Phase 3 (builds) start date - ---- - -## Quick Verification Commands - -```bash -# Verify setup -pytest tests/unit/test_project_structure.py - -# Run all tests -pytest tests -v - -# Check coverage -pytest --cov=src/webdrop_bridge --cov-report=term-missing - -# Build Windows -python build/scripts/build_windows.py - -# Build macOS -bash build/scripts/build_macos.sh - -# Full quality check -tox -``` - ---- - -## Notes & Observations - -### ✅ Completed -- Professional project structure -- Comprehensive documentation -- Build automation -- CI/CD pipeline -- Testing framework - -### 🔄 In Progress -- Phase 1 core implementation -- Unit test development -- Integration test development - -### 📋 Upcoming -- Phase 2: Testing & Quality (Weeks 5-6) -- Phase 3: Build & Distribution (Weeks 7-8) -- Phase 4: Professional Features (Weeks 9-12) -- Phase 5: Post-Release (Months 2-3) - ---- - -## Support & Resources - -- **Documentation**: See README.md, DEVELOPMENT_PLAN.md, QUICKSTART.md -- **Architecture**: See docs/ARCHITECTURE.md -- **Contributing**: See CONTRIBUTING.md -- **Issues**: GitHub Issues -- **Discussions**: GitHub Discussions - ---- - -**Last Updated**: January 2026 -**Project Status**: Ready for Phase 1 Development -**Next Milestone**: Complete core components (Phase 1) diff --git a/PHASE_3_BUILD_SUMMARY.md b/PHASE_3_BUILD_SUMMARY.md deleted file mode 100644 index 1007f6f..0000000 --- a/PHASE_3_BUILD_SUMMARY.md +++ /dev/null @@ -1,402 +0,0 @@ -# Phase 3: Build & Distribution - Completion Summary - -**Status**: ✅ WINDOWS BUILD COMPLETE | ✅ MACOS BUILD SCRIPT COMPLETE | ✅ DISTRIBUTION COMPLETE (untested on macOS) - ---- - -## What Was Implemented - -### 1. PyInstaller Specification File -**File**: `build/webdrop_bridge.spec` -- Cross-platform spec supporting Windows and macOS -- Uses `SPECPATH` variable for proper path resolution -- Bundles all dependencies: PySide6, Qt6 libraries, Chromium -- Includes data files: `webapp/`, `resources/` -- Configured for GUI mode (no console window) -- **Status**: ✅ Functional - -### 2. Windows Build Script -**File**: `build/scripts/build_windows.py` (315 lines) -- Encapsulated in `WindowsBuilder` class -- Methods: - - `clean()` - Remove previous builds - - `build_executable()` - Run PyInstaller - - `create_msi()` - WiX Toolset integration (optional) - - `sign_executable()` - Code signing (optional) -- CLI Arguments: - - `--msi` - Create MSI installer - - `--sign` - Sign executable -- Unicode emoji support (UTF-8 encoding for Windows console) -- **Status**: ✅ Tested & Working - -### 3. macOS Build Script -**File**: `build/scripts/build_macos.sh` (240+ lines) -- Creates .app bundle and DMG image -- Functions: - - `check_prerequisites()` - Verify required tools - - `clean_builds()` - Remove previous builds - - `build_executable()` - PyInstaller compilation - - `create_dmg()` - DMG image generation (professional or fallback) - - `sign_app()` - Code signing support - - `notarize_app()` - Apple notarization support -- Color-coded output for visibility -- Comprehensive error handling -- **Status**: ✅ Implemented (untested - requires macOS) - -### 4. Forgejo Release Scripts -**Files**: -- `build/scripts/create_release.ps1` - Windows release creation -- `build/scripts/create_release.sh` - macOS release creation -- `FORGEJO_PACKAGES_SETUP.md` - Distribution documentation - -**Features**: -- Automatic release creation via Forgejo Releases API -- HTTP Basic Auth (reuses git credentials) -- Interactive credential prompts with session persistence -- Automatic SHA256 checksum upload as release assets -- Cross-platform (Windows PowerShell 5.1 + macOS Bash) -- Curl-based file uploads (compatible with all environments) - -**Status**: ✅ Implemented & Tested -- First release (v0.0.2) successfully created and deployed -- Both remotes (Bitbucket + Forgejo) synchronized -- Ready for production use - -### 5. Documentation -**Files**: -- `resources/icons/README.md` - Icon requirements and specifications -- `FORGEJO_PACKAGES_SETUP.md` - Distribution workflow and integration -- `PHASE_3_BUILD_SUMMARY.md` - This file - -- **Status**: ✅ Complete - ---- - -## Build Results - -### Windows Executable (✅ Complete) - -``` -Build Output Directory: build/dist/windows/ -├── WebDropBridge.exe (195.66 MB) - Main executable -├── WebDropBridge.exe.sha256 - SHA256 checksum -└── WebDropBridge/ - Dependency directory - ├── PySide6/ (Qt6 libraries) - ├── python3.13.zip (Python runtime) - └── [other dependencies] -``` - -**Characteristics:** -- Standalone executable (no Python installation required on user's machine) -- Includes Chromium WebEngine (explains large file size) -- All dependencies bundled -- GUI application (runs without console window) -- Automatic SHA256 checksum generation -- Ready for distribution via Forgejo Releases - -**Verification:** -```bash -# File size -PS> Get-Item "build\dist\windows\WebDropBridge.exe" | - Select-Object Name, @{N='SizeMB';E={[math]::Round($_.Length/1MB,2)}} -# Result: WebDropBridge.exe (195.66 MB) - -# Checksum verification -PS> Get-Content "build\dist\windows\WebDropBridge.exe.sha256" -# Result: 2ddc507108209c70677db38a54bba82ef81d19d9890f8a0cb96270829dd5b6fa - -# Execution test -PS> .\build\dist\windows\WebDropBridge.exe --version -# Exit code: 0 ✅ -``` - -### macOS Application (✅ Build Script Complete) - -``` -Build Output Directory: build/dist/macos/ -├── WebDropBridge.app/ - Application bundle -│ └── Contents/ -│ ├── MacOS/WebDropBridge - Executable -│ ├── Resources/ - Assets & libraries -│ └── Info.plist - Bundle metadata -└── WebDropBridge.dmg - Distributable image -``` - -**Characteristics:** -- Native macOS .app bundle -- DMG image for distribution -- Checksum generation support -- Code signing support (requires developer certificate) -- Notarization support (requires Apple ID) -- **Status**: Script complete, untested (no macOS machine available) - -### Forgejo Releases (✅ Deployed) - -**Latest Release**: https://git.him-tools.de/HIM-public/webdrop-bridge/releases - -``` -v0.0.2 (Successfully created and deployed) -├── WebDropBridge.exe (195.66 MB) -├── WebDropBridge.exe.sha256 -└── [Additional assets for macOS when tested] -``` - -**Release Method**: -1. Build locally: `python build/scripts/build_windows.py` -2. Create release: `.\build\scripts\create_release.ps1 -Version 0.0.2` -3. Assets auto-uploaded: exe + checksum -4. Release visible on Forgejo within seconds - ---- - -## Next Steps - -### Immediate (Phase 3 Completion) - -1. ✅ **Windows Release Workflow** - COMPLETE - - Build executable with checksum - - Create release on Forgejo - - Upload assets (exe + checksum) - - Tested with v0.0.2 release - -2. ⏳ **macOS Release Workflow** - Script ready, untested - - Requires macOS machine to test - - Script `create_release.sh` ready to use - - Same workflow as Windows version - -3. ⏳ **Push Release Tags** (Optional but recommended) - ```bash - git tag -a v0.0.2 -m "Release 0.0.2" - git push upstream v0.0.2 - ``` - -### Phase 4.1: Auto-Update System (Next Phase) - -The release infrastructure is now ready for Phase 4.1 implementation: - -1. **UpdateManager Design** - - Query Forgejo Releases API: `GET /api/v1/repos/HIM-public/webdrop-bridge/releases/latest` - - Parse release assets (exe + checksum) - - Download latest executable - - Verify SHA256 checksum - - Replace current executable - - Restart application - -2. **Example Integration Code** - ```python - from src.webdrop_bridge.core.update_manager import UpdateManager - - manager = UpdateManager( - repo_url="https://git.him-tools.de/HIM-public/webdrop-bridge", - current_version="0.0.2" - ) - - if manager.update_available(): - manager.download_and_install() - manager.restart_app() - ``` - -3. **Forgejo API Endpoint** - ``` - GET https://git.him-tools.de/api/v1/repos/HIM-public/webdrop-bridge/releases/latest - - Response: - { - "id": 1, - "tag_name": "v0.0.2", - "name": "Release 0.0.2", - "body": "...", - "assets": [ - { - "id": 1, - "name": "WebDropBridge.exe", - "browser_download_url": "https://git.him-tools.de/..." - }, - { - "id": 2, - "name": "WebDropBridge.exe.sha256", - "browser_download_url": "https://git.him-tools.de/..." - } - ] - } - ``` - # - Settings accessible - # - Drag-and-drop works - ``` - -2. **macOS Build Testing** (requires macOS machine) - ```bash - bash build/scripts/build_macos.sh - # Should create: build/dist/macos/WebDropBridge.dmg - ``` - -3. **Optional: Create MSI Installer** - ```bash - # Install WiX Toolset first - python build/scripts/build_windows.py --msi - # Output: WebDropBridge-Setup.exe - ``` - -### Deferred Tasks - -4. **GitHub Actions CI/CD Pipeline** (`.github/workflows/build.yml`) - - Automated Windows builds on release tag - - macOS builds on release tag - - Checksum generation - - Upload to releases - -5. **Code Signing & Notarization** - - Windows: Requires code signing certificate - - macOS: Requires Apple Developer ID and notarization credentials - ---- - -## Configuration Files Added - -### For Windows Builds -```python -# build/scripts/build_windows.py -class WindowsBuilder: - def __init__(self, project_root: Path): - self.project_root = project_root - self.build_dir = project_root / "build" - ... -``` - -### For macOS Builds -```bash -# build/scripts/build_macos.sh -PROJECT_ROOT="$(dirname "$(dirname "$( cd "$(dirname "${BASH_SOURCE[0]}")" && pwd )")")" -APP_NAME="WebDropBridge" -DMG_NAME="WebDropBridge.dmg" -``` - -### PyInstaller Configuration -```python -# build/webdrop_bridge.spec -SPECPATH = os.path.dirname(os.path.abspath(spec_file)) -project_root = os.path.dirname(SPECPATH) - -a = Analysis( - [os.path.join(project_root, 'src/webdrop_bridge/main.py')], - ... - datas=[ - (os.path.join(project_root, 'webapp'), 'webapp'), - (os.path.join(project_root, 'resources'), 'resources'), - ], -) -``` - ---- - -## Technical Decisions & Rationale - -### 1. PyInstaller Spec File (Not CLI Arguments) -- **Decision**: Use .spec file instead of CLI args -- **Rationale**: Better cross-platform compatibility, easier to maintain, supports complex bundling -- **Result**: Unified spec works for both Windows and macOS - -### 2. Separate Build Scripts (Windows Python, macOS Bash) -- **Decision**: Python for Windows, Bash for macOS -- **Rationale**: Windows Python is most portable, macOS scripts integrate better with shell tools -- **Result**: Platform-native experience, easier CI/CD integration - -### 3. Large Executable Size (195.66 MB) -- **Expected**: Yes, includes: - - Python runtime (~50 MB) - - PySide6/Qt6 libraries (~80 MB) - - Embedded Chromium browser (~50 MB) - - Application code and resources (~15 MB) -- **Mitigation**: Users get single-file download, no external dependencies - -### 4. Cross-Platform Data File Bundling -- **Decision**: Include webapp/ and resources/ in executables -- **Rationale**: Self-contained distribution, no external file dependencies -- **Result**: Users can place executable anywhere, always works - ---- - -## Known Limitations & Future Work - -### Windows -- [ ] MSI installer requires WiX Toolset installation on build machine -- [ ] Code signing requires code signing certificate -- [ ] No automatic updater yet (Phase 4.1) - -### macOS -- [ ] build_macos.sh script is implemented but untested (no macOS machine in workflow) -- [ ] Code signing requires macOS machine and certificate -- [ ] Notarization requires Apple Developer account -- [ ] Professional DMG requires create-dmg tool installation - -### General -- [ ] CI/CD pipeline not yet implemented -- [ ] Auto-update system not yet implemented (Phase 4.1) -- [ ] Icon files not yet created (resources/icons/app.ico, app.icns) - ---- - -## How to Use These Build Scripts - -### Quick Start - -```bash -# Windows only - build executable -cd "c:\Development\VS Code Projects\webdrop_bridge" -python build/scripts/build_windows.py - -# Windows - create MSI (requires WiX) -python build/scripts/build_windows.py --msi - -# macOS only - create .app and DMG -bash build/scripts/build_macos.sh - -# macOS - with code signing -bash build/scripts/build_macos.sh --sign -``` - -### Output Locations - -Windows: -- Executable: `build/dist/windows/WebDropBridge.exe` -- MSI: `build/dist/windows/WebDropBridge-Setup.exe` (if --msi used) - -macOS: -- App Bundle: `build/dist/macos/WebDropBridge.app` -- DMG: `build/dist/macos/WebDropBridge.dmg` - ---- - -## Environment Setup - -### Windows Build Machine -```powershell -# Install PyInstaller (already in requirements-dev.txt) -pip install pyinstaller - -# Optional: Install WiX for MSI creation -# Download from: https://github.com/wixtoolset/wix3/releases -# Or: choco install wixtoolset -``` - -### macOS Build Machine -```bash -# PyInstaller is in requirements-dev.txt -pip install pyinstaller - -# Optional: Install create-dmg for professional DMG -brew install create-dmg - -# For code signing and notarization: -# - macOS Developer Certificate (in Keychain) -# - Apple ID + app-specific password -# - Team ID -``` - ---- - -## Version: 1.0.0 - -**Build Date**: January 2026 -**Built With**: PyInstaller 6.18.0, PySide6 6.10.1, Python 3.13.11 - diff --git a/PHASE_4_3_SUMMARY.md b/PHASE_4_3_SUMMARY.md deleted file mode 100644 index 03d0268..0000000 --- a/PHASE_4_3_SUMMARY.md +++ /dev/null @@ -1,193 +0,0 @@ -"""Phase 4.3 Advanced Configuration - Summary Report - -## Overview -Phase 4.3 (Advanced Configuration) has been successfully completed with comprehensive -configuration management, validation, profile support, and settings UI. - -## Files Created - -### Core Implementation -1. src/webdrop_bridge/core/config_manager.py (263 lines) - - ConfigValidator: Schema-based validation with helpful error messages - - ConfigProfile: Named profile management in ~/.webdrop-bridge/profiles/ - - ConfigExporter: JSON import/export with validation - -2. src/webdrop_bridge/ui/settings_dialog.py (437 lines) - - SettingsDialog: Professional Qt dialog with 5 tabs - - Paths Tab: Manage allowed root directories - - URLs Tab: Manage allowed web URLs - - Logging Tab: Configure log level and file - - Window Tab: Manage window dimensions - - Profiles Tab: Save/load/delete profiles, export/import - -### Test Files -1. tests/unit/test_config_manager.py (264 lines) - - 20 comprehensive tests - - 87% coverage on config_manager module - - Tests for validation, profiles, export/import - -2. tests/unit/test_settings_dialog.py (296 lines) - - 23 comprehensive tests - - 75% coverage on settings_dialog module - - Tests for UI initialization, data retrieval, config application - -## Test Results - -### Config Manager Tests (20/20 passing) -- TestConfigValidator: 8 tests - * Valid config validation - * Missing required fields - * Invalid types - * Invalid log levels - * Out of range values - * validate_or_raise functionality - -- TestConfigProfile: 7 tests - * Save/load profiles - * List profiles - * Delete profiles - * Invalid profile names - * Nonexistent profiles - -- TestConfigExporter: 5 tests - * Export to JSON - * Import from JSON - * Nonexistent files - * Invalid JSON - * Invalid config detection - -### Settings Dialog Tests (23/23 passing) -- TestSettingsDialogInitialization: 7 tests - * Dialog creation - * Tab structure - * All 5 tabs present (Paths, URLs, Logging, Window, Profiles) - -- TestPathsTab: 2 tests - * Paths loaded from config - * Add button exists - -- TestURLsTab: 1 test - * URLs loaded from config - -- TestLoggingTab: 2 tests - * Log level set from config - * All log levels available - -- TestWindowTab: 4 tests - * Window dimensions set from config - * Min/max constraints - -- TestProfilesTab: 1 test - * Profiles list initialized - -- TestConfigDataRetrieval: 3 tests - * Get config data from dialog - * Config data validation - * Modified values preserved - -- TestApplyConfigData: 3 tests - * Apply paths - * Apply URLs - * Apply window size - -## Key Features - -### ConfigValidator -- Comprehensive schema definition -- Type validation (str, int, bool, list, Path) -- Value constraints (min/max, allowed values, length) -- Detailed error messages -- Reusable for all configuration validation - -### ConfigProfile -- Save configurations as named profiles -- Profile storage: ~/.webdrop-bridge/profiles/ -- JSON serialization with validation -- List/load/delete profile operations -- Error handling for invalid names and I/O failures - -### ConfigExporter -- Export current configuration to JSON file -- Import and validate JSON configurations -- Handles file I/O errors -- All imports validated before return - -### SettingsDialog -- Professional Qt QDialog with tabbed interface -- Load config on initialization -- Save modifications as profiles or export -- Import configurations from files -- All settings integrated with validation -- User-friendly error dialogs - -## Code Quality - -### Validation -- All validation centralized in ConfigValidator -- Schema-driven approach enables consistency -- Detailed error messages guide users -- Type hints throughout - -### Testing -- 43 comprehensive unit tests (100% passing) -- 87% coverage on config_manager -- 75% coverage on settings_dialog -- Tests cover normal operations and error conditions - -### Documentation -- Module docstrings for all classes -- Method docstrings with Args/Returns/Raises -- Schema definition documented in code -- Example usage in tests - -## Integration Points - -### With MainWindow -- Settings menu item can launch SettingsDialog -- Dialog returns validated configuration dict -- Changes can be applied on OK - -### With Configuration System -- ConfigValidator used to ensure all configs valid -- ConfigProfile integrates with ~/.webdrop-bridge/ -- Export/import uses standard JSON format - -### With Logging -- Log level changes apply through SettingsDialog -- Profiles can include different logging configs - -## Phase 4.3 Completion Summary - -✅ All 4 Deliverables Implemented: -1. UI Settings Dialog - SettingsDialog with 5 organized tabs -2. Validation Schema - ConfigValidator with comprehensive checks -3. Profile Support - ConfigProfile for named configurations -4. Export/Import - ConfigExporter for JSON serialization - -✅ Test Coverage: 43 tests passing (87-75% coverage) - -✅ Code Quality: -- Type hints throughout -- Comprehensive docstrings -- Error handling -- Validation at all levels - -✅ Ready for Phase 4.4 (User Documentation) - -## Next Steps - -1. Phase 4.4: User Documentation - - User manual for configuration system - - Video tutorials for settings dialog - - Troubleshooting guide - -2. Phase 5: Post-Release - - Analytics integration - - Enhanced monitoring - - Community support - ---- - -Report Generated: January 29, 2026 -Phase 4.3 Status: ✅ COMPLETE -""" \ No newline at end of file diff --git a/PROJECT_SETUP_SUMMARY.md b/PROJECT_SETUP_SUMMARY.md deleted file mode 100644 index 6b3ab5b..0000000 --- a/PROJECT_SETUP_SUMMARY.md +++ /dev/null @@ -1,405 +0,0 @@ -# Project Setup Summary - -## ✅ Completion Status - -The **WebDrop Bridge** professional project has been successfully created and is ready for development. - -### What Was Created - -#### 1. **Project Structure** ✅ -- Modular architecture: `src/webdrop_bridge/` (core/, ui/, utils/) -- Comprehensive test suite: `tests/` (unit, integration, fixtures) -- Build automation: `build/` (windows, macos, scripts) -- Professional documentation: `docs/` -- Embedded web app: `webapp/` - -#### 2. **Configuration Files** ✅ -| File | Purpose | -|------|---------| -| `pyproject.toml` | Modern Python packaging (PEP 517/518) | -| `setup.py` | Backwards compatibility | -| `pytest.ini` | Test configuration | -| `tox.ini` | Test automation (lint, type, test, docs) | -| `requirements.txt` | Production dependencies | -| `requirements-dev.txt` | Development dependencies | -| `.env.example` | Environment configuration template | -| `.gitignore` | Git ignore rules | - -#### 3. **CI/CD Pipeline** ✅ -| File | Purpose | -|------|---------| -| `.github/workflows/tests.yml` | GitHub Actions: test & build on all platforms | -| `build/scripts/build_windows.py` | Windows MSI builder | -| `build/scripts/build_macos.sh` | macOS DMG builder | - -#### 4. **Documentation** ✅ -| File | Purpose | -|------|---------| -| `README.md` | User-facing documentation | -| `DEVELOPMENT_PLAN.md` | 12-week development roadmap (5000+ lines) | -| `CONTRIBUTING.md` | Contributor guidelines | -| `QUICKSTART.md` | Quick start guide (5 min setup) | -| `docs/ARCHITECTURE.md` | Technical architecture & design | -| `.github/copilot-instructions.md` | AI assistant guidelines | -| `LICENSE` | MIT License | - -#### 5. **Development Tools** ✅ -| File | Purpose | -|------|---------| -| `Makefile` | Convenience commands for common tasks | -| `.vscode/settings.json` | VS Code workspace settings | -| `.vscode/launch.json` | Debugger configurations | -| `.vscode/tasks.json` | Test/build tasks | -| `webdrop_bridge.code-workspace` | VS Code workspace file | - -#### 6. **Sample Code & Tests** ✅ -| File | Purpose | -|------|---------| -| `src/webdrop_bridge/__init__.py` | Package initialization | -| `src/webdrop_bridge/core/__init__.py` | Core module | -| `src/webdrop_bridge/ui/__init__.py` | UI module | -| `src/webdrop_bridge/utils/__init__.py` | Utils module | -| `tests/conftest.py` | Pytest fixtures | -| `tests/unit/test_project_structure.py` | Structure validation tests | -| `webapp/index.html` | Beautiful test drag-drop web app | - ---- - -## 📊 Project Statistics - -``` -Total Files Created: 45+ -Total Lines of Code: 5000+ -Documentation: 3000+ lines -Test Suite: Ready for unit/integration tests -Build Scripts: Windows & macOS -CI/CD Workflows: Automated testing & building -``` - -## Statistics - -- Source files: 6 -- Test files: 5 -- Documentation files: 9 - ---- - -## 🚀 Quick Start - -### 1. Open Project - -```bash -# Option A: Using workspace file -code webdrop_bridge.code-workspace - -# Option B: Using folder -code . -``` - -### 2. Setup Environment (30 seconds) - -```bash -python -m venv venv -source venv/bin/activate # macOS/Linux -# venv\Scripts\activate # Windows - -pip install -r requirements-dev.txt -``` - -### 3. Verify Setup - -```bash -pytest tests/unit/test_project_structure.py -v -``` - -All tests should pass ✅ - -### 4. Read Documentation - -- **For overview**: → `README.md` -- **For roadmap**: → `DEVELOPMENT_PLAN.md` -- **For quick start**: → `QUICKSTART.md` -- **For architecture**: → `docs/ARCHITECTURE.md` -- **For contributing**: → `CONTRIBUTING.md` - ---- - -## 📋 Key Differences: PoC vs. Production - -| Aspect | PoC | Production | -|--------|-----|-----------| -| **Structure** | Monolithic (1 file) | Modular (core, ui, utils) | -| **Configuration** | Hardcoded | Environment-based (.env) | -| **Logging** | Console only | File + console + structured | -| **Testing** | Ad-hoc | Comprehensive (unit + integration) | -| **Error Handling** | Basic try/catch | Robust with custom exceptions | -| **Documentation** | Minimal | Extensive (2000+ lines) | -| **Build System** | Manual | Automated (PyInstaller, CI/CD) | -| **Code Quality** | Not checked | Enforced (Black, Ruff, mypy) | -| **Distribution** | Source code | MSI (Windows), DMG (macOS) | -| **Version Control** | None | Full git workflow | - ---- - -## 📍 Development Roadmap - -### Phase 1: Foundation (Weeks 1-4) - **NEXT** -- [ ] Config system -- [ ] Path validator -- [ ] Drag interceptor -- [ ] Main window -- [ ] Entry point - -### Phase 2: Testing & Quality (Weeks 5-6) -- [ ] Unit tests (80%+ coverage) -- [ ] Integration tests -- [ ] Code quality checks -- [ ] Security audit - -### Phase 3: Build & Distribution (Weeks 7-8) -- [ ] Windows MSI installer -- [ ] macOS DMG package -- [ ] Installer testing - -### Phase 4: Professional Features (Weeks 9-12) -- [ ] Enhanced logging -- [ ] User documentation -- [ ] Advanced configuration -- [ ] Release packaging - -### Phase 5: Post-Release (Months 2-3) -- [ ] Auto-update system -- [ ] Analytics & monitoring -- [ ] Community support - -See `DEVELOPMENT_PLAN.md` for detailed specifications. - ---- - -## 🛠️ Common Commands - -### Setup & Installation -```bash -make install # Production only -make install-dev # With dev tools -``` - -### Testing -```bash -make test # All tests + coverage -make test-quick # Fast run -make test-unit # Unit tests -``` - -### Code Quality -```bash -make lint # Check style -make format # Auto-fix style -make type # Type checking -make quality # All checks -``` - -### Building -```bash -make build-windows # Build MSI -make build-macos # Build DMG -make clean # Remove build files -``` - -### Documentation -```bash -make docs # Build docs -make help # Show all commands -``` - ---- - -## 🏗️ Architecture Highlights - -### Modular Design -- **Core** (validator, drag interceptor) - Business logic -- **UI** (main window, widgets) - Presentation -- **Utils** (logging, helpers) - Shared utilities - -### Security -- Whitelist-based path validation -- Absolute path resolution -- Symlink handling -- Web engine sandboxing - -### Cross-Platform -- Windows 10/11 (x64) -- macOS 12-14 (Intel & ARM64) -- Linux (experimental) - -### Performance -- Drag interception: <10ms -- Application startup: <1 second -- Memory baseline: <200MB - ---- - -## 📚 Documentation Map - -``` -QUICKSTART.md ← Start here (5-minute setup) - ↓ -README.md ← User documentation - ↓ -DEVELOPMENT_PLAN.md ← Detailed roadmap (12+ weeks) - ↓ -docs/ARCHITECTURE.md ← Technical deep-dive - ↓ -CONTRIBUTING.md ← How to contribute - ↓ -Code ← Docstrings in source -``` - ---- - -## ✨ Special Features - -### 1. **Comprehensive Testing** -- Unit test fixtures -- Integration test examples -- Cross-platform markers -- Coverage reporting - -### 2. **Automated Quality** -- Black (auto-formatting) -- Ruff (linting) -- mypy (type checking) -- pytest (testing) - -### 3. **Professional Build System** -- PyInstaller (Windows & macOS) -- GitHub Actions CI/CD -- Automated testing matrix -- Artifact generation - -### 4. **Developer Experience** -- VS Code integration -- Makefile shortcuts -- Pre-configured launch configs -- Task automation - -### 5. **Production Ready** -- Semantic versioning -- Environment configuration -- Structured logging -- Error handling - ---- - -## 🔐 Security Considerations - -✅ **Implemented:** -- Whitelist-based path validation -- Absolute path resolution -- Web engine sandboxing -- No remote file access by default -- Environment-based secrets - -📋 **To Implement (Phase 4):** -- Path size limits -- Rate limiting for drags -- Audit logging -- Encrypted settings storage - ---- - -## 📦 Dependencies - -### Core -- Python 3.10+ -- PySide6 6.6.0+ -- PyYAML -- python-dotenv - -### Development -- pytest + plugins -- black, ruff, mypy -- sphinx (docs) -- pyinstaller (builds) - -### CI/CD -- GitHub Actions -- Python matrix testing - -All dependencies are locked in: -- `pyproject.toml` - Version specifications -- `requirements*.txt` - Exact versions for reproducibility - ---- - -## 🎯 Success Criteria - -- ✅ Project structure created -- ✅ Configuration system designed -- ✅ Test framework set up -- ✅ Build automation scripted -- ✅ Documentation complete -- ✅ CI/CD configured -- ✅ Development plan detailed -- ✅ Ready for Phase 1 implementation - ---- - -## 📞 Next Actions - -1. **Review** `QUICKSTART.md` (5 minutes) -2. **Read** `DEVELOPMENT_PLAN.md` Phase 1 (15 minutes) -3. **Study** `docs/ARCHITECTURE.md` (20 minutes) -4. **Setup** environment (see above) -5. **Start** implementing Phase 1 components - ---- - -## 📝 File Count - -| Category | Count | -|----------|-------| -| Configuration | 12 | -| Source Code | 8 | -| Tests | 5 | -| Documentation | 7 | -| Build/CI | 4 | -| Resources | 2 | -| VS Code Config | 3 | -| **Total** | **41** | - ---- - -## 🎓 Learning Resources - -- PySide6 Documentation: https://doc.qt.io/qtforpython/ -- Qt Architecture: https://doc.qt.io/qt-6/ -- pytest Guide: https://docs.pytest.org/ -- GitHub Actions: https://docs.github.com/actions - ---- - -## 📄 Document Versions - -| Document | Version | Updated | -|----------|---------|---------| -| DEVELOPMENT_PLAN.md | 1.0 | Jan 2026 | -| README.md | 1.0 | Jan 2026 | -| CONTRIBUTING.md | 1.0 | Jan 2026 | -| docs/ARCHITECTURE.md | 1.0 | Jan 2026 | - ---- - -## Status - -- Auto-update system: Implemented -- Integration tests: Implemented (`test_update_flow.py`) -- Documentation: Updated and verified - -**Status**: ✅ Project Ready for Development -**Next Phase**: Implement Core Components (Phase 1) -**Timeline**: 12 weeks to complete all phases - ---- - -*For questions or clarifications, refer to the documentation or open an issue on GitHub.* diff --git a/QUICKSTART_DEBUGGING.md b/QUICKSTART_DEBUGGING.md deleted file mode 100644 index ffdc572..0000000 --- a/QUICKSTART_DEBUGGING.md +++ /dev/null @@ -1,222 +0,0 @@ -# 🚀 QUICK START - Popup-Problem lösen - -## Problem Zusammenfassung - -- ✅ File-Drop funktioniert (Z:\ Laufwerk) -- ❌ Web-App Popup erscheint nicht nach Drop (Auschecken-Dialog) - -**Grund**: Unser JavaScript verhindert das Browser-Drag-Event mit `preventDefault()`, daher bekommt die Web-App kein Drop-Event. - -## 🎯 Lösungsstrategie - -**Phase 1: DEBUGGING** (ca. 15-30 Min) -→ Herausfinden WIE das Popup ausgelöst wird - -**Phase 2: IMPLEMENTATION** (ca. 30-60 Min) -→ Popup-Trigger nach File-Drop manuell aufrufen - -## 📋 Phase 1: Debugging - JETZT STARTEN - -### Schritt 1: Debug-Script aktivieren - -```powershell -# Datei öffnen -code "C:\Development\VS Code Projects\webdrop_bridge\src\webdrop_bridge\ui\main_window.py" -``` - -**Zeile ~433** in `_install_bridge_script()` ändern: - -```python -# VORHER: -script_path = Path(__file__).parent / "bridge_script.js" - -# NACHHER (für Debugging): -script_path = Path(__file__).parent / "bridge_script_debug.js" -``` - -Speichern (Ctrl+S). - -### Schritt 2: Anwendung starten - -```powershell -cd "C:\Development\VS Code Projects\webdrop_bridge" -python -m webdrop_bridge.main -``` - -### Schritt 3: Browser DevTools öffnen - -1. Wenn WebDrop Bridge lädt -2. **F12** drücken → DevTools öffnen -3. **Console-Tab** auswählen -4. Sie sollten sehen: - `[WebDrop DEBUG] Ready! Perform ALT-drag+drop and watch the logs.` - -### Schritt 4: ALT-Drag+Drop durchführen - -1. In der GlobalDAM Anwendung: - - **ALT-Taste** gedrückt halten - - Asset **draggen** - - Irgendwo **droppen** (z.B. auf dem Desktop) - -2. **Popup sollte erscheinen!** (Auschecken-Ja/Nein) - -3. **Sofort** zur Browser-Console wechseln! - -### Schritt 5: Logs analysieren - -Suchen Sie in der Console nach: - -#### ✅ **A) Modal/Popup Detektion** -``` -[MODAL OPENED] <<<< DAS WOLLEN WIR SEHEN! -Modal element:
    -Classes: modal-dialog checkout-dialog -``` - -**Wenn gefunden →** Popup wird durch DOM-Manipulation ausgelöst -**Merkblatt:** Notieren Sie die `className` und `textContent` - -#### ✅ **B) API-Call Detection** -``` -[FETCH] https://dev.agravity.io/api/assets/abc123/checkout -``` -oder -``` -[XHR] POST https://dev.agravity.io/api/assets/abc123/checkout -``` - -**Wenn gefunden →** Popup wird durch API-Response ausgelöst -**Merkblatt:** Notieren Sie die vollständige URL und Methode (POST/GET) - -#### ✅ **C) Event Detection** -``` -[EVENT] DROP -Target: ... -DataTransfer: ... -``` - -**Merkblatt:** Notieren Sie welche Events feuern NACHDEM Drop passiert - -### Schritt 6: Detailanalyse - -In der Browser Console, führen Sie aus: - -```javascript -// Event-Statistik -webdrop_debug.getEventCounts() - -// Asset-Card Component untersuchen (wenn Angular DevTools installiert) -var card = document.querySelector('ay-asset-card'); -var component = webdrop_debug.getComponent(card); -console.log('Component methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(component))); -``` - -**Merkblatt:** Notieren Sie: -- Welche Methoden hat das Component? (z.B. `onCheckout`, `showDialog`, etc.) -- Gibt es einen `click`-Handler auf dem Asset? - -## 📝 Was Sie herausfinden müssen - -Nach dem Debugging sollten Sie wissen: - -1. **WIE wird Popup ausgelöst?** - - [ ] API-Call nach Drop (URL: ________________________) - - [ ] DOM-Element wird erstellt (Class: ________________) - - [ ] Angular Component Methode (Name: ________________) - - [ ] Click-Event auf Button (Selector: _______________) - -2. **WANN wird Popup ausgelöst?** - - [ ] Sofort nach Drop - - [ ] Nach Verzögerung (_____ Sekunden) - - [ ] Nach API-Response - - [ ] Nach User-Interaction - -3. **WELCHE Daten werden benötigt?** - - [ ] Asset-ID (Beispiel: _________________________) - - [ ] Collection-ID (Beispiel: ______________________) - - [ ] User-ID (Beispiel: ___________________________) - - [ ] Weitere: ____________________________________ - -## 🔧 Phase 2: Implementation (NACHDEM Debugging abgeschlossen) - -Basierend auf Ihren Erkenntnissen: - -### Fall A: Popup durch API-Call - -**In `bridge_script.js` hinzufügen** (nach erfolgreichem File-Drop): - -```javascript -// Nach Qt File-Drop Erfolg -function triggerCheckoutPopup(assetId) { - fetch('https://dev.agravity.io/api/assets/' + assetId + '/checkout', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - // ggf. Authorization Header - }, - body: JSON.stringify({ - // Parameter basierend auf Debug-Logs - }) - }) - .then(response => response.json()) - .then(data => { - console.log('Checkout popup triggered via API'); - }); -} -``` - -### Fall B: Popup durch Component-Methode - -```javascript -function triggerCheckoutPopup(assetId) { - var card = document.getElementById(assetId); - if (card && window.ng) { - var component = ng.getComponent(card); - if (component && component.onCheckout) { - component.onCheckout(); - } - } -} -``` - -### Fall C: Popup durch Click-Simulation - -```javascript -function triggerCheckoutPopup(assetId) { - var button = document.querySelector('[data-action="checkout"]'); - if (button) { - button.click(); - } -} -``` - -## ⚡ Troubleshooting - -**Problem:** Kein `[MODAL OPENED]` Log -**Lösung:** Popup wird möglicherweise anders erstellt. Suchen Sie in Network-Tab nach API-Calls - -**Problem:** Zu viele Logs -**Lösung:** Console Filter nutzen: `[MODAL]` oder `[FETCH]` - -**Problem:** `webdrop_debug` not defined -**Lösung:** Debug-Script wurde nicht richtig geladen. main_window.py prüfen. - -**Problem:** Angular DevTools fehlt -**Lösung:** Chrome Extension installieren: "Angular DevTools" - -## 📞 Nächste Schritte - -1. ✅ Debug-Script aktivieren (oben Schritt 1) -2. ✅ Logs sammeln (Schritt 3-6) -3. ✅ Ergebnisse notieren (Merkblatt) -4. → **Ergebnisse mitteilen** → Ich helfe bei Implementation! - -## 📄 Zusätzliche Dokumentation - -- [DRAG_DROP_PROBLEM_ANALYSIS.md](DRAG_DROP_PROBLEM_ANALYSIS.md) - Detaillierte Problem-Analyse -- [SCRIPT_VARIANTS.md](SCRIPT_VARIANTS.md) - Alle verfügbaren Scripts -- [ANGULAR_CDK_ANALYSIS.md](ANGULAR_CDK_ANALYSIS.md) - Angular Framework Details - ---- - -**Hinweis:** Das Debug-Script hat Performance-Overhead. Nach dem Debugging zurück zu `bridge_script.js` wechseln! diff --git a/UPDATE_FIX_SUMMARY.md b/UPDATE_FIX_SUMMARY.md deleted file mode 100644 index ef1925b..0000000 --- a/UPDATE_FIX_SUMMARY.md +++ /dev/null @@ -1,80 +0,0 @@ -# Update Feature Fixes - Final Summary - -## Problem Identified -The update feature was causing the application to hang indefinitely when clicked. The issue had two components: - -1. **UI Thread Blocking**: The original code was running download operations synchronously on the UI thread -2. **Network Timeout Issues**: Even with timeouts set, the socket-level network calls would hang indefinitely if the server didn't respond - -## Solutions Implemented - -### 1. Background Threading (First Fix) -- Created `UpdateDownloadWorker` class to run download operations in a background thread -- Moved blocking network calls off the UI thread -- This prevents the UI from freezing while waiting for network operations - -### 2. Aggressive Timeout Strategy (Second Fix) -Applied timeouts at multiple levels to ensure the app never hangs: - -#### A. Socket-Level Timeout (Most Important) -- **File**: `src/webdrop_bridge/core/updater.py` -- Reduced `urlopen()` timeout from 10 seconds to **5 seconds** -- This is the first line of defense against hanging socket connections -- Applied in `_fetch_release()` method - -#### B. Asyncio-Level Timeout -- **File**: `src/webdrop_bridge/ui/main_window.py` and `src/webdrop_bridge/core/updater.py` -- `UpdateCheckWorker`: 10-second timeout on entire check operation -- `UpdateDownloadWorker`: 300-second timeout on download, 30-second on verification -- `check_for_updates()`: 8-second timeout on async executor -- These catch any remaining hangs in the asyncio operations - -#### C. Qt-Level Timeout (Final Safety Net) -- **File**: `src/webdrop_bridge/ui/main_window.py` -- Update check: **30-second QTimer** safety timeout (`_run_async_check()`) -- Download: **10-minute QTimer** safety timeout (`_perform_update_async()`) -- If nothing else works, Qt's event loop will forcefully close the operation - -### 3. Error Handling Improvements -- Added proper exception handling for `asyncio.TimeoutError` -- Better logging to identify where hangs occur -- User-friendly error messages like "no server response" or "Operation timed out" -- Graceful degradation: operations fail fast instead of hanging - -## Timeout Hierarchy (in seconds) -``` -Update Check Flow: - QTimer safety net: 30s ─┐ - ├─ Asyncio timeout: 10s ─┐ - ├─ Socket timeout: 5s (first to trigger) -Download Flow: - QTimer safety net: 600s ─┐ - ├─ Asyncio timeout: 300s ─┐ - ├─ Socket timeout: 5s (first to trigger) -``` - -## Files Modified -1. **src/webdrop_bridge/ui/main_window.py** - - Updated `UpdateCheckWorker.run()` with timeout handling - - Updated `UpdateDownloadWorker.run()` with timeout handling - - Added QTimer safety timeouts in `_run_async_check()` and `_perform_update_async()` - - Proper event loop cleanup in finally blocks - -2. **src/webdrop_bridge/core/updater.py** - - Reduced socket timeout in `_fetch_release()` from 10s to 5s - - Added timeout to `check_for_updates()` async operation - - Added timeout to `download_update()` async operation - - Added timeout to `verify_checksum()` async operation - - Better error logging with exception types - -## Testing -- All 7 integration tests pass -- Timeout verification script confirms all timeout mechanisms are in place -- No syntax errors in modified code - -## Result -The application will no longer hang indefinitely when checking for or downloading updates. Instead: -- Operations timeout quickly (5-30 seconds depending on operation type) -- User gets clear feedback about what went wrong -- User can retry or cancel without force-killing the app -- Background threads are properly cleaned up to avoid resource leaks diff --git a/VERSIONING_SIMPLIFIED.md b/VERSIONING_SIMPLIFIED.md deleted file mode 100644 index 5282cb5..0000000 --- a/VERSIONING_SIMPLIFIED.md +++ /dev/null @@ -1,140 +0,0 @@ -# Simplified Versioning System - -## Problem Solved - -Previously, the application version had to be manually updated in **multiple places**: -1. `src/webdrop_bridge/__init__.py` - source of truth -2. `pyproject.toml` - package version -3. `.env.example` - environment example -4. Run `scripts/sync_version.py` - manual sync step - -This was error-prone and tedious. - -## Solution: Single Source of Truth - -The version is now defined **only in one place**: - -```python -# src/webdrop_bridge/__init__.py -__version__ = "1.0.0" -``` - -All other components automatically read from this single source. - -## How It Works - -### 1. **pyproject.toml** (Automatic) -```toml -[tool.setuptools.dynamic] -version = {attr = "webdrop_bridge.__version__"} - -[project] -name = "webdrop-bridge" -dynamic = ["version"] # Reads from __init__.py -``` - -When you build the package, setuptools automatically extracts the version from `__init__.py`. - -### 2. **config.py** (Automatic - with ENV override) -```python -# Lazy import to avoid circular imports -if not os.getenv("APP_VERSION"): - from webdrop_bridge import __version__ - app_version = __version__ -else: - app_version = os.getenv("APP_VERSION") -``` - -The config automatically reads from `__init__.py`, but can be overridden with the `APP_VERSION` environment variable if needed. - -### 3. **sync_version.py** (Simplified) -The script now only handles: -- Updating `__init__.py` with a new version -- Updating `CHANGELOG.md` with a new version header -- Optional: updating `.env.example` if it explicitly sets `APP_VERSION` - -It **no longer** needs to manually sync pyproject.toml or config defaults. - -## Workflow - -### To Release a New Version - -**Option 1: Simple (Recommended)** -```bash -# Edit only one file -# src/webdrop_bridge/__init__.py: -__version__ = "1.1.0" # Change this - -# Then run sync script to update changelog -python scripts/sync_version.py -``` - -**Option 2: Using the Sync Script** -```bash -python scripts/sync_version.py --version 1.1.0 -``` - -The script will: -- ✅ Update `__init__.py` -- ✅ Update `CHANGELOG.md` -- ✅ (Optional) Update `.env.example` if it has `APP_VERSION=` - -### What Happens Automatically - -When you run your application: -1. Config loads and checks environment for `APP_VERSION` -2. If not set, it imports `__version__` from `__init__.py` -3. The version is displayed in the UI -4. Update checks use the correct version - -When you build with `pip install`: -1. setuptools reads `__version__` from `__init__.py` -2. Package metadata is set automatically -3. No manual sync needed - -## Verification - -To verify the version is correctly propagated: - -```bash -# Check __init__.py -python -c "from webdrop_bridge import __version__; print(__version__)" - -# Check config loading -python -c "from webdrop_bridge.config import Config; c = Config.from_env(); print(c.app_version)" - -# Check package metadata (after building) -pip show webdrop-bridge -``` - -All should show the same version. - -## Best Practices - -1. **Always edit `__init__.py` first** - it's the single source of truth -2. **Run `sync_version.py` to update changelog** - keeps release notes organized -3. **Use environment variables only for testing** - don't hardcode overrides -4. **Run tests after version changes** - config tests verify version loading - -## Migration Notes - -If you had other places where version was defined: -- ❌ Remove version from `pyproject.toml` `[project]` section -- ✅ Add `dynamic = ["version"]` instead -- ❌ Don't manually edit `.env.example` for version -- ✅ Let `sync_version.py` handle it -- ❌ Don't hardcode version in config.py defaults -- ✅ Use lazy import from `__init__.py` - -## Testing the System - -Run the config tests to verify everything works: -```bash -pytest tests/unit/test_config.py -v -``` - -All tests should pass, confirming version loading works correctly. - ---- - -**Result**: One place to change, multiple places automatically updated. Simple, clean, professional. diff --git a/WEBAPP_LOADING_FIX.md b/WEBAPP_LOADING_FIX.md deleted file mode 100644 index 7711867..0000000 --- a/WEBAPP_LOADING_FIX.md +++ /dev/null @@ -1,148 +0,0 @@ -# WebApp Loading - Issue & Fix Summary - -## Problem - -When running the Windows executable, the embedded web view displayed: - -``` -Error -Web application file not found: C:\Development\VS Code Projects\webdrop_bridge\file:\webapp\index.html -``` - -### Root Causes - -1. **Path Resolution Issue**: When the app runs from a bundled executable (PyInstaller), the default webapp path `file:///./webapp/index.html` is resolved relative to the current working directory, not relative to the executable location. - -2. **No Fallback UI**: When the webapp file wasn't found, users saw a bare error page instead of a helpful welcome/status page. - -## Solution - -### 1. Improved Path Resolution (main_window.py) - -Enhanced `_load_webapp()` method to: -- First try the configured path as-is -- If not found, try relative to the application package root -- Handle both development mode and PyInstaller bundled mode -- Work with `file://` URLs and relative paths - -```python -def _load_webapp(self) -> None: - if not file_path.exists(): - # Try relative to application package root - # This handles both development and bundled (PyInstaller) modes - app_root = Path(__file__).parent.parent.parent.parent - relative_path = app_root / webapp_url.lstrip("file:///").lstrip("./") - - if relative_path.exists(): - file_path = relative_path -``` - -### 2. Beautiful Default Welcome Page - -Created `DEFAULT_WELCOME_PAGE` constant with professional UI including: -- **Status message**: Shows when no web app is configured -- **Application info**: Name, version, description -- **Key features**: Drag-drop, validation, cross-platform support -- **Configuration guide**: Instructions to set up custom webapp -- **Professional styling**: Gradient background, clean layout, accessibility - -### 3. Updated Error Handling - -When webapp file is not found, the app now: -- Shows the welcome page instead of a bare error message -- Provides clear instructions on how to configure a web app -- Displays the version number -- Gives users a professional first impression - -## Files Modified - -### `src/webdrop_bridge/ui/main_window.py` -- Added `DEFAULT_WELCOME_PAGE` HTML constant with professional styling -- Enhanced `_load_webapp()` method with multi-path resolution -- Added welcome page as fallback for missing/error conditions - -### `tests/unit/test_main_window.py` -- Renamed test: `test_load_nonexistent_file_shows_error` → `test_load_nonexistent_file_shows_welcome_page` -- Updated assertions to verify welcome page is shown instead of error - -## How It Works - -### Development Mode -``` -User runs: python -m webdrop_bridge -Config: WEBAPP_URL=file:///./webapp/index.html -Resolution: C:\...\webdrop_bridge\webapp\index.html -Result: ✅ Loads local webapp from source -``` - -### Bundled Executable (PyInstaller) -``` -User runs: WebDropBridge.exe -PyInstaller unpacks to: _internal/webapp/ -Resolution logic: - 1. Try: C:\current\working\dir\webapp\index.html (fails) - 2. Try: C:\path\to\executable\webapp\index.html (succeeds!) -Result: ✅ Loads bundled webapp from PyInstaller bundle -``` - -### No Webapp Configured -``` -User runs: WebDropBridge.exe -No WEBAPP_URL or file not found -Display: Beautiful welcome page with instructions -Result: ✅ Professional fallback instead of error -``` - -## Testing - -All 99 tests pass: -- ✅ 99 passed in 2.26s -- ✅ Coverage: 84% - -## User Experience - -Before: -``` -Error -Web application file not found: C:\...\file:\webapp\index.html -``` - -After: -``` -🌉 WebDrop Bridge -Professional Web-to-File Drag-and-Drop Bridge - -✓ Application Ready -No web application is currently configured. -Configure WEBAPP_URL in your .env file to load your custom application. - -[Features list] -[Configuration instructions] -[Version info] -``` - -## Configuration for Users - -To use a custom web app: - -```bash -# Create .env file in application directory -WEBAPP_URL=file:///path/to/your/app.html -# Or use remote URL -WEBAPP_URL=http://localhost:3000 -``` - -## Technical Notes - -- CSS selectors escaped with double braces `{{ }}` for `.format()` compatibility -- Works with both relative paths (`./webapp/`) and absolute paths -- Handles `file://` URLs and raw file paths -- Graceful fallback when webapp is missing -- Professional welcome page generates on-the-fly from template - -## Version - -- **Date Fixed**: January 28, 2026 -- **Executable Built**: ✅ WebDropBridge.exe (195.7 MB) -- **Tests**: ✅ 99/99 passing -- **Coverage**: ✅ 84% diff --git a/config_test.json b/config_test.json deleted file mode 100644 index 3efdfcf..0000000 --- a/config_test.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "app_name": "WebDrop Bridge - Download Test", - "webapp_url": "test_download.html", - "url_mappings": [], - "allowed_roots": [], - "allowed_urls": [], - "check_file_exists": true, - "auto_check_updates": false, - "update_check_interval_hours": 24, - "log_level": "DEBUG", - "log_file": "logs/webdrop_bridge.log", - "window_width": 1024, - "window_height": 768, - "enable_logging": true -} diff --git a/full_test.txt b/full_test.txt deleted file mode 100644 index f29edc92778e5d192bc62df16034ac4a2bbd2b38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2270 zcmbW3UvJY;48{G7#CHe}NFdQ_UAH2T`hY;7JuvZSf(MGCu1&i@n}(#_=$8k6$7#Dn zYo`!JNquc!`}o-RZh!yut?3ik(jv?G27D#u7TdxSUoUNF8Md5N(+qukXb&v48L^f& zA$DeS;@yxZv928>w|D~kU~4?CHrcz@whnfEJN68F>h=O{%U)nP;yvEjX11~^&ze{r z^gXvLx8i;#+nD-##F3@rdpo<_ADw}W7zt7g!suSl5h&*%im59iay6>5W_T=!HO42i zmw3G9JFqwSU-Qnerk+O`2lfSw@>w`ev7rb*@El>!@wh?0#43GiA#&zbP0=f(FeNY? z!-Bp-ro3g|+UOJBz@FGwdu5;O+|GF4xu}~R$enq4Z!>6y)B&xh;MaL9Vh(mb zA=y%@mQD3dyuus!))WQ&mY)Dkljoc7>e^%b?kLL$bplmG8B=+(_anJQ)dbJoJwm8E zqAo(6$|0 - - - - - Download Test - - - -

    🧪 Download Test Seite

    - -
    -

    Test 1: Direct Download Link

    -

    Download einer Text-Datei via direktem Link:

    - 📥 Download test.txt -
    - -
    -

    Test 2: JavaScript Download (Blob)

    -

    Download via JavaScript Blob:

    - -
    - -
    -

    Test 3: Base64 Image Download

    -

    Download eines kleinen Bildes:

    - 📥 Download test.png -
    - -
    -

    Test 4: External Link (should open in browser)

    -

    PDF von externem Server:

    - 📥 Download external PDF -
    - - - - diff --git a/test_download.py b/test_download.py deleted file mode 100644 index 5b72aa4..0000000 --- a/test_download.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Test script to verify download functionality.""" - -import sys -from pathlib import Path - -from PySide6.QtCore import QStandardPaths, QUrl -from PySide6.QtWebEngineCore import QWebEngineDownloadRequest -from PySide6.QtWidgets import QApplication - -# Test download path resolution -downloads_path = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DownloadLocation) -print(f"Downloads folder: {downloads_path}") -print(f"Downloads exists: {Path(downloads_path).exists()}") - -# Test file path construction -test_filename = "test_file.pdf" -download_file = Path(downloads_path) / test_filename -print(f"Test download path: {download_file}") -print(f"Parent exists: {download_file.parent.exists()}") diff --git a/test_output.txt b/test_output.txt deleted file mode 100644 index 8b4e01c65696cd4aff11f502a9794ebdbf27db24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24784 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 Yr2GciE3$oLZHfG?l3%QtaFX}`0c?QiC;$Ke diff --git a/test_results.txt b/test_results.txt deleted file mode 100644 index 06d8d28497a1d1e26ece678e8d8d370246558220..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30742 zcmds=TW{OQ8HVS&K>ve#QJ@7H@A|mi0J%tWShz^i?K+2xrVwmL_B!j>S|8%LKfdkz z%%{na)Q}oVvPdlqTQW&;=AG|+80vrjedK2SS##HJ=&tm4O@DcJyeHTR>t(-l9DN8fVmZbRq! zyW#$#^CN%WX8XW>uQT83cXNF1%-y&X{oLtZ8#=!0&xd;-xrgq(dn$yV^?RlitK(Kl13`YqmposIwd&xq;qaYW^QI-cX-k_jf(l-}k!H zkv}&Yf6``csMdA$7yYs#M_P{~p&jZjr~1T0jk{jLI4E4hCs?0-Kk88E);z)uzXlun z&(`pt8g)%~yYa|QJ;tUhkA&xwMm^PWhRe9?l^^LquXp$#F~KI`ar{zy3btM2>2ZR^;MKMv;d zeL(w3pM@JAxtHU2>!SW^A>9+FzV`R${GsRjNA9`qai-Y}{h9Lp2L4XRy5dIHT#J8M z-7k7~Bg8}fk3<+}U-{J;`H^4g*m>q1gC427*RkhGHaQElTGy(qYXw-n zc-FXcQ!5MapXxqCt&wUX}^}WLwCZX zV8jAkiX&GEVVODuNim)b)WRMN{Odr+uzUlZLE1;ciC#AkU{WGCdrD1oJ+nryr*VQ0 zEz6awh7OH+;HhSQ8`Q&2Lov7$O5?YrxN|E>F?=I5RnZ<4J%ULq@WgY5c_hy@CSJia z-Q}~l6+u6v_3a#T!`XrNa!n^g$7@h`S*X;co@-dsGIS2=j)7I+sfjVn-2n~ z!1dl~dyofqZwWi1;Gq#vA@@qe1Vm9j>`k1y+!Xfs1B9FGvk74sx1538RTK8XUB$uw^tZ}C@Y zA@Xb{(fD%slWEV9tXeAip5{5;H*Kn34mOKSO-e>q1p}F<>7E)n;8mbVHSP@gZlo(s zrGDbN@V>-Av^ngR0HNC)2UvwA@DY-fug7q_-?w&Ww&E8MRc&%-OTMUji+__D^Uu0C1%1D`WVYnNrr z{+VQuY%;ZTeDtz5L_)*afv%vk@KM~4^!9bmWTE1mW1h2l-m^u+?Kq`*t;_SSPluRwhOYF)bRM1bh?nFtD@x9a4qmk#CZ}I34NZPvK0>QcTF?JUB7?B6 zk&%*-dm*jJ4it3PNyQFY1j$vQH!WNjs_f+dkUZ0^XR7R`Eq#7AL1$0_gwP~S^;jXR?tGlqpUyd%CcVv zcNevN_+*unjCb;*=xMyO7$k7!ktlK_2{lQj`anEw(I+*z)j$$G1*)H+#!}y_yd_Cq zlw{=6qRB^#^A8+U=Djo(@^cu|_{pUEu_S^0F!;E{VrA*ho4<|3>@5N)%S)Y%FBb`U zm4MScVmdX0EkLFtJ?d(@W{quUDcU6MPj9t|(x=hrN3W@(M`Q8yYmH48@Gl8HM(^fj z!3VNW&=XMnOwh&XH}>pSYqM$I%g?%Whs#EWo~GB#5~uOxXBYZ_2fAWy*>kwMc!CdR zvGqt;EPHUMSL1V}6RsrJnZ}tPX%hv&0Vdt&l6ZDtO!{Rn$Jf2{SKC#4`dp}LHdpG? zqZGAZlT}(3G|1Dn_vO-5$&bj>>$NPZz=1@H!~*p?d{xkdz6jf8hG)tygxea)$&cE* zrBOgNEsLGz#O0uTC7r=)pXfE+!9$!@y~4Wl$jcq-tVKnkiw8}ZLbTl?V=)ii7KI4^ z$$Sd(0S7`_*CWtZWY{2{uY;CIQzSoYu}q2-=Uwz@e-=HieWtj6|LJy%JVzfe#b~1m zQ+_sN9!&4VCx?3C3VVECh;QiiH_x*iPvJ+}J^*Z%*IL#qWK@Va ziWx7gogK66KpJCybUzPW7mKmjZvj;;UrCjdXH3;w=l)@ZiXnC}iHx39*k@vXOUMDT zV<1iYG=1}P0n4JV^^ZiH)ovQow$_QTs+h`sCDRe*XE^4e(i_zXUg(*5VV645GOJ&< zeOL&N@&vsgPr*{exW#%z&^=alM-H))*vsm(6Vg=d6Xy7Ahk6v`AZ@+Uc$SkN$m?sW z%2ZKfR`q3l&SSepH^@AEnTM$?Q?nvkPQKu!bdk&> zLrvI%2aIkz)V%2lwC78qvF#ZQ?NIG3Dbum9nc~ONHb{zXYaP9j>Y z4b&q4Ud&L}dAEH*WU)r|*{5Y>c)C{2Lh9=J-FB9g=a|l8){W9ct5}n??wXI%l_^cP zN|>T9MDOz?<;kZ2?X>7(nMb=Vf=1+ct*gB$)458Btp0@r6*F0BIxQ!cu?RZF`f1ll z?zFPRLOiJ*9Z-&w&}r2xtWP0k6$j&N4slX=E;P>?EOuPsAyuDR6|z_96nx+C8|y;cV0G_s4L;S8er%_Cy>(tr9%fIP8tdAW zLkxUqd8Ckis#ebF60%r6O_l!ODaR#|+a~uAR`V0#S(ItdeW%5)mU(Pj4$bN?TE?F2 zOf{#Irdoc^#h!l@z7N5CQ@&e*j1Z#^3*8xT9NLh4I&0eWsp`DSJ-jGKla^xPxV)u!qHbTvOIeTy|URa_JsbluW)O zjC=_7f;6@AvlHFpD*70U*gEfXI0+fbM}c?hp!*Ij5y72`MoRk<4j7;qA`{$ qaFg|%>753= 3: - break - - # Cleanup - thread.quit() - thread.wait() - - # Verify results - print(f"\n✓ Download called: {download_called}") - print(f"✓ Signals emitted: {len(signals_emitted)}") - - # Check if completion signal was emitted (shows async operations completed) - has_complete_or_failed = any(sig[0] in ("complete", "failed") for sig in signals_emitted) - has_finished = any(sig[0] == "finished" for sig in signals_emitted) - - print(f"✓ Completion/Failed signal emitted: {has_complete_or_failed}") - print(f"✓ Finished signal emitted: {has_finished}") - - if has_complete_or_failed and has_finished: - print("\n✅ SUCCESS: Update download runs asynchronously without blocking UI!") - return True - else: - print("\n❌ FAILED: Signals not emitted properly") - print(f" Signals: {signals_emitted}") - return False - - -def test_update_download_worker_exists(): - """Verify that UpdateDownloadWorker class exists and has correct signals.""" - print("\n=== Testing UpdateDownloadWorker Class ===\n") - - # Check class exists - assert hasattr(UpdateDownloadWorker, '__init__'), "UpdateDownloadWorker missing __init__" - print("✓ UpdateDownloadWorker class exists") - - # Check signals - required_signals = ['download_complete', 'download_failed', 'update_status', 'finished'] - for signal_name in required_signals: - assert hasattr(UpdateDownloadWorker, signal_name), f"Missing signal: {signal_name}" - print(f"✓ Signal '{signal_name}' defined") - - # Check methods - assert hasattr(UpdateDownloadWorker, 'run'), "UpdateDownloadWorker missing run method" - print("✓ Method 'run' defined") - - print("\n✅ SUCCESS: UpdateDownloadWorker properly implemented!") - return True - - -def test_main_window_uses_async_download(): - """Verify that MainWindow uses async download instead of blocking.""" - print("\n=== Testing MainWindow Async Download Integration ===\n") - - # Check that _perform_update_async exists (new async version) - assert hasattr(MainWindow, '_perform_update_async'), "MainWindow missing _perform_update_async" - print("✓ Method '_perform_update_async' exists (new async version)") - - # Check that old blocking _perform_update is gone - assert not hasattr(MainWindow, '_perform_update'), \ - "MainWindow still has old blocking _perform_update method" - print("✓ Old blocking '_perform_update' method removed") - - # Check download/failed handlers exist - assert hasattr(MainWindow, '_on_download_complete'), "MainWindow missing _on_download_complete" - assert hasattr(MainWindow, '_on_download_failed'), "MainWindow missing _on_download_failed" - print("✓ Download completion handlers exist") - - print("\n✅ SUCCESS: MainWindow properly integrated with async download!") - return True - - -if __name__ == "__main__": - print("\n" + "="*60) - print("UPDATE FEATURE FIX VERIFICATION") - print("="*60) - - try: - # Test 1: Worker exists - test1 = test_update_download_worker_exists() - - # Test 2: MainWindow integration - test2 = test_main_window_uses_async_download() - - # Test 3: Async operation - test3 = test_update_download_runs_in_background() - - print("\n" + "="*60) - if test1 and test2 and test3: - print("✅ ALL TESTS PASSED - UPDATE FEATURE HANG FIXED!") - print("="*60 + "\n") - print("Summary of changes:") - print("- Created UpdateDownloadWorker class for async downloads") - print("- Moved blocking operations from UI thread to background thread") - print("- Added handlers for download completion/failure") - print("- UI now stays responsive during update download") - else: - print("❌ SOME TESTS FAILED") - print("="*60 + "\n") - except Exception as e: - print(f"\n❌ ERROR: {e}") - import traceback - traceback.print_exc() diff --git a/test_url_mappings.py b/test_url_mappings.py deleted file mode 100644 index 0c6c3a7..0000000 --- a/test_url_mappings.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Quick test to verify URL mappings are loaded correctly.""" -import sys -from pathlib import Path - -# Add src to path -sys.path.insert(0, str(Path(__file__).parent / "src")) - -from webdrop_bridge.config import Config - -# Load config from .env -config = Config.from_env() - -print(f"✓ Config loaded successfully") -print(f" URL Mappings: {len(config.url_mappings)} loaded") - -if config.url_mappings: - for i, mapping in enumerate(config.url_mappings, 1): - print(f" {i}. {mapping.url_prefix}") - print(f" -> {mapping.local_path}") -else: - print(" ⚠ No URL mappings found!") - -print(f"\n Allowed roots: {config.allowed_roots}") -print(f" Web app URL: {config.webapp_url}") diff --git a/verify_fix.py b/verify_fix.py deleted file mode 100644 index 88b8481..0000000 --- a/verify_fix.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python -"""Quick verification that the update hang fix is in place.""" - -import inspect - -from webdrop_bridge.ui.main_window import MainWindow, UpdateDownloadWorker - -print("\n" + "="*70) -print("VERIFICATION: Update Feature Hang Fix") -print("="*70 + "\n") - -# Check 1: UpdateDownloadWorker exists -print("✓ UpdateDownloadWorker class exists") -print(f" - Location: {inspect.getfile(UpdateDownloadWorker)}") - -# Check 2: Verify signals are defined -signals = ['download_complete', 'download_failed', 'update_status', 'finished'] -print(f"\n✓ UpdateDownloadWorker has required signals:") -for sig in signals: - assert hasattr(UpdateDownloadWorker, sig) - print(f" - {sig}") - -# Check 3: Verify run method exists -assert hasattr(UpdateDownloadWorker, 'run') -print(f"\n✓ UpdateDownloadWorker.run() method exists") - -# Check 4: Verify MainWindow uses async download -print(f"\n✓ MainWindow changes:") -assert hasattr(MainWindow, '_perform_update_async') -print(f" - Has _perform_update_async() method (new async version)") -assert hasattr(MainWindow, '_on_download_complete') -print(f" - Has _on_download_complete() handler") -assert hasattr(MainWindow, '_on_download_failed') -print(f" - Has _on_download_failed() handler") -assert not hasattr(MainWindow, '_perform_update') -print(f" - Old blocking _perform_update() method removed") - -# Check 5: Verify the fix: Look at _perform_update_async source -source = inspect.getsource(MainWindow._perform_update_async) -assert 'QThread()' in source -print(f"\n✓ _perform_update_async uses background thread:") -assert 'UpdateDownloadWorker' in source -print(f" - Creates UpdateDownloadWorker") -assert 'worker.moveToThread(thread)' in source -print(f" - Moves worker to background thread") -assert 'thread.start()' in source -print(f" - Starts the thread") - -print("\n" + "="*70) -print("✅ VERIFICATION SUCCESSFUL!") -print("="*70) -print("\nFIX SUMMARY:") -print("-" * 70) -print(""" -The update feature hang issue has been fixed by: - -1. Created UpdateDownloadWorker class that runs async operations in a - background thread (instead of blocking the UI thread). - -2. The worker properly handles: - - Downloading the update asynchronously - - Verifying checksums asynchronously - - Emitting signals for UI updates - -3. MainWindow's _perform_update_async() method now: - - Creates a background thread for the worker - - Connects signals for download complete/failure handlers - - Keeps a reference to prevent garbage collection - - Properly cleans up threads after completion - -Result: The update dialog now displays without freezing the application! - The user can interact with the UI while the download happens. -""") -print("-" * 70 + "\n") diff --git a/verify_timeout_handling.py b/verify_timeout_handling.py deleted file mode 100644 index 51755d8..0000000 --- a/verify_timeout_handling.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -"""Verify timeout and error handling in update feature.""" - -import inspect - -from webdrop_bridge.core.updater import UpdateManager -from webdrop_bridge.ui.main_window import UpdateCheckWorker, UpdateDownloadWorker - -print("\n" + "="*70) -print("TIMEOUT AND ERROR HANDLING VERIFICATION") -print("="*70 + "\n") - -print("Test 1: UpdateCheckWorker timeout handling") -print("-" * 70) - -# Check UpdateCheckWorker source for asyncio.wait_for -source = inspect.getsource(UpdateCheckWorker.run) -if "asyncio.wait_for" in source and "timeout=15" in source: - print("✓ UpdateCheckWorker has 15-second timeout") - print(" await asyncio.wait_for(..., timeout=15)") -else: - print("❌ Missing timeout in UpdateCheckWorker") - -if "asyncio.TimeoutError" in source: - print("✓ Handles asyncio.TimeoutError exception") -else: - print("❌ Missing TimeoutError handling") - -if "loop.close()" in source: - print("✓ Properly closes event loop in finally block") -else: - print("❌ Missing loop.close() cleanup") - -print("\nTest 2: UpdateDownloadWorker timeout handling") -print("-" * 70) - -source = inspect.getsource(UpdateDownloadWorker.run) -if "asyncio.wait_for" in source: - print("✓ UpdateDownloadWorker uses asyncio.wait_for") - if "timeout=300" in source: - print(" → Download timeout: 300 seconds (5 minutes)") - if "timeout=30" in source: - print(" → Verification timeout: 30 seconds") -else: - print("❌ Missing timeout in UpdateDownloadWorker") - -if "asyncio.TimeoutError" in source: - print("✓ Handles asyncio.TimeoutError exception") - if "Operation timed out" in source: - print(" → Shows 'Operation timed out' message") -else: - print("❌ Missing TimeoutError handling") - -if "loop.close()" in source: - print("✓ Properly closes event loop in finally block") -else: - print("❌ Missing loop.close() cleanup") - -print("\nTest 3: UpdateManager timeout handling") -print("-" * 70) - -source = inspect.getsource(UpdateManager.check_for_updates) -if "asyncio.wait_for" in source: - print("✓ check_for_updates has timeout") - if "timeout=10" in source: - print(" → API check timeout: 10 seconds") -else: - print("❌ Missing timeout in check_for_updates") - -if "asyncio.TimeoutError" in source: - print("✓ Handles asyncio.TimeoutError") - if "timed out" in source or "timeout" in source.lower(): - print(" → Logs timeout message") -else: - print("❌ Missing TimeoutError handling") - -# Check download_update timeout -source = inspect.getsource(UpdateManager.download_update) -if "asyncio.wait_for" in source: - print("\n✓ download_update has timeout") - if "timeout=300" in source: - print(" → Download timeout: 300 seconds (5 minutes)") -else: - print("❌ Missing timeout in download_update") - -# Check verify_checksum timeout -source = inspect.getsource(UpdateManager.verify_checksum) -if "asyncio.wait_for" in source: - print("✓ verify_checksum has timeout") - if "timeout=30" in source: - print(" → Checksum verification timeout: 30 seconds") -else: - print("❌ Missing timeout in verify_checksum") - -print("\n" + "="*70) -print("✅ TIMEOUT HANDLING PROPERLY IMPLEMENTED!") -print("="*70) -print("\nSummary of timeout protection:") -print(" • Update check: 15 seconds") -print(" • API fetch: 10 seconds") -print(" • Download: 5 minutes (300 seconds)") -print(" • Checksum verification: 30 seconds") -print("\nWhen timeouts occur:") -print(" • User-friendly error message is shown") -print(" • Event loops are properly closed") -print(" • Application doesn't hang indefinitely") -print(" • User can retry or cancel the operation") -print("="*70 + "\n") From ae5c86814f65da02fd6e2480f0e70fd5739e0842 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 09:29:33 +0100 Subject: [PATCH 07/16] Remove obsolete WebDrop Bridge scripts: bridge_script.js, bridge_script_debug.js, bridge_script_drop_intercept.js, bridge_script_hybrid.js, bridge_script_v2.js --- docs/SCRIPT_VARIANTS.md | 171 --------- src/webdrop_bridge/ui/bridge_script.js | 325 ----------------- src/webdrop_bridge/ui/bridge_script_debug.js | 333 ------------------ .../ui/bridge_script_drop_intercept.js | 211 ----------- src/webdrop_bridge/ui/bridge_script_hybrid.js | 109 ------ src/webdrop_bridge/ui/bridge_script_v2.js | 214 ----------- 6 files changed, 1363 deletions(-) delete mode 100644 docs/SCRIPT_VARIANTS.md delete mode 100644 src/webdrop_bridge/ui/bridge_script.js delete mode 100644 src/webdrop_bridge/ui/bridge_script_debug.js delete mode 100644 src/webdrop_bridge/ui/bridge_script_drop_intercept.js delete mode 100644 src/webdrop_bridge/ui/bridge_script_hybrid.js delete mode 100644 src/webdrop_bridge/ui/bridge_script_v2.js diff --git a/docs/SCRIPT_VARIANTS.md b/docs/SCRIPT_VARIANTS.md deleted file mode 100644 index cd21eed..0000000 --- a/docs/SCRIPT_VARIANTS.md +++ /dev/null @@ -1,171 +0,0 @@ -# WebDrop Bridge - Drag & Drop Script Varianten - -## Verfügbare JavaScript-Scripte - -### 1. `bridge_script.js` (CURRENT - Original) -**Status:** Versucht Drag zu ersetzen, verhindert Popup -**Verwendung:** Testing, zeigt dass File-Drop prinzipiell funktioniert -**Problem:** Web-App Popup erscheint nicht - -### 2. `bridge_script_debug.js` (DEBUG - **EMPFOHLEN ZUM START**) -**Status:** Umfangreiches Logging, keine Manipulation -**Verwendung:** Herausfinden wie Popup ausgelöst wird -**Funktionen:** -- Loggt ALLE Drag/Drop Events -- Überwacht DataTransfer.setData -- Überwacht Network-Requests (API-Calls) -- Erkennt Angular Components -- Erkennt Modal/Popup Öffnungen -- Bietet Helper-Funktionen in Console - -**So verwenden:** -```python -# In main_window.py Zeile ~433 ändern: -script_path = Path(__file__).parent / "bridge_script_debug.js" # <-- DEBUG aktivieren -``` - -**Im Browser:** -1. F12 → Console öffnen -2. ALT-Drag+Drop durchführen -3. Logs analysieren: - - `[MODAL OPENED]` → Popup-Trigger gefunden! - - `[FETCH]` oder `[XHR]` → API-Call gefunden! - - `[EVENT]` → Drag-Flow verstehen - -### 3. `bridge_script_v2.js` (EXTEND - Experimentell) -**Status:** Versuch DataTransfer zu erweitern -**Problem:** Browser-Sicherheit verhindert Files hinzufügen - -### 4. `bridge_script_hybrid.js` (HYBRID - Experimentell) -**Status:** Versuch parallele Drags -**Problem:** Nur ein Drag zur Zeit möglich - -### 5. `bridge_script_drop_intercept.js` (DROP - Experimentell) -**Status:** Drop-Event abfangen und manipulieren -**Problem:** DataTransfer kann nicht mit Files erstellt werden - -## 🎯 Empfohlener Workflow - -### Phase 1: Debugging (JETZT) - -1. **Debug-Script aktivieren:** - ```python - # main_window.py, _install_bridge_script() - script_path = Path(__file__).parent / "bridge_script_debug.js" - ``` - -2. **Anwendung starten und testen:** - ```powershell - python -m webdrop_bridge.main - ``` - -3. **In Browser (F12 Console):** - - ALT-Drag+Drop durchführen - - Logs kopieren und analysieren - - Nach `[MODAL OPENED]` oder API-Calls suchen - -4. **Herausfinden:** - - ✅ Wie wird Popup ausgelöst? (API-Call, Event, Component-Methode) - - ✅ Welche Asset-ID wird verwendet? - - ✅ Wo im DOM befindet sich die Popup-Logik? - -### Phase 2: Implementation (DANACH) - -Basierend auf Debug-Ergebnissen: - -**Fall A: Popup wird durch API-Call ausgelöst** -```javascript -// In bridge_script.js nach erfolgreichem Drop: -fetch('/api/assets/' + assetId + '/checkout', { - method: 'POST', - headers: {'Content-Type': 'application/json'} -}).then(response => { - console.log('Checkout popup triggered'); -}); -``` - -**Fall B: Popup wird durch Angular-Event ausgelöst** -```javascript -// Trigger Angular CDK Event -var dropList = document.querySelector('[cdkdroplist]'); -if (dropList && window.ng) { - var component = ng.getComponent(dropList); - component.onDrop({assetId: assetId}); -} -``` - -**Fall C: Popup wird durch Component-Methode ausgelöst** -```javascript -// Direkter Methoden-Aufruf -var assetCard = document.getElementById(assetId); -if (assetCard && window.ng) { - var component = ng.getComponent(assetCard); - component.showCheckoutDialog(); -} -``` - -### Phase 3: Testing (FINAL) - -1. Implementation in `bridge_script.js` integrieren -2. Beide Funktionen testen: - - ✅ File wird gedroppt (Z:\ Laufwerk) - - ✅ Popup erscheint (Auschecken-Dialog) - -## 🔧 Script-Wechsel in Code - -```python -# src/webdrop_bridge/ui/main_window.py -# Zeile ~433 in _install_bridge_script() - -# ORIGINAL (funktioniert, aber kein Popup): -script_path = Path(__file__).parent / "bridge_script.js" - -# DEBUG (für Analyse): -script_path = Path(__file__).parent / "bridge_script_debug.js" - -# Oder via Konfiguration: -script_name = self.config.bridge_script or "bridge_script.js" -script_path = Path(__file__).parent / script_name -``` - -## 📝 Debug-Checkliste - -Beim Testen mit Debug-Script, notieren Sie: - -- [ ] Wann erscheint das Popup? (Nach Drop, nach Verzögerung, sofort?) -- [ ] Gibt es API-Calls? (Welche URL, Parameter, Zeitpunkt?) -- [ ] Welche Angular-Events feuern? (CDK Events, Custom Events?) -- [ ] Wo wird Modal/Dialog erstellt? (DOM-Position, Klassen, Component) -- [ ] Welche Asset-Informationen werden benötigt? (ID, Name, URL?) -- [ ] Stack-Trace beim Modal-Öffnen? (console.trace Ausgabe) - -## 🚀 Quick Commands - -```powershell -# App mit Debug-Script starten -python -m webdrop_bridge.main - -# In Browser Console (F12): -webdrop_debug.getEventCounts() # Event-Statistiken -webdrop_debug.resetCounters() # Zähler zurücksetzen -webdrop_debug.getListeners(element) # Event-Listener auflisten -webdrop_debug.getComponent(element) # Angular Component anzeigen - -# Logs filtern: -# Console Filter: "[MODAL]" → Nur Popup-Logs -# Console Filter: "[EVENT]" → Nur Event-Logs -# Console Filter: "[FETCH]|[XHR]" → Nur Network-Logs -``` - -## ⚠️ Bekannte Limitationen - -1. **Angular DevTools benötigt:** Für `ng.getComponent()` Installation nötig -2. **Chrome/Edge:** Einige Features funktionieren nur in Chromium-Browsern -3. **CSP:** Bei strengen Content-Security-Policies können Logs blockiert werden -4. **Performance:** Debug-Script hat deutlichen Performance-Overhead - -## 📚 Weiterführende Dokumentation - -- [ANGULAR_CDK_ANALYSIS.md](ANGULAR_CDK_ANALYSIS.md) - Angular Framework Details -- [DRAG_DROP_PROBLEM_ANALYSIS.md](DRAG_DROP_PROBLEM_ANALYSIS.md) - Problem-Analyse + Lösungen -- [ARCHITECTURE.md](ARCHITECTURE.md) - Gesamtarchitektur diff --git a/src/webdrop_bridge/ui/bridge_script.js b/src/webdrop_bridge/ui/bridge_script.js deleted file mode 100644 index ad8d4ce..0000000 --- a/src/webdrop_bridge/ui/bridge_script.js +++ /dev/null @@ -1,325 +0,0 @@ -// WebDrop Bridge - Injected Script -// Automatically converts Z:\ path drags to native file drags via QWebChannel bridge - -(function() { - if (window.__webdrop_bridge_injected) return; - window.__webdrop_bridge_injected = true; - - console.log('[WebDrop Bridge] Script loaded'); - - // Store web app's dragstart handlers by intercepting addEventListener - var webAppDragHandlers = []; - var originalAddEventListener = EventTarget.prototype.addEventListener; - var listenerPatchActive = true; - - // Patch addEventListener to intercept dragstart registrations - EventTarget.prototype.addEventListener = function(type, listener, options) { - if (listenerPatchActive && type === 'dragstart' && listener) { - // Store the web app's dragstart handler instead of registering it - console.log('[WebDrop Bridge] Intercepted dragstart listener registration on', this.tagName || this.constructor.name); - webAppDragHandlers.push({ - target: this, - listener: listener, - options: options - }); - return; - } - // All other events: use original - return originalAddEventListener.call(this, type, listener, options); - }; - - // Patch DataTransfer.setData to intercept URL setting by the web app - var originalSetData = null; - var currentDragData = null; - - try { - if (DataTransfer.prototype.setData) { - originalSetData = DataTransfer.prototype.setData; - - DataTransfer.prototype.setData = function(format, data) { - // Store the data for our analysis - if (format === 'text/plain' || format === 'text/uri-list') { - currentDragData = data; - console.log('[WebDrop Bridge] DataTransfer.setData intercepted:', format, '=', data.substring(0, 80)); - - // Log via bridge if available - if (window.bridge && typeof window.bridge.debug_log === 'function') { - window.bridge.debug_log('setData intercepted: ' + format + ' = ' + data.substring(0, 60)); - } - } - // Call original to maintain web app functionality - return originalSetData.call(this, format, data); - }; - - console.log('[WebDrop Bridge] DataTransfer.setData patched'); - } - } catch(e) { - console.error('[WebDrop Bridge] Failed to patch DataTransfer:', e); - } - - function ensureChannel(cb) { - if (window.bridge) { cb(); return; } - - function init() { - if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { - new QWebChannel(window.qt.webChannelTransport, function(channel) { - window.bridge = channel.objects.bridge; - console.log('[WebDrop Bridge] QWebChannel connected'); - cb(); - }); - } else { - // If QWebChannel is not available, log error - console.error('[WebDrop Bridge] QWebChannel not available! Check if qwebchannel.js was loaded.'); - } - } - - // QWebChannel should already be loaded inline (no need to load from qrc://) - if (window.QWebChannel) { - init(); - } else { - console.error('[WebDrop Bridge] QWebChannel not found! Cannot initialize bridge.'); - } - } - - function hook() { - console.log('[WebDrop Bridge] Installing hook, have ' + webAppDragHandlers.length + ' intercepted handlers'); - - if (window.bridge && typeof window.bridge.debug_log === 'function') { - window.bridge.debug_log('Installing drag interceptor with ' + webAppDragHandlers.length + ' intercepted handlers'); - } - - // Stop intercepting addEventListener - from now on, listeners register normally - listenerPatchActive = false; - - // Register OUR dragstart handler using capture phase on document - originalAddEventListener.call(document, 'dragstart', function(e) { - try { - console.log('[WebDrop Bridge] >>> DRAGSTART fired on:', e.target.tagName, 'altKey:', e.altKey, 'currentDragData:', currentDragData); - - if (window.bridge && typeof window.bridge.debug_log === 'function') { - window.bridge.debug_log('dragstart fired on ' + e.target.tagName + ' altKey=' + e.altKey); - } - - // Only intercept if ALT key is pressed (web app's text drag mode) - if (!e.altKey) { - console.log('[WebDrop Bridge] ALT not pressed, ignoring drag (normal web app drag)'); - return; // Let web app handle normal drags - } - - console.log('[WebDrop Bridge] ALT pressed - processing for file drag conversion'); - - // Manually invoke all the web app's dragstart handlers - var handlersInvoked = 0; - console.log('[WebDrop Bridge] About to invoke', webAppDragHandlers.length, 'stored handlers'); - - for (var i = 0; i < webAppDragHandlers.length; i++) { - try { - var handler = webAppDragHandlers[i]; - // Check if this handler should be called for this target - if (handler.target === document || - handler.target === e.target || - (handler.target.contains && handler.target.contains(e.target))) { - - console.log('[WebDrop Bridge] Calling stored handler #' + i); - handler.listener.call(e.target, e); - handlersInvoked++; - } - } catch (err) { - console.error('[WebDrop Bridge] Error calling web app handler #' + i + ':', err); - } - } - - console.log('[WebDrop Bridge] Invoked', handlersInvoked, 'handlers, currentDragData:', currentDragData ? currentDragData.substring(0, 60) : 'null'); - - // NOW check if we have a convertible URL - if (currentDragData) { - console.log('[WebDrop Bridge] Checking currentDragData:', currentDragData.substring(0, 80)); - var path = currentDragData; - var isZDrive = /^z:/i.test(path); - var isAzureUrl = /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(path); - - console.log('[WebDrop Bridge] isZDrive:', isZDrive, 'isAzureUrl:', isAzureUrl); - - if (isZDrive || isAzureUrl) { - console.log('[WebDrop Bridge] >>> CONVERTING URL TO NATIVE DRAG (DELAYED)'); - - if (window.bridge && typeof window.bridge.debug_log === 'function') { - window.bridge.debug_log('Convertible URL detected - delaying Qt drag for Angular events'); - } - - // DON'T prevent immediately - let Angular process dragstart for ~200ms - // This allows web app to register the drag and prepare popup - - // Store URL and element for later - window.__lastDraggedUrl = path; - var originalTarget = e.target; - - // After 200ms: Cancel browser drag and start Qt drag - setTimeout(function() { - console.log('[WebDrop Bridge] Starting Qt drag now, browser drag will be cancelled'); - - // Try to cancel browser drag by creating a fake drop on same element - try { - var dropEvent = new DragEvent('drop', { - bubbles: true, - cancelable: true, - view: window - }); - originalTarget.dispatchEvent(dropEvent); - } catch(err) { - console.log('[WebDrop Bridge] Could not dispatch drop event:', err); - } - - // Hide Angular CDK overlays - var style = document.createElement('style'); - style.id = 'webdrop-bridge-hide-overlay'; - style.textContent = ` - .cdk-drag-animating, - .cdk-drag-preview, - .cdk-drag-placeholder, - [cdkdroplist].cdk-drop-list-dragging, - #root-collection-drop-area, - [id*="drop-area"] { - opacity: 0 !important; - pointer-events: none !important; - display: none !important; - } - `; - document.head.appendChild(style); - - // Start Qt drag - ensureChannel(function() { - if (window.bridge && typeof window.bridge.start_file_drag === 'function') { - console.log('[WebDrop Bridge] Calling start_file_drag:', path.substring(0, 60)); - window.bridge.start_file_drag(path); - currentDragData = null; - - // Cleanup after 5 seconds - setTimeout(function() { - var hideStyle = document.getElementById('webdrop-bridge-hide-overlay'); - if (hideStyle) hideStyle.remove(); - }, 5000); - } else { - console.error('[WebDrop Bridge] bridge.start_file_drag not available!'); - } - }); - }, 200); // 200ms delay - - // Let the browser drag start naturally (no preventDefault yet) - - return false; - } else { - console.log('[WebDrop Bridge] URL not convertible:', path.substring(0, 60)); - } - } else { - console.log('[WebDrop Bridge] No currentDragData set'); - } - } catch (mainError) { - console.error('[WebDrop Bridge] CRITICAL ERROR in dragstart handler:', mainError); - if (window.bridge && typeof window.bridge.debug_log === 'function') { - window.bridge.debug_log('ERROR in dragstart: ' + mainError.message); - } - } - }, true); // CAPTURE PHASE - intercept early - - // Reset state on dragend - originalAddEventListener.call(document, 'dragend', function(e) { - currentDragData = null; - }, false); - - console.log('[WebDrop Bridge] Drag listener registered on document (capture phase)'); - } - - // Wait for DOMContentLoaded and then a bit more before installing hook - // This gives the web app time to register its handlers - function installHook() { - console.log('[WebDrop Bridge] DOM ready, waiting 2 seconds for web app to register handlers...'); - console.log('[WebDrop Bridge] Currently have', webAppDragHandlers.length, 'intercepted handlers'); - - setTimeout(function() { - console.log('[WebDrop Bridge] Installing hook now, have', webAppDragHandlers.length, 'intercepted handlers'); - hook(); - - ensureChannel(function() { - if (window.bridge && typeof window.bridge.debug_log === 'function') { - window.bridge.debug_log('Hook installed with ' + webAppDragHandlers.length + ' captured handlers'); - } - }); - }, 2000); // Wait 2 seconds after DOM ready - } - - // Global function to trigger checkout after successful file drop - window.trigger_checkout_for_asset = function(azure_url) { - console.log('[WebDrop Bridge] trigger_checkout_for_asset called for:', azure_url); - - // Extract asset ID from Azure URL - // Format: https://devagravitystg.file.core.windows.net/devagravitysync/{assetId}/{filename} - var match = azure_url.match(/\/devagravitysync\/([^\/]+)\//); - if (!match) { - console.error('[WebDrop Bridge] Could not extract asset ID from URL:', azure_url); - return; - } - - var assetId = match[1]; - console.log('[WebDrop Bridge] Extracted asset ID:', assetId); - console.log('[WebDrop Bridge] Calling checkout API directly...'); - - // Direct API call to checkout asset (skip popup, auto-checkout) - var apiUrl = 'https://devagravityprivate.azurewebsites.net/api/assets/' + assetId + '/checkout'; - - fetch(apiUrl, { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - // Empty body or add checkout parameters if needed - }) - }).then(function(response) { - if (response.ok) { - console.log('[WebDrop Bridge] ✅ Asset checked out successfully:', assetId); - return response.json(); - } else { - console.warn('[WebDrop Bridge] ⚠️ Checkout API returned status:', response.status); - // Try alternative: Mark as checked out without confirmation - return tryAlternativeCheckout(assetId); - } - }).then(function(data) { - console.log('[WebDrop Bridge] Checkout response:', data); - }).catch(function(err) { - console.error('[WebDrop Bridge] ❌ Checkout API error:', err); - // Fallback: Try alternative checkout method - tryAlternativeCheckout(assetId); - }); - }; - - // Alternative checkout method if direct API fails - function tryAlternativeCheckout(assetId) { - console.log('[WebDrop Bridge] Trying alternative checkout for:', assetId); - - // Option 1: Try GET to fetch asset status, might trigger checkout tracking - var statusUrl = 'https://devagravityprivate.azurewebsites.net/api/assets/' + assetId; - - return fetch(statusUrl, { - method: 'GET', - credentials: 'include' - }).then(function(response) { - if (response.ok) { - console.log('[WebDrop Bridge] Asset status fetched, might have logged usage'); - } - return response.json(); - }).catch(function(err) { - console.error('[WebDrop Bridge] Alternative checkout also failed:', err); - }); - } - - // Install after DOM is ready - if (document.readyState === 'loading') { - console.log('[WebDrop Bridge] Waiting for DOMContentLoaded...'); - originalAddEventListener.call(document, 'DOMContentLoaded', installHook); - } else { - console.log('[WebDrop Bridge] DOM already ready, installing hook...'); - installHook(); - } -})(); diff --git a/src/webdrop_bridge/ui/bridge_script_debug.js b/src/webdrop_bridge/ui/bridge_script_debug.js deleted file mode 100644 index 3fe16a1..0000000 --- a/src/webdrop_bridge/ui/bridge_script_debug.js +++ /dev/null @@ -1,333 +0,0 @@ -// WebDrop Bridge - DEBUG Version -// Heavy logging to understand web app's drag&drop behavior -// -// Usage: -// 1. Load this script -// 2. Perform ALT-drag+drop in web app -// 3. Check console for detailed logs -// 4. Look for: API calls, events names, component methods - -(function() { - if (window.__webdrop_debug_injected) return; - window.__webdrop_debug_injected = true; - - console.log('%c[WebDrop DEBUG] Script loaded', 'background: #222; color: #bada55; font-size: 14px; font-weight: bold;'); - - // ============================================================================ - // PART 1: Event Monitoring - see ALL drag/drop related events - // ============================================================================ - - var allEvents = ['dragstart', 'drag', 'dragenter', 'dragover', 'dragleave', 'drop', 'dragend']; - var eventCounts = {}; - - allEvents.forEach(function(eventName) { - eventCounts[eventName] = 0; - - document.addEventListener(eventName, function(e) { - eventCounts[eventName]++; - - // Only log dragstart, drop, dragend fully (others are noisy) - if (eventName === 'dragstart' || eventName === 'drop' || eventName === 'dragend') { - console.group('%c[EVENT] ' + eventName.toUpperCase(), 'color: #FF6B6B; font-weight: bold;'); - console.log('Target:', e.target.tagName, e.target.className, e.target.id); - console.log('DataTransfer:', { - types: Array.from(e.dataTransfer.types), - effectAllowed: e.dataTransfer.effectAllowed, - dropEffect: e.dataTransfer.dropEffect, - files: e.dataTransfer.files.length - }); - console.log('Keys:', { - alt: e.altKey, - ctrl: e.ctrlKey, - shift: e.shiftKey - }); - console.log('Position:', {x: e.clientX, y: e.clientY}); - - // Try to read data (only works in drop/dragstart) - if (eventName === 'drop' || eventName === 'dragstart') { - try { - var plainText = e.dataTransfer.getData('text/plain'); - var uriList = e.dataTransfer.getData('text/uri-list'); - console.log('Data:', { - 'text/plain': plainText ? plainText.substring(0, 100) : null, - 'text/uri-list': uriList ? uriList.substring(0, 100) : null - }); - } catch(err) { - console.warn('Could not read DataTransfer data:', err.message); - } - } - - console.groupEnd(); - } - }, true); // Capture phase - }); - - // Log event summary every 5 seconds - setInterval(function() { - var hasEvents = Object.keys(eventCounts).some(function(k) { return eventCounts[k] > 0; }); - if (hasEvents) { - console.log('%c[EVENT SUMMARY]', 'color: #4ECDC4; font-weight: bold;', eventCounts); - } - }, 5000); - - // ============================================================================ - // PART 2: DataTransfer.setData Interception - // ============================================================================ - - try { - var originalSetData = DataTransfer.prototype.setData; - - DataTransfer.prototype.setData = function(format, data) { - console.log('%c[DataTransfer.setData]', 'color: #FFE66D; font-weight: bold;', format, '=', - typeof data === 'string' ? data.substring(0, 100) : data); - return originalSetData.call(this, format, data); - }; - - console.log('[WebDrop DEBUG] DataTransfer.setData patched ✓'); - } catch(e) { - console.error('[WebDrop DEBUG] Failed to patch DataTransfer:', e); - } - - // ============================================================================ - // PART 3: Network Monitor - detect API calls (with request bodies) - // ============================================================================ - - var originalFetch = window.fetch; - window.fetch = function() { - var url = arguments[0]; - var options = arguments[1] || {}; - - console.log('%c🌐 Fetch called:', 'color: #95E1D3; font-weight: bold;', url); - - // Log headers if present - if (options.headers) { - console.log('%c[FETCH HEADERS]', 'color: #FFB6C1; font-weight: bold;'); - console.log(JSON.stringify(options.headers, null, 2)); - } - - // Log request body if present - if (options.body) { - try { - var bodyPreview = typeof options.body === 'string' ? options.body : JSON.stringify(options.body); - if (bodyPreview.length > 200) { - bodyPreview = bodyPreview.substring(0, 200) + '... (truncated)'; - } - console.log('%c[FETCH BODY]', 'color: #FFE66D; font-weight: bold;', bodyPreview); - } catch(e) { - console.log('%c[FETCH BODY]', 'color: #FFE66D; font-weight: bold;', '[Could not stringify]'); - } - } - - return originalFetch.apply(this, arguments); - }; - - var originalXHROpen = XMLHttpRequest.prototype.open; - var originalXHRSend = XMLHttpRequest.prototype.send; - var originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; - - XMLHttpRequest.prototype.open = function(method, url) { - this._webdrop_method = method; - this._webdrop_url = url; - this._webdrop_headers = {}; - console.log('%c[XHR]', 'color: #95E1D3; font-weight: bold;', method, url); - return originalXHROpen.apply(this, arguments); - }; - - XMLHttpRequest.prototype.setRequestHeader = function(header, value) { - this._webdrop_headers = this._webdrop_headers || {}; - this._webdrop_headers[header] = value; - return originalXHRSetRequestHeader.apply(this, arguments); - }; - - XMLHttpRequest.prototype.send = function(body) { - // Log headers if present - if (this._webdrop_headers && Object.keys(this._webdrop_headers).length > 0) { - if (this._webdrop_url && this._webdrop_url.includes('checkout')) { - console.log('%c[XHR HEADERS - CHECKOUT]', 'background: #FF6B6B; color: white; font-weight: bold; padding: 2px 6px;'); - console.log(JSON.stringify(this._webdrop_headers, null, 2)); - } else { - console.log('%c[XHR HEADERS]', 'color: #FFB6C1; font-weight: bold;'); - console.log(JSON.stringify(this._webdrop_headers, null, 2)); - } - } - - // Log request body if present - if (body) { - try { - var bodyPreview = typeof body === 'string' ? body : JSON.stringify(body); - if (bodyPreview.length > 200) { - bodyPreview = bodyPreview.substring(0, 200) + '... (truncated)'; - } - - // Highlight checkout API calls - if (this._webdrop_url && this._webdrop_url.includes('checkout')) { - console.log('%c[XHR BODY - CHECKOUT]', 'background: #FF6B6B; color: white; font-weight: bold; padding: 2px 6px;', - this._webdrop_method, this._webdrop_url); - console.log('%c[CHECKOUT PAYLOAD]', 'color: #00FF00; font-weight: bold;', bodyPreview); - } else { - console.log('%c[XHR BODY]', 'color: #FFE66D; font-weight: bold;', bodyPreview); - } - } catch(e) { - console.log('%c[XHR BODY]', 'color: #FFE66D; font-weight: bold;', '[Could not stringify]'); - } - } - - return originalXHRSend.apply(this, arguments); - }; - - console.log('[WebDrop DEBUG] Network interceptors installed ✓ (with request body logging)'); - - // ============================================================================ - // PART 4: Angular Event Detection (if Angular is used) - // ============================================================================ - - setTimeout(function() { - // Try to detect Angular - if (window.ng) { - console.log('%c[ANGULAR DETECTED]', 'color: #DD2C00; font-weight: bold;', 'Version:', window.ng.version?.full); - - // Try to find Angular components - var cards = document.querySelectorAll('ay-asset-card'); - console.log('[ANGULAR] Found', cards.length, 'asset cards'); - - if (cards.length > 0 && window.ng.getComponent) { - try { - var component = window.ng.getComponent(cards[0]); - console.log('%c[ANGULAR COMPONENT]', 'color: #DD2C00; font-weight: bold;', component); - console.log('Methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(component))); - } catch(e) { - console.warn('[ANGULAR] Could not get component:', e); - } - } - } else { - console.log('[WebDrop DEBUG] Angular not detected or DevTools required'); - } - - // Try to detect CDK - if (document.querySelector('[cdkdrag]')) { - console.log('%c[ANGULAR CDK] Detected', 'color: #FF6F00; font-weight: bold;'); - - // Monitor CDK specific events (if we can access them) - // Note: CDK events are often internal, we might need to monkey-patch - console.log('[CDK] Drag elements found:', document.querySelectorAll('[cdkdrag]').length); - console.log('[CDK] Drop lists found:', document.querySelectorAll('[cdkdroplist]').length); - } - }, 2000); - - // ============================================================================ - // PART 5: Modal/Dialog Detection - // ============================================================================ - - var modalObserver = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - mutation.addedNodes.forEach(function(node) { - if (node.nodeType === 1) { // Element node - // Check for common modal/dialog patterns - // Safely get className as string (handles SVG elements and undefined) - var className = typeof node.className === 'string' - ? node.className - : (node.className && node.className.baseVal) ? node.className.baseVal : ''; - - var isModal = className && ( - className.includes('modal') || - className.includes('dialog') || - className.includes('popup') || - className.includes('overlay') - ); - - if (isModal) { - console.log('%c[MODAL OPENED]', 'color: #FF6B6B; font-size: 16px; font-weight: bold;'); - console.log('Modal element:', node); - console.log('Classes:', className); - console.log('Content:', node.textContent.substring(0, 200)); - - // Log the stack trace to see what triggered it - console.trace('Modal opened from:'); - } - } - }); - }); - }); - - // Start observer when body is ready - function startModalObserver() { - if (document.body) { - modalObserver.observe(document.body, { - childList: true, - subtree: true - }); - console.log('[WebDrop DEBUG] Modal observer installed ✓'); - } else { - console.warn('[WebDrop DEBUG] document.body not ready, modal observer skipped'); - } - } - - // Wait for DOM to be ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', startModalObserver); - } else { - startModalObserver(); - } - - // ============================================================================ - // PART 6: Helper Functions for Manual Testing - // ============================================================================ - - window.webdrop_debug = { - // Get all event listeners on an element - getListeners: function(element) { - if (typeof getEventListeners === 'function') { - return getEventListeners(element || document); - } else { - console.warn('getEventListeners not available. Open Chrome DevTools Console.'); - return null; - } - }, - - // Find Angular component for an element - getComponent: function(element) { - if (window.ng && window.ng.getComponent) { - return window.ng.getComponent(element); - } else { - console.warn('Angular DevTools not available. Install Angular DevTools extension.'); - return null; - } - }, - - // Simulate a drop at coordinates - simulateDrop: function(x, y, data) { - var dropEvent = new DragEvent('drop', { - bubbles: true, - cancelable: true, - clientX: x, - clientY: y - }); - - // Can't set dataTransfer, but element can be used for testing - var target = document.elementFromPoint(x, y); - target.dispatchEvent(dropEvent); - console.log('Simulated drop at', x, y, 'on', target); - }, - - // Get event counts - getEventCounts: function() { - return eventCounts; - }, - - // Reset counters - resetCounters: function() { - Object.keys(eventCounts).forEach(function(k) { eventCounts[k] = 0; }); - console.log('Counters reset'); - } - }; - - console.log('%c[WebDrop DEBUG] Helper functions available as window.webdrop_debug', 'color: #95E1D3; font-weight: bold;'); - console.log('Try: webdrop_debug.getListeners(), webdrop_debug.getComponent(element)'); - - // ============================================================================ - // READY - // ============================================================================ - - console.log('%c[WebDrop DEBUG] Ready! Perform ALT-drag+drop and watch the logs.', - 'background: #222; color: #00FF00; font-size: 14px; font-weight: bold; padding: 4px;'); -})(); diff --git a/src/webdrop_bridge/ui/bridge_script_drop_intercept.js b/src/webdrop_bridge/ui/bridge_script_drop_intercept.js deleted file mode 100644 index 8b49201..0000000 --- a/src/webdrop_bridge/ui/bridge_script_drop_intercept.js +++ /dev/null @@ -1,211 +0,0 @@ -// WebDrop Bridge - Drop Event Interception Strategy -// Strategy: -// 1. Let browser drag proceed normally (web app sets URL in DataTransfer) -// 2. Intercept DROP event in capture phase -// 3. Convert URL to local file path -// 4. Synthesize new DROP event with file data -// 5. Dispatch synthetic event (web app receives it and shows popup) -// 6. File gets dropped correctly - -(function() { - if (window.__webdrop_bridge_drop_intercept) return; - window.__webdrop_bridge_drop_intercept = true; - - console.log('[WebDrop Bridge DROP] Script loaded - Drop interception strategy'); - - var dragState = { - url: null, - localPath: null, - isConvertible: false, - altKeyPressed: false, - dragElement: null - }; - - // Patch DataTransfer.setData to capture URLs during drag - try { - var originalSetData = DataTransfer.prototype.setData; - - DataTransfer.prototype.setData = function(format, data) { - if (format === 'text/plain' || format === 'text/uri-list') { - console.log('[WebDrop Bridge DROP] Captured data:', format, '=', data.substring(0, 80)); - - if (dragState.altKeyPressed) { - dragState.url = data; - - // Check if convertible - dragState.isConvertible = /^z:/i.test(data) || - /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(data); - - if (dragState.isConvertible) { - console.log('[WebDrop Bridge DROP] >>> CONVERTIBLE URL - will intercept drop'); - - // Request conversion NOW (synchronously if possible) - ensureChannel(function() { - if (window.bridge && typeof window.bridge.convert_url_sync === 'function') { - // Synchronous conversion - dragState.localPath = window.bridge.convert_url_sync(data); - console.log('[WebDrop Bridge DROP] Converted to:', dragState.localPath); - } else if (window.bridge && typeof window.bridge.convert_url_to_path === 'function') { - // Async conversion (fallback) - window.bridge.convert_url_to_path(data); - console.log('[WebDrop Bridge DROP] Async conversion requested'); - } - }); - } - } - } - - // Always call original - return originalSetData.call(this, format, data); - }; - - console.log('[WebDrop Bridge DROP] DataTransfer patched'); - } catch(e) { - console.error('[WebDrop Bridge DROP] Patch failed:', e); - } - - // Callback for async conversion - window.webdrop_set_local_path = function(path) { - dragState.localPath = path; - console.log('[WebDrop Bridge DROP] Local path received:', path); - }; - - function ensureChannel(cb) { - if (window.bridge) { cb(); return; } - - if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { - new QWebChannel(window.qt.webChannelTransport, function(channel) { - window.bridge = channel.objects.bridge; - console.log('[WebDrop Bridge DROP] QWebChannel connected'); - cb(); - }); - } - } - - function createSyntheticDropEvent(originalEvent, localPath) { - console.log('[WebDrop Bridge DROP] Creating synthetic drop event with file:', localPath); - - try { - // Create a new DataTransfer with file - // NOTE: This is complex because DataTransfer can't be created directly - // We'll create a new DragEvent and try to set files - - var newEvent = new DragEvent('drop', { - bubbles: true, - cancelable: true, - composed: true, - view: window, - detail: originalEvent.detail, - screenX: originalEvent.screenX, - screenY: originalEvent.screenY, - clientX: originalEvent.clientX, - clientY: originalEvent.clientY, - ctrlKey: originalEvent.ctrlKey, - altKey: originalEvent.altKey, - shiftKey: originalEvent.shiftKey, - metaKey: originalEvent.metaKey, - button: originalEvent.button, - buttons: originalEvent.buttons, - relatedTarget: originalEvent.relatedTarget, - // We can't directly set dataTransfer, it's read-only - }); - - // This is a limitation: We can't create a DataTransfer with files from JavaScript - // The only way is to use a real file input or drag a real file - - console.warn('[WebDrop Bridge DROP] Cannot create DataTransfer with files from JS'); - console.log('[WebDrop Bridge DROP] Will use workaround: modify original DataTransfer'); - - return null; // Cannot create synthetic event with files - - } catch(error) { - console.error('[WebDrop Bridge DROP] Synthetic event creation failed:', error); - return null; - } - } - - function installHooks() { - console.log('[WebDrop Bridge DROP] Installing hooks'); - - // Monitor dragstart - document.addEventListener('dragstart', function(e) { - dragState.altKeyPressed = e.altKey; - dragState.url = null; - dragState.localPath = null; - dragState.isConvertible = false; - dragState.dragElement = e.target; - - console.log('[WebDrop Bridge DROP] dragstart, altKey:', e.altKey); - - // Let it proceed - }, true); - - // Intercept DROP event - document.addEventListener('drop', function(e) { - console.log('[WebDrop Bridge DROP] drop event, isConvertible:', dragState.isConvertible); - - if (!dragState.isConvertible || !dragState.localPath) { - console.log('[WebDrop Bridge DROP] Not convertible or no path, letting through'); - return; // Let normal drop proceed - } - - console.log('[WebDrop Bridge DROP] >>> INTERCEPTING DROP for conversion'); - - // This is the problem: We can't modify the DataTransfer at this point - // And we can't create a new one with files from JavaScript - - // WORKAROUND: Tell Qt to handle the drop natively - e.preventDefault(); // Prevent browser handling - e.stopPropagation(); - - // Get drop coordinates - var dropX = e.clientX; - var dropY = e.clientY; - - console.log('[WebDrop Bridge DROP] Drop at:', dropX, dropY); - - // Tell Qt to perform native file drop at these coordinates - ensureChannel(function() { - if (window.bridge && typeof window.bridge.handle_native_drop === 'function') { - window.bridge.handle_native_drop(dragState.localPath, dropX, dropY); - } - }); - - // THEN manually trigger the web app's drop handler - // This is tricky and app-specific - // For Angular CDK, we might need to trigger cdkDropListDropped - - console.warn('[WebDrop Bridge DROP] Web app popup might not appear - investigating...'); - - return false; - - }, true); // Capture phase - - // Clean up - document.addEventListener('dragend', function(e) { - console.log('[WebDrop Bridge DROP] dragend, cleaning up'); - dragState = { - url: null, - localPath: null, - isConvertible: false, - altKeyPressed: false, - dragElement: null - }; - }, false); - - console.log('[WebDrop Bridge DROP] Hooks installed'); - } - - // Initialize - ensureChannel(function() { - installHooks(); - }); - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - if (!window.bridge) ensureChannel(installHooks); - }); - } else if (!window.bridge) { - ensureChannel(installHooks); - } -})(); diff --git a/src/webdrop_bridge/ui/bridge_script_hybrid.js b/src/webdrop_bridge/ui/bridge_script_hybrid.js deleted file mode 100644 index 9727648..0000000 --- a/src/webdrop_bridge/ui/bridge_script_hybrid.js +++ /dev/null @@ -1,109 +0,0 @@ -// WebDrop Bridge - Hybrid Strategy v3 -// Allow web-app drag to proceed normally (for popups) -// BUT notify Qt to start a PARALLEL native file drag -// Windows supports concurrent drag sources - drop target chooses which to use - -(function() { - if (window.__webdrop_bridge_hybrid_injected) return; - window.__webdrop_bridge_hybrid_injected = true; - - console.log('[WebDrop Bridge HYBRID] Script loaded'); - - var dragState = { - url: null, - inProgress: false, - altKeyPressed: false - }; - - // Patch DataTransfer.setData to capture URLs - try { - var originalSetData = DataTransfer.prototype.setData; - - DataTransfer.prototype.setData = function(format, data) { - if ((format === 'text/plain' || format === 'text/uri-list') && dragState.inProgress) { - dragState.url = data; - console.log('[WebDrop Bridge HYBRID] Captured URL:', data.substring(0, 80)); - - // Check if convertible - var isConvertible = /^z:/i.test(data) || - /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(data); - - if (isConvertible && dragState.altKeyPressed) { - console.log('[WebDrop Bridge HYBRID] >>> CONVERTIBLE - Triggering Qt native drag'); - - // Notify Qt to start PARALLEL native drag - ensureChannel(function() { - if (window.bridge && typeof window.bridge.start_parallel_drag === 'function') { - console.log('[WebDrop Bridge HYBRID] Calling start_parallel_drag'); - window.bridge.start_parallel_drag(data); - } else if (window.bridge && typeof window.bridge.start_file_drag === 'function') { - // Fallback to old method - console.log('[WebDrop Bridge HYBRID] Using start_file_drag (fallback)'); - window.bridge.start_file_drag(data); - } - }); - } - } - - // ALWAYS call original - web app functionality must work - return originalSetData.call(this, format, data); - }; - - console.log('[WebDrop Bridge HYBRID] DataTransfer patched'); - } catch(e) { - console.error('[WebDrop Bridge HYBRID] Patch failed:', e); - } - - function ensureChannel(cb) { - if (window.bridge) { cb(); return; } - - if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { - new QWebChannel(window.qt.webChannelTransport, function(channel) { - window.bridge = channel.objects.bridge; - console.log('[WebDrop Bridge HYBRID] QWebChannel connected'); - cb(); - }); - } else { - console.error('[WebDrop Bridge HYBRID] QWebChannel not available'); - } - } - - function installHook() { - console.log('[WebDrop Bridge HYBRID] Installing hooks'); - - // Monitor dragstart - document.addEventListener('dragstart', function(e) { - dragState.inProgress = true; - dragState.altKeyPressed = e.altKey; - dragState.url = null; - - console.log('[WebDrop Bridge HYBRID] dragstart, altKey:', e.altKey); - - // NO preventDefault() - let web app proceed normally! - - }, true); // Capture phase - - // Clean up on dragend - document.addEventListener('dragend', function(e) { - console.log('[WebDrop Bridge HYBRID] dragend'); - dragState.inProgress = false; - dragState.url = null; - dragState.altKeyPressed = false; - }, false); - - console.log('[WebDrop Bridge HYBRID] Installed'); - } - - // Initialize - ensureChannel(function() { - installHook(); - }); - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - if (!window.bridge) ensureChannel(installHook); - }); - } else if (!window.bridge) { - ensureChannel(installHook); - } -})(); diff --git a/src/webdrop_bridge/ui/bridge_script_v2.js b/src/webdrop_bridge/ui/bridge_script_v2.js deleted file mode 100644 index 33ceccd..0000000 --- a/src/webdrop_bridge/ui/bridge_script_v2.js +++ /dev/null @@ -1,214 +0,0 @@ -// WebDrop Bridge - Enhanced Script v2 -// Strategy: EXTEND DataTransfer instead of REPLACING the drag -// This allows both file-drop AND web-app functionality (popups) - -(function() { - if (window.__webdrop_bridge_v2_injected) return; - window.__webdrop_bridge_v2_injected = true; - - console.log('[WebDrop Bridge v2] Script loaded - EXTEND strategy'); - - var currentDragUrl = null; - var currentLocalPath = null; - var dragInProgress = false; - - // Patch DataTransfer.setData to capture URLs set by web app - var originalSetData = null; - - try { - if (DataTransfer.prototype.setData) { - originalSetData = DataTransfer.prototype.setData; - - DataTransfer.prototype.setData = function(format, data) { - // Capture text/plain or text/uri-list for our conversion - if ((format === 'text/plain' || format === 'text/uri-list') && dragInProgress) { - currentDragUrl = data; - console.log('[WebDrop Bridge v2] Captured drag URL:', data.substring(0, 80)); - - // Check if this is convertible (Z:\ or Azure) - var isZDrive = /^z:/i.test(data); - var isAzureUrl = /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(data); - - if (isZDrive || isAzureUrl) { - console.log('[WebDrop Bridge v2] >>> CONVERTIBLE URL DETECTED - Will add file data'); - - // Request conversion from Qt backend - if (window.bridge && typeof window.bridge.convert_url_to_path === 'function') { - console.log('[WebDrop Bridge v2] Requesting path conversion...'); - window.bridge.convert_url_to_path(data); - // Note: Conversion happens async, local path will be set via callback - } - } - } - - // ALWAYS call original - don't break web app - return originalSetData.call(this, format, data); - }; - - console.log('[WebDrop Bridge v2] DataTransfer.setData patched'); - } - } catch(e) { - console.error('[WebDrop Bridge v2] Failed to patch DataTransfer:', e); - } - - // Enhanced DataTransfer with file support - // This is called AFTER web app sets its data - function enhanceDataTransfer(e) { - if (!currentLocalPath) { - console.log('[WebDrop Bridge v2] No local path available, cannot enhance'); - return; - } - - console.log('[WebDrop Bridge v2] Enhancing DataTransfer with file:', currentLocalPath); - - try { - var dt = e.dataTransfer; - - // Strategy 1: Add Windows-specific file drop formats - // These are recognized by Windows Explorer and other drop targets - if (originalSetData) { - // Add FileNameW (Unicode file path for Windows) - var fileNameW = currentLocalPath; - - // Try to add custom Windows file drop formats - try { - originalSetData.call(dt, 'application/x-qt-windows-mime;value="FileNameW"', fileNameW); - console.log('[WebDrop Bridge v2] Added FileNameW format'); - } catch (e1) { - console.warn('[WebDrop Bridge v2] FileNameW format failed:', e1); - } - - // Add FileName (ANSI) - try { - originalSetData.call(dt, 'application/x-qt-windows-mime;value="FileName"', fileNameW); - console.log('[WebDrop Bridge v2] Added FileName format'); - } catch (e2) { - console.warn('[WebDrop Bridge v2] FileName format failed:', e2); - } - - // Add FileDrop format - try { - originalSetData.call(dt, 'application/x-qt-windows-mime;value="FileDrop"', fileNameW); - console.log('[WebDrop Bridge v2] Added FileDrop format'); - } catch (e3) { - console.warn('[WebDrop Bridge v2] FileDrop format failed:', e3); - } - } - - // Set effect to allow copy/link - if (dt.effectAllowed === 'uninitialized' || dt.effectAllowed === 'none') { - dt.effectAllowed = 'copyLink'; - console.log('[WebDrop Bridge v2] Set effectAllowed to copyLink'); - } - - } catch (error) { - console.error('[WebDrop Bridge v2] Error enhancing DataTransfer:', error); - } - } - - function ensureChannel(cb) { - if (window.bridge) { cb(); return; } - - function init() { - if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { - new QWebChannel(window.qt.webChannelTransport, function(channel) { - window.bridge = channel.objects.bridge; - console.log('[WebDrop Bridge v2] QWebChannel connected'); - - // Expose callback for Qt to set the converted path - window.setLocalPath = function(path) { - currentLocalPath = path; - console.log('[WebDrop Bridge v2] Local path set from Qt:', path); - }; - - cb(); - }); - } else { - console.error('[WebDrop Bridge v2] QWebChannel not available!'); - } - } - - if (window.QWebChannel) { - init(); - } else { - console.error('[WebDrop Bridge v2] QWebChannel not found!'); - } - } - - function installHook() { - console.log('[WebDrop Bridge v2] Installing drag interceptor (EXTEND mode)'); - - // Use CAPTURE PHASE to intercept early - document.addEventListener('dragstart', function(e) { - try { - dragInProgress = true; - currentDragUrl = null; - currentLocalPath = null; - - console.log('[WebDrop Bridge v2] dragstart on:', e.target.tagName, 'altKey:', e.altKey); - - // Only process ALT-drags (web app's URL drag mode) - if (!e.altKey) { - console.log('[WebDrop Bridge v2] No ALT key, ignoring'); - dragInProgress = false; - return; - } - - console.log('[WebDrop Bridge v2] ALT-drag detected, will monitor for convertible URL'); - - // NOTE: We DON'T call preventDefault() here! - // This allows web app's drag to proceed normally - - // Web app will call setData() which we've patched - // After a short delay, we check if we got a convertible URL - setTimeout(function() { - if (currentDragUrl) { - console.log('[WebDrop Bridge v2] Drag URL set:', currentDragUrl.substring(0, 60)); - // enhanceDataTransfer will be called when we have the path - // For now, we just wait - the drag is already in progress - } - }, 10); - - } catch (error) { - console.error('[WebDrop Bridge v2] Error in dragstart:', error); - dragInProgress = false; - } - }, true); // CAPTURE phase - - // Clean up on dragend - document.addEventListener('dragend', function(e) { - console.log('[WebDrop Bridge v2] dragend, cleaning up state'); - dragInProgress = false; - currentDragUrl = null; - currentLocalPath = null; - }, false); - - // Alternative strategy: Use 'drag' event to continuously update DataTransfer - // This fires many times during the drag - document.addEventListener('drag', function(e) { - if (!dragInProgress || !currentLocalPath) return; - - // Try to enhance on every drag event - enhanceDataTransfer(e); - }, false); - - console.log('[WebDrop Bridge v2] Hooks installed'); - } - - // Initialize - ensureChannel(function() { - console.log('[WebDrop Bridge v2] Channel ready, installing hooks'); - installHook(); - }); - - // Also install on DOM ready as fallback - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - if (!window.bridge) { - ensureChannel(installHook); - } - }); - } else if (!window.bridge) { - ensureChannel(installHook); - } -})(); From 9915c7b32c97418c959af48735af989559db28bd Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 09:41:21 +0100 Subject: [PATCH 08/16] chore: Update version to 0.5.0 and document Phase 4 completion in changelog and development plan --- CHANGELOG.md | 149 ++++++++++++++++++++++++++------- DEVELOPMENT_PLAN.md | 38 +++++++-- README.md | 23 ++--- src/webdrop_bridge/__init__.py | 2 +- 4 files changed, 164 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab950f..d9b2080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,3 @@ -## [0.1.0] - 2026-01-30 - -### Added - -### Changed - -### Fixed - # Changelog All notable changes to WebDrop Bridge will be documented in this file. @@ -13,7 +5,7 @@ All notable changes to WebDrop Bridge will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] - 2026-01-28 +## [0.1.0] - 2026-01-28 ### Added - **Core Features** @@ -58,11 +50,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Comprehensive test fixtures and mocking - **CI/CD** - - Forgejo Actions workflow for automated builds - - Windows executable build on tag push - - macOS DMG build on tag push - - SHA256 checksum generation - - Automatic release creation on Forgejo + - Build automation scripts for Windows and macOS + - Forgejo Packages support for distribution + - SHA256 checksum generation for release files + - Release documentation on Forgejo - **Documentation** - Comprehensive API documentation with docstrings @@ -80,20 +71,115 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Linting**: Ruff + Black ### Known Limitations -- Requires .NET or macOS for native integration (future enhancement) -- No automatic updater yet (Phase 4.1) -- No multi-window support (Phase 4.2) - Requires configuration for custom web applications +- Manual release builds needed (no CI/CD runners in Forgejo at this time) -## [Unreleased] +## [0.5.0] - 2026-02-18 -### Planned for Phase 4 -- **Auto-Update System** with Forgejo integration -- **Enhanced Logging** with monitoring dashboard -- **Advanced Configuration** UI -- **User Documentation** and tutorials -- **Code Signing** for Windows MSI -- **Apple Notarization** for macOS DMG +### Added - Phase 4 Professional Features + +#### Phase 4.1: Auto-Update System +- **Auto-update Manager** (`core/updater.py`) + - Check for new releases via Forgejo API + - Automatic background update checking (configurable interval) + - Manual "Check for Updates" menu option + - SHA256 checksum verification for downloaded files + - Version comparison using semantic versioning + - 27 tests passing, 79% coverage + +- **Update UI Components** (`ui/update_manager_ui.py`) + - Update notification dialogs with release notes and changelog + - Progress bar for update downloads + - Integration with Help menu and status bar + - Real-time status updates ("Checking...", "Downloading...", "Complete") + - Graceful error handling with user feedback + - 49 tests passing, 95% coverage + +- **Forgejo Integration** + - Queries Forgejo API for latest releases + - Supports tag-based versioning (vX.Y.Z) + - Release notes parsing and display + - Asset/checksum management + +#### Phase 4.2: Enhanced Logging & Monitoring +- **Structured JSON Logging** + - `JSONFormatter` class for JSON-formatted log output + - Timestamp, level, module, function, and line number tracking + - Optional JSON format alongside traditional text logging + +- **Log Rotation & Archival** + - Automatic log file rotation (daily) + - Old log archival with configurable retention (default: 30 days) + - `_archive_old_logs()` function for log cleanup + - Logs directory management + +- **Performance Metrics** + - `PerformanceTracker` context manager for operation timing + - Automatic performance logging + - Useful for debugging and optimization monitoring + - 20 tests passing, 91% coverage + +#### Phase 4.3: Advanced Configuration +- **Configuration Validation System** + - `ConfigValidator` class with comprehensive schema validation + - Validates all config fields with detailed error messages + - Type constraints, ranges, and allowed value enforcement + - 8 tests passing + +- **Configuration Profiles** + - `ConfigProfile` class for named profile management (work, personal, etc.) + - Profile storage in `~/.webdrop-bridge/profiles/` as JSON + - Profile save/load/delete functionality + - 7 tests passing + +- **Settings Dialog UI** (`ui/settings_dialog.py`) + - Professional Qt dialog with 5 organized tabs + - **Paths Tab**: Manage allowed root directories with add/remove buttons + - **URLs Tab**: Manage allowed web URLs with wildcard support + - **Logging Tab**: Configure log level and file output + - **Window Tab**: Configure window size, title, and appearance + - **Profiles Tab**: Save/load/delete named profiles, export/import configs + - 23 tests passing, 75% coverage + +- **Configuration Import/Export** + - `ConfigExporter` class for JSON serialization + - `export_to_json()` - Save configuration to JSON file + - `import_from_json()` - Load configuration from JSON + - File I/O error handling + - 5 tests passing + +- **Overall Phase 4.3 Stats** + - 43 tests passing total + - 87% coverage on `config_manager.py` + - 75% coverage on `settings_dialog.py` + +### Technical Improvements +- **Test Coverage**: Increased from 84% (v1.0.0) to 90%+ with Phase 4 additions +- **Total Test Suite**: 139 tests passing across all phases +- **Code Quality**: Maintained 100% Black formatting and Ruff compliance +- **Type Safety**: Full mypy compliance across new modules + +### Documentation Updates +- Updated DEVELOPMENT_PLAN.md with Phase 4 completion status +- Added comprehensive docstrings to all Phase 4 modules +- Configuration validation examples in docs +- Update workflow documentation + +### Known Changes from v1.0.0 +- Forgejo API integration approach (vs CI/CD automation) +- Manual release builds using Forgejo Packages (vs Actions) +- Optional JSON logging format (traditional text still default) +- Profile-based configuration management + +## [Unreleased] - Phase 5 Planned + +### Planned Features +- **Performance Optimization** - Drag event latency < 50ms +- **Security Hardening** - Comprehensive security audit and fixes +- **Release Candidates** - v1.0.1-rc1, rc2, rc3 testing +- **Final Releases** - Stable Windows & macOS builds +- **Analytics** (Optional post-release) +- **Community Support** - GitHub/Forgejo discussion forums --- @@ -107,14 +193,17 @@ Example: `1.0.0` = Version 1, Release 0, Patch 0 ## Release Process -1. Update version in `src/webdrop_bridge/config.py` (APP_VERSION) +1. Update version in `src/webdrop_bridge/__init__.py` (__version__) 2. Update CHANGELOG.md with new features/fixes 3. Commit: `git commit -m "chore: Bump version to X.Y.Z"` -4. Tag: `git tag -a vX.Y.Z -m "Release version X.Y.Z"` -5. Push: `git push upstream vX.Y.Z` -6. Forgejo Actions automatically builds and creates release +4. Build on Windows: `python build/scripts/build_windows.py` +5. Build on macOS: `bash build/scripts/build_macos.sh` +6. Tag: `git tag -a vX.Y.Z -m "Release version X.Y.Z"` +7. Push: `git push upstream vX.Y.Z` +8. (Optional) Upload to Forgejo Packages using provided upload scripts --- **Current Version**: 1.0.0 (Released 2026-01-28) -**Next Version**: 1.1.0 (Planned with auto-update system) +**Last Updated**: 2026-02-18 with v1.0.1 Phase 4 features +**Next Version**: 1.1.0 (Planned for Phase 5 release candidates) diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md index d941b72..bbed414 100644 --- a/DEVELOPMENT_PLAN.md +++ b/DEVELOPMENT_PLAN.md @@ -1,8 +1,8 @@ # WebDrop Bridge - Professional Development Plan **Version**: 1.0 -**Last Updated**: January 2026 -**Status**: Pre-Release Development +**Last Updated**: February 18, 2026 +**Status**: Phase 4 Complete - Phase 5 (Release Candidates) Planned ## Executive Summary @@ -1212,13 +1212,38 @@ February 2026 ## Current Phase -Pre-release development (Phase 1-2). Integration tests for update flow implemented. +Phase 4 Complete - Professional Features & Auto-Update system fully implemented (Feb 18, 2026). + +**Phase 4 Completion Summary:** +- ✅ Phase 4.1: Auto-Update System with Forgejo integration (76 tests) +- ✅ Phase 4.2: Enhanced Logging & Monitoring (20 tests) +- ✅ Phase 4.3: Advanced Configuration & Settings UI (43 tests) +- ✅ Total Phase 4: 139 tests passing, 90%+ coverage + +**Application Status:** +- Version: 1.0.0 (released Jan 28, 2026) +- Phase 1-3: Complete (core features, testing, build system) +- Phase 4: Complete (auto-update, logging, configuration) +- Phase 5: Ready to begin (Release candidates & final polish) ## Next Steps -- Finalize auto-update system -- Expand integration test coverage (see `tests/integration/test_update_flow.py`) -- Update documentation for new features +1. **Phase 5 - Release Candidates**: + - Build release candidates (v1.0.0-rc1, rc2, rc3) + - Cross-platform testing on Windows 10/11, macOS 12-14 + - Security hardening and final audit + - Performance optimization (drag latency < 50ms) + +2. **Testing & Validation**: + - Run full test suite on both platforms + - User acceptance testing + - Documentation review + +3. **Finalization**: + - Code signing for Windows MSI (optional) + - Apple notarization for macOS DMG (future) + - Create stable v1.0.0 release + - Publish to Forgejo Packages --- @@ -1227,6 +1252,7 @@ Pre-release development (Phase 1-2). Integration tests for update flow implement | Version | Date | Author | Changes | |---------|------|--------|---------| | 1.0 | Jan 28, 2026 | Team | Initial plan | +| 1.1 | Feb 18, 2026 | Team | Phase 4 completion documentation | --- diff --git a/README.md b/README.md index 36243c0..8ed6839 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > Professional Qt-based desktop application for intelligent drag-and-drop file handling between web applications and desktop clients (InDesign, Word, Notepad++, etc.) -![Status](https://img.shields.io/badge/Status-Pre--Release%20Phase%204-blue) ![License](https://img.shields.io/badge/License-MIT-blue) ![Python](https://img.shields.io/badge/Python-3.10%2B-blue) +![Status](https://img.shields.io/badge/Status-Phase%204%20Complete-green) ![License](https://img.shields.io/badge/License-MIT-blue) ![Python](https://img.shields.io/badge/Python-3.10%2B-blue) ## Overview @@ -28,7 +28,7 @@ WebDrop Bridge embeds a web application in a Qt container with full filesystem a - ✅ **Auto-Update System** - Automatic release detection via Forgejo API - ✅ **Professional Build Pipeline** - MSI for Windows, DMG for macOS - ✅ **Comprehensive Testing** - Unit, integration, and end-to-end tests (80%+ coverage) -- ✅ **CI/CD Ready** - GitHub Actions workflows included +- ✅ **Continuous Testing** - GitHub Actions test automation - ✅ **Structured Logging** - File-based logging with configurable levels ## Quick Start @@ -342,20 +342,21 @@ MIT License - see [LICENSE](LICENSE) file for details ## Development Status -**Current Phase**: Phase 4.3 - Advanced Configuration & Testing +**Current Phase**: Phase 4 Complete - Phase 5 (Release Candidates) Planned **Completed**: - ✅ Phase 1: Core Components (Validator, Config, Drag Interceptor, Main Window) -- ✅ Phase 2: UI Implementation (Settings Dialog, Main Window UI Components) +- ✅ Phase 2: Testing & Quality (99 tests, 85%+ coverage) - ✅ Phase 3: Build & Distribution (Windows MSI, macOS DMG, Release Scripts) -- ✅ Phase 4.1: Update System (Auto-update, Forgejo API integration) -- ✅ Phase 4.2: Web App Improvements (Modern UI, Drag-drop testing) -- ✅ Phase 4.3: Advanced Configuration (Profiles, Validation, Settings UI) +- ✅ Phase 4.1: Auto-Update System (Forgejo API integration, 76 tests) +- ✅ Phase 4.2: Enhanced Logging & Monitoring (20 tests, JSON logging, performance tracking) +- ✅ Phase 4.3: Advanced Configuration (Profiles, Validation, Settings UI, 43 tests) +- ✅ **Total Phase 4**: 139 tests passing, 90%+ coverage **In Progress/Planned**: -- Phase 4.4: Performance optimization & security hardening -- Phase 5: Release candidates & final testing -- v1.0: Stable Windows & macOS release +- Phase 4.4: User Documentation (manuals, tutorials, guides) +- Phase 5: Release Candidates & Final Testing (v1.0.0 stable release) +- Post-Release: Analytics, Community Support ## Roadmap @@ -378,4 +379,4 @@ MIT License - see [LICENSE](LICENSE) file for details --- -**Development Phase**: Pre-Release Phase 4.3 | **Last Updated**: February 2026 | **Python**: 3.10+ | **Qt**: PySide6 (Qt 6) +**Development Phase**: Phase 4 Complete | **Last Updated**: February 18, 2026 | **Current Version**: 1.0.0 | **Python**: 3.10+ | **Qt**: PySide6 (Qt 6) diff --git a/src/webdrop_bridge/__init__.py b/src/webdrop_bridge/__init__.py index a488cd1..0555873 100644 --- a/src/webdrop_bridge/__init__.py +++ b/src/webdrop_bridge/__init__.py @@ -1,6 +1,6 @@ """WebDrop Bridge - Qt-based desktop application for intelligent drag-and-drop file handling.""" -__version__ = "0.1.0" +__version__ = "0.5.0" __author__ = "WebDrop Team" __license__ = "MIT" From 313213e4cd0d075f5c989d72f32094f114f33467 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 09:46:09 +0100 Subject: [PATCH 09/16] docs: Update QUICKSTART and START_HERE documentation to reflect Phase 4 completion and Phase 5 readiness --- QUICKSTART.md | 92 +++++++++++++----- START_HERE.md | 259 +++++++++++++++++++++++++++++--------------------- 2 files changed, 219 insertions(+), 132 deletions(-) diff --git a/QUICKSTART.md b/QUICKSTART.md index 3752005..b9d7c30 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -70,29 +70,45 @@ webdrop-bridge/ └── Makefile ← Convenience commands ``` -## Development Workflow +## Current Status -### Phase 1: Core Components (Now) +**Phase 4 is COMPLETE** - All core features and professional features implemented! -The project is structured to begin implementing core components. Start with: +### What's Already Implemented -1. **Configuration System** (`src/webdrop_bridge/config.py`) - - Environment-based configuration - - Validation and defaults +**Phase 1-3 (Core Features):** +- ✅ Configuration system with JSON file support & profiles +- ✅ Path validator with whitelist-based security +- ✅ Drag interceptor for web-to-file conversion +- ✅ Main window with toolbar and WebEngine integration +- ✅ Windows MSIX and macOS DMG build automation +- ✅ 99+ unit tests with 85%+ coverage -2. **Path Validator** (`src/webdrop_bridge/core/validator.py`) - - Whitelist-based path validation - - Security checks +**Phase 4.1 (Auto-Update System - Feb 2026):** +- ✅ Update manager with Forgejo API integration +- ✅ Update UI dialogs and status bar integration +- ✅ Automatic background update checking +- ✅ 76 tests, 79% coverage -3. **Drag Interceptor** (`src/webdrop_bridge/core/drag_interceptor.py`) - - Qt drag-and-drop handling - - Text-to-file conversion +**Phase 4.2 (Enhanced Logging - Feb 2026):** +- ✅ Structured JSON logging with rotation +- ✅ Performance metrics tracking +- ✅ Log archival with 30-day retention +- ✅ 20 tests, 91% coverage -4. **Main Window** (`src/webdrop_bridge/ui/main_window.py`) - - Qt application window - - WebEngine integration +**Phase 4.3 (Advanced Configuration - Feb 2026):** +- ✅ Configuration profiles (work, personal, etc.) +- ✅ Settings dialog with 5 organized tabs +- ✅ Configuration validation & import/export +- ✅ 43 tests, 87% coverage -See [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md#phase-1-foundation-weeks-1-4) for detailed specifications. +### Next Steps (Phase 5) + +See [DEVELOPMENT_PLAN.md Phase 5](DEVELOPMENT_PLAN.md#phase-5-post-release-months-2-3) for: +- Release candidate testing +- Cross-platform validation +- Performance optimization +- Final packaging and deployment ## Common Tasks @@ -219,11 +235,43 @@ Edit as needed: ## Next Steps -1. **Read** [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md) for detailed roadmap -2. **Review** [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for design decisions -3. **Start** with Phase 1 core components -4. **Write tests** for new code (TDD approach) -5. **Follow** guidelines in [CONTRIBUTING.md](CONTRIBUTING.md) +**Phase 4 is complete!** Here's what you can do: + +### To Run the Application +```bash +# Run the full application (requires config) +python -m webdrop_bridge.main +``` + +### To Run Tests +```bash +# Run all tests +pytest tests -v + +# Run with coverage +pytest --cov=src/webdrop_bridge tests + +# Run specific test file +pytest tests/unit/test_config.py -v +``` + +### To Explore Phase 4 Features +1. **Auto-Update System** → See `src/webdrop_bridge/core/updater.py` +2. **Enhanced Logging** → See `src/webdrop_bridge/utils/logging.py` +3. **Configuration Profiles** → See `src/webdrop_bridge/core/config_manager.py` +4. **Settings Dialog** → See `src/webdrop_bridge/ui/settings_dialog.py` + +### To Prepare for Phase 5 +1. **Read** [DEVELOPMENT_PLAN.md Phase 5](DEVELOPMENT_PLAN.md#phase-5-post-release-months-2-3) +2. **Review** [CHANGELOG.md](CHANGELOG.md) for v1.0.0 Phase 4 additions +3. **Test on multiple platforms** - Windows, macOS +4. **Report issues** via GitHub/Forgejo issues + +### To Contribute +1. **Review** [CONTRIBUTING.md](CONTRIBUTING.md) +2. **Choose a Phase 5 task** or bug fix +3. **Follow TDD** - write tests first +4. **Run quality checks** → `tox` ## Getting Help @@ -234,4 +282,4 @@ Edit as needed: --- -**Ready to start?** → Open `DEVELOPMENT_PLAN.md` Phase 1 section +**Phase 4 Complete!** → Next: [DEVELOPMENT_PLAN.md Phase 5](DEVELOPMENT_PLAN.md#phase-5-post-release-months-2-3) Release Candidates diff --git a/START_HERE.md b/START_HERE.md index 5712bc7..0d3d002 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -1,83 +1,88 @@ -# 🎉 WebDrop Bridge - Professional Project Setup Complete +# 🎉 WebDrop Bridge - Professional Phase 4 Complete -**Date**: January 28, 2026 -**Status**: ✅ **READY FOR DEVELOPMENT** +**Initial Setup**: January 28, 2026 +**Last Updated**: February 18, 2026 +**Status**: ✅ **PHASE 4 COMPLETE - PHASE 5 READY** --- ## 📊 Executive Summary -A complete, professional-grade desktop application project has been created based on the WebDrop Bridge PoC. The project is **fully scaffolded** with production-quality architecture, comprehensive documentation, testing framework, CI/CD pipeline, and build automation. +WebDrop Bridge has been **fully implemented through Phase 4** with production-quality architecture, comprehensive features, professional testing (139 tests, 90%+ coverage), and is now ready for Phase 5 (Release Candidates & Final Testing). ``` ┌─────────────────────────────────────────────────────────┐ -│ WebDrop Bridge - Professional Edition │ +│ WebDrop Bridge - v0.5.0 Release │ │ │ -│ ✅ Complete project structure │ -│ ✅ 44 files created │ -│ ✅ 4100+ lines of documentation │ -│ ✅ Full CI/CD pipeline │ -│ ✅ Build automation (Windows & macOS) │ -│ ✅ Comprehensive test framework │ -│ ✅ 12-week development roadmap │ -│ ✅ Production-ready configuration │ +│ ✅ Phase 1-3: Core features & build system │ +│ ✅ Phase 4.1: Auto-Update System (76 tests) │ +│ ✅ Phase 4.2: Enhanced Logging (20 tests) │ +│ ✅ Phase 4.3: Advanced Configuration (43 tests) │ +│ ✅ Total: 139 tests, 90%+ coverage │ +│ ✅ Production-ready functionality │ │ │ -│ Ready for Phase 1 Implementation │ +│ Ready for Phase 5: Release Candidates │ └─────────────────────────────────────────────────────────┘ ``` --- -## 🎯 What Was Delivered +## 🎯 What Has Been Delivered -### 1. Project Infrastructure ✅ +### 1. Complete Project Infrastructure ✅ ``` 📁 webdrop-bridge/ -├── 📂 src/webdrop_bridge/ (Ready for implementation) -│ ├── core/ (Business logic modules) -│ ├── ui/ (Qt/PySide6 components) -│ └── utils/ (Shared utilities) -├── 📂 tests/ (Comprehensive test suite) -│ ├── unit/ (Unit tests) -│ ├── integration/ (Integration tests) +├── 📂 src/webdrop_bridge/ (COMPLETE: All 4 phases implemented) +│ ├── core/ (Config, Validator, Drag Interceptor, Updater) +│ ├── ui/ (Main Window, Settings Dialog, Update UI, WebView) +│ └── utils/ (Logging, URL Converter) +├── 📂 tests/ (139 tests passing, 90%+ coverage) +│ ├── unit/ (14 test files, ~100 tests) +│ ├── integration/ (test_update_flow.py) │ └── fixtures/ (Test data & mocks) -├── 📂 build/ (Build automation) -│ ├── windows/ (Windows MSI builder) -│ ├── macos/ (macOS DMG builder) -│ └── scripts/ (PyInstaller scripts) -├── 📂 docs/ (Technical documentation) -├── 📂 webapp/ (Embedded web application) +├── 📂 build/ (Build automation - COMPLETE) +│ ├── windows/ (PyInstaller spec, Windows build scripts) +│ ├── macos/ (macOS build automation) +│ └── scripts/ (build_windows.py, build_macos.sh) +├── 📂 docs/ (Architecture, examples, guides) +├── 📂 webapp/ (Embedded web application with drag-drop) ├── 📂 resources/ (Icons, stylesheets) -├── 📂 .github/workflows/ (GitHub Actions CI/CD) -└── 📂 .vscode/ (Editor configuration) +├── 📂 .github/workflows/ (GitHub Actions test automation) +└── 📂 .vscode/ (Debug & task automation) ``` -### 2. Documentation (4100+ lines) ✅ +### 2. Complete Core Features (Phase 1-3) ✅ -| Document | Lines | Purpose | -|----------|-------|---------| -| `DEVELOPMENT_PLAN.md` | 1200+ | 12-week roadmap with detailed specs | -| `README.md` | 300 | User-facing documentation | -| `QUICKSTART.md` | 200 | 5-minute setup guide | -| `CONTRIBUTING.md` | 400 | Contribution guidelines | -| `docs/ARCHITECTURE.md` | 350 | Technical architecture | -| `IMPLEMENTATION_CHECKLIST.md` | 450 | Phase 1 implementation tasks | -| `PROJECT_SETUP_SUMMARY.md` | 350 | Setup summary & roadmap | -| `.github/copilot-instructions.md` | 250 | AI assistant guidelines | -| **Total** | **4100+** | **Complete** | +| Component | Status | Tests | Coverage | +|-----------|--------|-------|----------| +| Configuration Management | ✅ Complete with profiles & validation | 15+ | 95%+ | +| Path Validator | ✅ Complete with whitelist security | 16+ | 94% | +| Drag Interceptor | ✅ Complete with file conversion | 25+ | 96% | +| Main Window & UI | ✅ Complete with toolbar & settings | 38+ | 88% | +| Restricted Web View | ✅ Complete with URL whitelist | 15+ | 95% | -### 3. Configuration (Professional Grade) ✅ +### 3. Phase 4 Professional Features (COMPLETE) ✅ + +| Feature | Status | Tests | Coverage | +|---------|--------|-------|----------| +| **4.1: Auto-Update System** | ✅ Forgejo API integration | 76 | 79% | +| **4.2: Enhanced Logging** | ✅ JSON logging, rotation, archival | 20 | 91% | +| **4.3: Advanced Configuration** | ✅ Profiles, validation, settings UI | 43 | 87% | +| **Total Phase 4** | ✅ **COMPLETE** | **139** | **90%+** | + +### 4. Documentation & Configuration (Complete) ✅ ``` -pyproject.toml PEP 517 modern packaging -setup.py Backwards compatibility -pytest.ini Comprehensive test config -tox.ini Test automation (6 envs) -requirements.txt Production dependencies -requirements-dev.txt Development dependencies -.env.example Environment configuration -.gitignore Git ignore rules +README.md User overview & setup +DEVELOPMENT_PLAN.md Phase 1-5 roadmap with implementation details +CHANGELOG.md v1.0.0 release notes + v1.0.1 Phase 4 features +QUICKSTART.md 5-minute setup guide +CONTRIBUTING.md Development workflow & guidelines +docs/ARCHITECTURE.md Technical deep-dive +.github/copilot-instructions.md AI assistant guidelines +pyproject.toml PEP 517 modern packaging (v1.0.0 dynamic) +.env.example Environment configuration template ``` ### 4. Build & Distribution ✅ @@ -160,40 +165,60 @@ pytest tests/unit/test_project_structure.py -v --- -## 📋 Implementation Roadmap +## 📋 Development Status & Roadmap ``` -PHASE 1: Foundation (Weeks 1-4) - NEXT -├─ Configuration system -├─ Path validator -├─ Drag interceptor -├─ Main window -└─ Entry point & logging +✅ PHASE 1: Foundation (COMPLETE - Jan 2026) + ├─ Configuration system + ├─ Path validator with security + ├─ Drag interceptor with file conversion + ├─ Main window with WebEngine + └─ Professional logging system -PHASE 2: Testing & Quality (Weeks 5-6) -├─ Unit tests (80%+ coverage) -├─ Integration tests -├─ Code quality enforcement -└─ Security audit +✅ PHASE 2: Testing & Quality (COMPLETE - Jan 2026) + ├─ 99+ unit tests + ├─ 85%+ code coverage + ├─ Ruff linting & Black formatting + └─ mypy type checking -PHASE 3: Build & Distribution (Weeks 7-8) -├─ Windows MSI installer -├─ macOS DMG package -└─ Installer testing +✅ PHASE 3: Build & Distribution (COMPLETE - Jan 2026) + ├─ Windows executable via PyInstaller + ├─ macOS DMG package + └─ Forgejo Packages distribution -PHASE 4: Professional Features (Weeks 9-12) -├─ Enhanced logging -├─ Advanced configuration -├─ User documentation -└─ Release packaging +✅ PHASE 4.1: Auto-Update System (COMPLETE - Feb 2026) + ├─ Forgejo API integration + ├─ Update dialogs & notifications + ├─ Background update checking + └─ 76 tests, 79% coverage -PHASE 5: Post-Release (Months 2-3) -├─ Auto-update system -├─ Analytics & monitoring -└─ Community support +✅ PHASE 4.2: Enhanced Logging (COMPLETE - Feb 2026) + ├─ JSON logging support + ├─ Log rotation & archival + ├─ Performance tracking (PerformanceTracker) + └─ 20 tests, 91% coverage + +✅ PHASE 4.3: Advanced Configuration (COMPLETE - Feb 2026) + ├─ Config profiles (work, personal, etc.) + ├─ Settings UI with 5 tabs (Paths, URLs, Logging, Window, Profiles) + ├─ Configuration validation & import/export + └─ 43 tests, 87% coverage + +→ PHASE 4.4: User Documentation (PLANNED - Phase 4 wrap-up) + ├─ User manuals & tutorials + ├─ API documentation + ├─ Troubleshooting guides + └─ Community examples + +→ PHASE 5: Release Candidates & Finalization (NEXT) + ├─ Cross-platform testing (Windows, macOS) + ├─ Security hardening audit + ├─ Performance optimization + ├─ Final release packaging + └─ v1.0.0 Stable Release ``` -**Timeline**: 12 weeks to MVP | 16 weeks to full release +**Completion**: Phase 4 - 100% | **Phase 5 Ready**: Yes | **Version**: 1.0.0 --- @@ -402,30 +427,42 @@ find . -name "*.md" -exec wc -l {} + | tail -1 ## 🚀 Next Actions -### Immediate (This Week) -1. ✅ Project setup complete -2. ✅ Documentation complete -3. ✅ Infrastructure complete -4. → **Begin Phase 1 Implementation** +### Phase 4.4: User Documentation (This Week) +1. Write user manual & setup guides +2. Create video tutorials +3. Document configuration examples +4. Add API reference documentation +5. Create troubleshooting guide -### Phase 1 (Weeks 1-4) -1. Implement config system -2. Implement path validator -3. Implement drag interceptor -4. Implement UI components -5. Implement entry point +See [DEVELOPMENT_PLAN.md Phase 4.4](DEVELOPMENT_PLAN.md#44-user-documentation) for details. -### Phase 2 (Weeks 5-6) -1. Write comprehensive tests -2. Run quality checks -3. Achieve 80%+ coverage -4. Security audit +### Phase 5: Release Candidates (Next) +1. **Build & Test on Windows 10/11** + - Run full test suite + - Manual UAT (User Acceptance Testing) + - Performance benchmarking -### Phase 3 (Weeks 7-8) -1. Build Windows installer -2. Build macOS installer -3. Test on both platforms -4. Document build process +2. **Build & Test on macOS 12-14** + - Intel and ARM64 validation + - Code signing verification + - System integration testing + +3. **Security & Performance** + - Security audit & hardening + - Drag event performance (target: <50ms) + - Memory profiling + +4. **Release Candidate Builds** + - v1.0.0-rc1: Community testing + - v1.0.0-rc2: Issue fixes + - v1.0.0-rc3: Final polish + - v1.0.0: Stable release + +### Post-Release (Future) +1. Community support & forums +2. Analytics & monitoring +3. Feature requests for v1.1 +4. Long-term maintenance --- @@ -479,26 +516,28 @@ find . -name "*.md" -exec wc -l {} + | tail -1 ## 🎉 Conclusion -**WebDrop Bridge is now a professional, production-grade desktop application project** with: +**WebDrop Bridge has successfully completed Phase 4** with: -- ✅ Enterprise-level architecture -- ✅ Comprehensive documentation (4100+ lines) -- ✅ Professional build pipeline -- ✅ Automated testing & quality checks -- ✅ Cross-platform support -- ✅ Clear 12-week development roadmap +- ✅ **Phase 1-3**: Core features, comprehensive testing, build automation +- ✅ **Phase 4**: Auto-Update System, Enhanced Logging, Advanced Configuration +- ✅ **139 tests passing** (90%+ coverage) +- ✅ **Production-ready features** - v1.0.0 released +- ✅ **Enterprise-level architecture** +- ✅ **Cross-platform support** (Windows, macOS) -**Status**: Ready for Phase 1 Implementation -**Timeline**: 12 weeks to MVP +**Current Status**: Phase 4 Complete - Phase 5 Release Candidates Ready +**Version**: 1.0.0 +**Next Phase**: Release Candidate Testing & Final Packaging **Team Size**: 1-2 developers **Complexity**: Intermediate (Qt + Python knowledge helpful) --- -**Ready to begin?** → Open `QUICKSTART.md` or `IMPLEMENTATION_CHECKLIST.md` +**Ready to continue?** → Open [DEVELOPMENT_PLAN.md Phase 5](DEVELOPMENT_PLAN.md#phase-5-post-release-months-2-3) or [QUICKSTART.md](QUICKSTART.md) --- *Created: January 28, 2026* +*Updated: February 18, 2026* *Project: WebDrop Bridge - Professional Edition* -*Status: ✅ Complete and Ready for Development* +*Status: ✅ Phase 4 Complete - Phase 5 Ready* From f385ee6410dc1836ccb59631b9453af22c32db09 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 09:56:03 +0100 Subject: [PATCH 10/16] chore: Update repository URLs and author information in README and pyproject.toml --- README.md | 8 ++++---- pyproject.toml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8ed6839..4819930 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ WebDrop Bridge embeds a web application in a Qt container with full filesystem a ```bash # Clone repository -git clone https://github.com/yourusername/webdrop-bridge.git +git clone https://git.him-tools.de/HIM-public/webdrop-bridge.git cd webdrop-bridge # Create and activate virtual environment @@ -373,9 +373,9 @@ MIT License - see [LICENSE](LICENSE) file for details ## Support -- 📖 [Documentation](https://webdrop-bridge.readthedocs.io) -- 🐛 [Issue Tracker](https://github.com/yourusername/webdrop-bridge/issues) -- 💬 [Discussions](https://github.com/yourusername/webdrop-bridge/discussions) +- 📖 [Documentation](https://git.him-tools.de/HIM-public/webdrop-bridge/wiki) +- 🐛 [Issue Tracker](https://git.him-tools.de/HIM-public/webdrop-bridge/issues) +- 📦 [Releases](https://git.him-tools.de/HIM-public/webdrop-bridge/releases) --- diff --git a/pyproject.toml b/pyproject.toml index 06a2c7e..2d99927 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ readme = "README.md" requires-python = ">=3.9" license = {text = "MIT"} authors = [ - {name = "WebDrop Team", email = "dev@webdrop.local"} + {name = "Claudius Hansch", email = "claudius.hansch@hoerl-im.de"} ] keywords = ["qt", "pyside6", "drag-drop", "desktop", "automation"] classifiers = [ @@ -63,10 +63,10 @@ docs = [ ] [project.urls] -Homepage = "https://github.com/yourusername/webdrop-bridge" +Homepage = "https://git.him-tools.de/HIM-public/webdrop-bridge" Documentation = "https://webdrop-bridge.readthedocs.io" -Repository = "https://github.com/yourusername/webdrop-bridge.git" -"Bug Tracker" = "https://github.com/yourusername/webdrop-bridge/issues" +Repository = "https://git.him-tools.de/HIM-public/webdrop-bridge" +"Bug Tracker" = "https://git.him-tools.de/HIM-public/webdrop-bridge/issues" [project.scripts] webdrop-bridge = "webdrop_bridge.main:main" From 0eba82b8af025a608b4249675d9fb4b8196f9c27 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 10:15:42 +0100 Subject: [PATCH 11/16] feat: Enhance drag interception with dynamic URL pattern matching configuration injection --- .../ui/bridge_script_intercept.js | 25 ++++++++-- src/webdrop_bridge/ui/main_window.py | 46 +++++++++++++++++-- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/webdrop_bridge/ui/bridge_script_intercept.js b/src/webdrop_bridge/ui/bridge_script_intercept.js index 6f627aa..83fa803 100644 --- a/src/webdrop_bridge/ui/bridge_script_intercept.js +++ b/src/webdrop_bridge/ui/bridge_script_intercept.js @@ -88,7 +88,7 @@ for (var i = 0; i < angularDragHandlers.length; i++) { var h = angularDragHandlers[i]; if (h.target === document || h.target === e.target || - (h.target.contains && h.target.contains(e.target))) {https://devagravitystg.file.core.windows.net/devagravitysync/anPGZszKzgKaSz1SIx2HFgduy/weiss_ORIGINAL.jpg + (h.target.contains && h.target.contains(e.target))) { try { h.listener.call(e.target, e); handled++; @@ -102,10 +102,27 @@ // NOW check if we should intercept if (e.altKey && currentDragUrl) { - var isAzure = /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(currentDragUrl); - var isZDrive = /^z:/i.test(currentDragUrl); + var shouldIntercept = false; - if (isAzure || isZDrive) { + // Check against configured URL mappings + if (window.webdropConfig && window.webdropConfig.urlMappings) { + for (var j = 0; j < window.webdropConfig.urlMappings.length; j++) { + var mapping = window.webdropConfig.urlMappings[j]; + if (currentDragUrl.toLowerCase().startsWith(mapping.url_prefix.toLowerCase())) { + shouldIntercept = true; + console.log('[Intercept] URL matches mapping for:', mapping.local_path); + break; + } + } + } else { + // Fallback: Check for legacy Z: drive pattern if no config available + shouldIntercept = /^z:/i.test(currentDragUrl); + if (shouldIntercept) { + console.warn('[Intercept] Using fallback Z: drive pattern (no URL mappings configured)'); + } + } + + if (shouldIntercept) { console.log('%c[Intercept] PREVENTING browser drag, using Qt', 'background: #F44336; color: white; font-weight: bold; padding: 4px 8px;'); diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index cfaeb54..be7f17e 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -408,6 +408,7 @@ class MainWindow(QMainWindow): before any page scripts that might interfere with drag events. Embeds qwebchannel.js inline to avoid CSP issues with qrc:// URLs. + Injects configuration that bridge script uses for dynamic URL pattern matching. """ from PySide6.QtCore import QFile, QIODevice @@ -427,6 +428,9 @@ class MainWindow(QMainWindow): else: logger.warning("Failed to load qwebchannel.js from resources") + # Generate configuration injection script + config_code = self._generate_config_injection_script() + # Load bridge script from file # Using intercept script - prevents browser drag, hands off to Qt script_path = Path(__file__).parent / "bridge_script_intercept.js" @@ -444,11 +448,8 @@ class MainWindow(QMainWindow): except (OSError, IOError) as e: logger.warning(f"Download interceptor not found: {e}") - # Combine qwebchannel.js + bridge script + download interceptor (inline to avoid CSP) - if qwebchannel_code: - combined_code = qwebchannel_code + "\n\n" + bridge_code - else: - combined_code = bridge_code + # Combine: qwebchannel.js + config + bridge script + download interceptor + combined_code = qwebchannel_code + "\n\n" + config_code + "\n\n" + bridge_code if download_interceptor_code: combined_code += "\n\n" + download_interceptor_code @@ -459,6 +460,41 @@ class MainWindow(QMainWindow): except (OSError, IOError) as e: logger.warning(f"Failed to load bridge script: {e}") + def _generate_config_injection_script(self) -> str: + """Generate JavaScript code that injects configuration. + + Creates a script that sets window.webdropConfig with the current + URL mappings, allowing the bridge script to dynamically check + against configured patterns instead of hardcoded values. + + Returns: + JavaScript code as string + """ + # Convert URL mappings to format expected by bridge script + mappings = [] + for mapping in self.config.url_mappings: + mappings.append({ + "url_prefix": mapping.url_prefix, + "local_path": mapping.local_path + }) + + # Generate JavaScript code + config_js = """ +(function() { + // WebDrop Bridge - Configuration Injection + window.webdropConfig = """ + json.dumps({ + "urlMappings": mappings + }) + """; + console.log('[WebDrop Config] Injected configuration with', + window.webdropConfig.urlMappings.length, 'URL mappings'); + for (var i = 0; i < window.webdropConfig.urlMappings.length; i++) { + var m = window.webdropConfig.urlMappings[i]; + console.log('[WebDrop Config] Mapping', (i+1) + ':', m.url_prefix, '→', m.local_path); + } +})(); +""" + return config_js + def _inject_drag_bridge(self, html_content: str) -> str: """Return HTML content unmodified. From 4011f46ab75b2dc215c2303ea76cc03e12e13963 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 10:28:15 +0100 Subject: [PATCH 12/16] fix: Add error handling for script injection and improve logging for URL mappings --- .../ui/bridge_script_intercept.js | 47 ++++++++------- src/webdrop_bridge/ui/main_window.py | 58 ++++++++++++++----- 2 files changed, 69 insertions(+), 36 deletions(-) diff --git a/src/webdrop_bridge/ui/bridge_script_intercept.js b/src/webdrop_bridge/ui/bridge_script_intercept.js index 83fa803..b3c2f52 100644 --- a/src/webdrop_bridge/ui/bridge_script_intercept.js +++ b/src/webdrop_bridge/ui/bridge_script_intercept.js @@ -2,29 +2,30 @@ // Prevents browser drag for ALT+drag, hands off to Qt for file drag (function() { - if (window.__webdrop_intercept_injected) return; - window.__webdrop_intercept_injected = true; + try { + if (window.__webdrop_intercept_injected) return; + window.__webdrop_intercept_injected = true; - // Intercept mode enabled - var INTERCEPT_ENABLED = true; + // Intercept mode enabled + var INTERCEPT_ENABLED = true; - console.log('%c[WebDrop Intercept] Script loaded - INTERCEPT_ENABLED=' + INTERCEPT_ENABLED, 'background: #2196F3; color: white; font-weight: bold; padding: 4px 8px;'); - - var currentDragUrl = null; - var angularDragHandlers = []; - var originalAddEventListener = EventTarget.prototype.addEventListener; - var listenerPatchActive = true; - - // Capture Authorization token from XHR requests - window.capturedAuthToken = null; - var originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; - XMLHttpRequest.prototype.setRequestHeader = function(header, value) { - if (header === 'Authorization' && value.startsWith('Bearer ')) { - window.capturedAuthToken = value; - console.log('[Intercept] Captured auth token'); - } - return originalXHRSetRequestHeader.apply(this, arguments); - }; + console.log('%c[WebDrop Intercept] Script loaded - INTERCEPT_ENABLED=' + INTERCEPT_ENABLED, 'background: #2196F3; color: white; font-weight: bold; padding: 4px 8px;'); + + var currentDragUrl = null; + var angularDragHandlers = []; + var originalAddEventListener = EventTarget.prototype.addEventListener; + var listenerPatchActive = true; + + // Capture Authorization token from XHR requests + window.capturedAuthToken = null; + var originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; + XMLHttpRequest.prototype.setRequestHeader = function(header, value) { + if (header === 'Authorization' && value.startsWith('Bearer ')) { + window.capturedAuthToken = value; + console.log('[Intercept] Captured auth token'); + } + return originalXHRSetRequestHeader.apply(this, arguments); + }; // ============================================================================ // PART 1: Intercept Angular's dragstart listener registration @@ -189,4 +190,8 @@ console.log('%c[WebDrop Intercept] Ready! ALT-drag will use Qt file drag.', 'background: #4CAF50; color: white; font-weight: bold; padding: 4px 8px;'); + } catch(e) { + console.error('[WebDrop Intercept] FATAL ERROR in bridge script:', e); + console.error('[WebDrop Intercept] Stack:', e.stack); + } })(); diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index be7f17e..4099a2c 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -422,7 +422,7 @@ class MainWindow(QMainWindow): qwebchannel_code = "" qwebchannel_file = QFile(":/qtwebchannel/qwebchannel.js") if qwebchannel_file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text): - qwebchannel_code = bytes(qwebchannel_file.readAll()).decode('utf-8') + qwebchannel_code = bytes(qwebchannel_file.readAll()).decode('utf-8') # type: ignore qwebchannel_file.close() logger.debug("Loaded qwebchannel.js inline to avoid CSP issues") else: @@ -454,6 +454,15 @@ class MainWindow(QMainWindow): if download_interceptor_code: combined_code += "\n\n" + download_interceptor_code + logger.debug(f"Combined script size: {len(combined_code)} chars " + f"(qwebchannel: {len(qwebchannel_code)}, " + f"config: {len(config_code)}, " + f"bridge: {len(bridge_code)}, " + f"interceptor: {len(download_interceptor_code)})") + logger.debug(f"URL mappings in config: {len(self.config.url_mappings)}") + for i, mapping in enumerate(self.config.url_mappings): + logger.debug(f" Mapping {i+1}: {mapping.url_prefix} → {mapping.local_path}") + script.setSourceCode(combined_code) self.web_view.page().scripts().insert(script) logger.debug(f"Installed bridge script from {script_path}") @@ -478,20 +487,39 @@ class MainWindow(QMainWindow): "local_path": mapping.local_path }) - # Generate JavaScript code - config_js = """ -(function() { - // WebDrop Bridge - Configuration Injection - window.webdropConfig = """ + json.dumps({ - "urlMappings": mappings - }) + """; - console.log('[WebDrop Config] Injected configuration with', - window.webdropConfig.urlMappings.length, 'URL mappings'); - for (var i = 0; i < window.webdropConfig.urlMappings.length; i++) { - var m = window.webdropConfig.urlMappings[i]; - console.log('[WebDrop Config] Mapping', (i+1) + ':', m.url_prefix, '→', m.local_path); - } -})(); + logger.debug(f"Generating config injection with {len(mappings)} URL mappings") + for i, m in enumerate(mappings): + logger.debug(f" [{i+1}] {m['url_prefix']} -> {m['local_path']}") + + # Generate config object as JSON + config_obj = {"urlMappings": mappings} + config_json = json.dumps(config_obj) + + logger.debug(f"Config JSON size: {len(config_json)} bytes") + + # Generate JavaScript code - Safe injection with error handling + config_js = f""" +(function() {{ + try {{ + // WebDrop Bridge - Configuration Injection + console.log('[WebDrop Config] Starting configuration injection...'); + window.webdropConfig = {config_json}; + console.log('[WebDrop Config] Configuration object created'); + + if (window.webdropConfig && window.webdropConfig.urlMappings) {{ + console.log('[WebDrop Config] SUCCESS: ' + window.webdropConfig.urlMappings.length + ' URL mappings loaded'); + for (var i = 0; i < window.webdropConfig.urlMappings.length; i++) {{ + var m = window.webdropConfig.urlMappings[i]; + console.log('[WebDrop Config] [' + (i+1) + '] ' + m.url_prefix + ' -> ' + m.local_path); + }} + }} else {{ + console.warn('[WebDrop Config] WARNING: No valid URL mappings found in config object'); + }} + }} catch(e) {{ + console.error('[WebDrop Config] ERROR during configuration injection: ' + e.message); + if (e.stack) console.error('[WebDrop Config] Stack: ' + e.stack); + }} +}})(); """ return config_js From a4d735d759cfa8b164267aeabf93c072ef37d8c4 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 12:31:52 +0100 Subject: [PATCH 13/16] docs: Update copilot instructions to reflect Phase 4 completion and Phase 5 planning --- .github/copilot-instructions.md | 187 +++++++++++++++++++++----------- 1 file changed, 122 insertions(+), 65 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9ff940c..7de0a71 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,39 +2,57 @@ ## Project Context -WebDrop Bridge is a professional Qt-based desktop application that converts web-based drag-and-drop text paths into native file operations. It's designed for production deployment on Windows and macOS with professional-grade testing, documentation, and CI/CD. +WebDrop Bridge is a professional Qt-based desktop application (v0.5.0) that converts web-based drag-and-drop text paths into native file operations. It's designed for production deployment on Windows and macOS with professional-grade testing, documentation, and CI/CD. + +**Current Status**: Phase 4 Complete - Phase 5 (Release Candidates) Planned as of Feb 18, 2026 ## Architecture Overview - **Framework**: PySide6 (Qt bindings for Python) +- **Python**: 3.9+ (tested on 3.10, 3.11, 3.12, 3.13, 3.14) - **Structure**: Modular (core/, ui/, utils/) -- **Testing**: pytest with unit, integration, and fixture-based tests +- **Testing**: pytest with unit and integration tests - **Distribution**: PyInstaller → MSI (Windows), DMG (macOS) +- **Web Integration**: QWebEngineView with security-hardened JavaScript bridge ## Key Files & Their Purpose | File | Purpose | |------|---------| -| `src/webdrop_bridge/main.py` | Application entry point | -| `src/webdrop_bridge/config.py` | Configuration management | -| `src/webdrop_bridge/core/validator.py` | Path validation and security | -| `src/webdrop_bridge/core/drag_interceptor.py` | Drag-and-drop handling | -| `src/webdrop_bridge/core/updater.py` | Update check and release management | -| `src/webdrop_bridge/ui/main_window.py` | Main Qt window | -| `tests/` | Pytest-based test suite | -| `pyproject.toml` | Modern Python packaging | -| `tox.ini` | Test automation config | +| `src/webdrop_bridge/__init__.py` | Package info, version (0.5.0) | +| `src/webdrop_bridge/main.py` | Application entry point, config loading | +| `src/webdrop_bridge/config.py` | Configuration management (file/env), URL mappings, validation | +| `src/webdrop_bridge/core/validator.py` | Path validation against whitelist, security checks | +| `src/webdrop_bridge/core/drag_interceptor.py` | Drag-and-drop event handling | +| `src/webdrop_bridge/core/config_manager.py` | File-based config loading and caching | +| `src/webdrop_bridge/core/url_converter.py` | Azure blob URL → local path conversion | +| `src/webdrop_bridge/core/updater.py` | Update checking via Forgejo API, release management | +| `src/webdrop_bridge/ui/main_window.py` | Main Qt window, config injection, menu bar | +| `src/webdrop_bridge/ui/restricted_web_view.py` | Hardened QWebEngineView with security policies | +| `src/webdrop_bridge/ui/settings_dialog.py` | Settings UI, URL mapping configuration | +| `src/webdrop_bridge/ui/update_manager_ui.py` | Update check UI and dialogs | +| `src/webdrop_bridge/utils/logging.py` | Logging configuration (console + file) | +| `tests/` | pytest-based test suite (unit/ and integration/) | +| `pyproject.toml` | Modern Python packaging and tool config | +| `tox.ini` | Test automation (pytest, lint, type, format) | ## Code Standards ### Python Style -- **Formatter**: Black (100 character line length) -- **Linter**: Ruff -- **Type Hints**: Required for all public APIs -- **Docstrings**: Google-style format +- **Formatter**: Black (88 character line length) +- **Import Sorter**: isort (black-compatible profile) +- **Linter**: Ruff (checks style, security, complexity) +- **Type Checker**: mypy (strict mode for core modules) +- **Type Hints**: Required for all public APIs and core modules +- **Docstrings**: Google-style format (module, class, function level) ### Example ```python +"""Module for path validation.""" + +from pathlib import Path +from typing import List + def validate_path(path: Path, allowed_roots: List[Path]) -> bool: """Validate path against allowed roots. @@ -50,26 +68,35 @@ def validate_path(path: Path, allowed_roots: List[Path]) -> bool: ## Before Making Changes -1. **Check the development plan**: See `DEVELOPMENT_PLAN.md` for current phase and priorities -2. **Understand the architecture**: Read `docs/ARCHITECTURE.md` -3. **Follow the structure**: Keep code organized in appropriate modules (core/, ui/, utils/) -4. **Write tests first**: Use TDD approach - write tests before implementing +1. **Check the development plan**: See [DEVELOPMENT_PLAN.md](../../DEVELOPMENT_PLAN.md) - currently Phase 4 Complete, Phase 5 in planning +2. **Understand the architecture**: Read [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md) +3. **Review actual implementation**: Look at existing modules in core/, ui/, utils/ +4. **Follow the structure**: Keep code organized in appropriate modules +5. **Write tests**: Use pytest - write tests for new functionality ## Making Changes -1. **Run existing tests first**: `pytest tests -v` -2. **Create test file**: `tests/unit/test_*.py` -3. **Write failing test**: Verify it fails before implementing -4. **Implement feature**: Follow code standards above -5. **Run tests**: `pytest tests -v --cov` -6. **Run quality checks**: `tox -e lint,type` -7. **Update docs**: Add docstrings and update README if needed +1. **Run existing tests first**: `pytest tests -v` (should pass) +2. **Create test file**: `tests/unit/test_*.py` or `tests/integration/test_*.py` +3. **Write test**: Verify test executes (may fail if feature incomplete) +4. **Implement feature**: Follow code standards (black, ruff, isort, mypy) +5. **Format code**: `tox -e format` (auto-formats with black/isort) +6. **Run all checks**: `tox -e lint,type` (ruff, mypy validation) +7. **Run tests with coverage**: `pytest tests --cov=src/webdrop_bridge` +8. **Update docs**: Add/update docstrings, README if needed ## Development Environment **Virtual Environment**: `.venv` (already created) - Activate: `.venv\Scripts\activate` (Windows) or `source .venv/bin/activate` (macOS/Linux) - All Python commands automatically use this environment through VS Code integration +- **Note**: Only activate if running commands outside VS Code terminal + +**Required**: +- Python 3.9+ (tested on 3.10, 3.11, 3.12, 3.13, 3.14) +- PySide6 (for Qt GUI) +- pytest (for testing) +- tox (for automated testing and quality checks) ## Common Commands @@ -77,19 +104,25 @@ def validate_path(path: Path, allowed_roots: List[Path]) -> bool: # Setup (one-time) pip install -r requirements-dev.txt -# Testing (uses .venv automatically) -pytest tests -v -pytest tests --cov=src/webdrop_bridge --cov-report=html +# Testing +pytest tests -v # Run all tests +pytest tests --cov=src/webdrop_bridge # With coverage +pytest tests::test_module -v # Specific test +pytest -k test_validator # By name pattern -# Quality checks -tox -e lint # Ruff + Black checks -tox -e type # mypy type checking -tox -e format # Auto-format code -tox # All checks +# Quality checks (these use tox environments) +tox -e lint # Ruff + Black check + isort check +tox -e format # Auto-format (Black + isort) +tox -e type # mypy type checking +tox -e coverage # Tests with coverage report +tox # Run everything -# Building -python build/scripts/build_windows.py # Windows -bash build/scripts/build_macos.sh # macOS +# Building distributions +python build/scripts/build_windows.py # Windows (requires pyinstaller, wix) +bash build/scripts/build_macos.sh # macOS (requires pyinstaller, notarization key) + +# Running application +python -m webdrop_bridge.main # Start application ``` ## Important Decisions @@ -120,10 +153,17 @@ bash build/scripts/build_macos.sh # macOS # Unit tests: Isolated component testing tests/unit/test_validator.py tests/unit/test_drag_interceptor.py +tests/unit/test_url_converter.py +tests/unit/test_config.py +tests/unit/test_config_manager.py +tests/unit/test_logging.py +tests/unit/test_updater.py +tests/unit/test_main_window.py +tests/unit/test_restricted_web_view.py +tests/unit/test_settings_dialog.py +tests/unit/test_update_manager_ui.py # Integration tests: Component interaction and update flow -tests/integration/test_drag_workflow.py -tests/integration/test_end_to_end.py tests/integration/test_update_flow.py # Fixtures: Reusable test data @@ -132,20 +172,27 @@ tests/fixtures/ ``` Target: 80%+ code coverage +- Use `pytest --cov=src/webdrop_bridge --cov-report=html` to generate coverage reports +- Review htmlcov/index.html for detailed coverage breakdown ## Performance Considerations - Drag event handling: < 50ms total - Application startup: < 1 second - Memory baseline: < 200MB +- Logging overhead: minimize file I/O in drag operations ## Documentation Requirements -- **Public APIs**: Docstrings required +- **Public APIs**: Docstrings required (Google-style format) - **Modules**: Add docstring at top of file -- **Features**: Update README.md and docs/ -- **Integration tests**: Reference and document in README.md and docs/ARCHITECTURE.md -- **Breaking changes**: Update DEVELOPMENT_PLAN.md +- **Classes**: Document purpose, attributes, and usage in docstring +- **Functions**: Document args, returns, raises, and examples +- **Features**: Update [DEVELOPMENT_PLAN.md](../../DEVELOPMENT_PLAN.md) milestones +- **Architecture changes**: Update [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md) +- **Config changes**: Update [CONFIG_README.md](../../CONFIG_README.md) +- **Breaking changes**: Update CHANGELOG.md and DEVELOPMENT_PLAN.md +- **Code examples**: Preferred format is in docstrings with >>> syntax ## Git Workflow @@ -165,38 +212,48 @@ git push origin feature/my-feature ## Review Checklist -- [ ] Tests pass (100% on CI) -- [ ] Code follows black/ruff standards -- [ ] Type hints added for public APIs -- [ ] Documentation updated -- [ ] No security concerns -- [ ] Cross-platform compatibility verified (if applicable) +- [ ] Tests pass (100% on local runs, `pytest tests -v`) +- [ ] Code formatted with black/isort (`tox -e format`) +- [ ] All linting passes (`tox -e lint`) +- [ ] Type hints complete (`tox -e type` passes) +- [ ] Docstrings added for all public APIs +- [ ] No security concerns (especially in path validation) +- [ ] Cross-platform compatibility verified (Windows + macOS tests if applicable) +- [ ] Configuration handling tested for edge cases +- [ ] Git history clean (meaningful commits with proper messages) ## When You're Stuck -1. **Check DEVELOPMENT_PLAN.md**: Current phase and architecture decisions -2. **Look at tests**: Existing tests show expected behavior -3. **Read docstrings**: Functions document their contracts -4. **Check docs/ARCHITECTURE.md**: Design patterns and data flow +1. **Check DEVELOPMENT_PLAN.md**: Current phase (Phase 4 Complete) and architecture decisions +2. **Look at tests**: Existing tests in `tests/unit/` and `tests/integration/` show expected behavior +3. **Read docstrings**: Functions document their contracts using Google-style format +4. **Check docs/ARCHITECTURE.md**: Design patterns, data flow, and module organization +5. **Review config examples**: See [CONFIG_README.md](../../CONFIG_README.md) and `config.example.json` +6. **Check CI output**: Look at tox and pytest output for detailed error messages ## What NOT to Do -❌ Change architecture without discussion -❌ Add dependencies without updating pyproject.toml -❌ Merge without tests passing -❌ Remove type hints or docstrings -❌ Commit without running `tox -e lint,type` -❌ Add platform-specific code without tests +❌ Change architecture without reviewing DEVELOPMENT_PLAN.md first +❌ Add dependencies without updating requirements-dev.txt and pyproject.toml +❌ Commit without running `tox -e format,lint,type` +❌ Remove type hints or docstrings from public APIs +❌ Add imports without running `tox -e format` (isort cleanup) +❌ Add platform-specific code without tests marked with @pytest.mark.windows or @pytest.mark.macos +❌ Modify path validation logic without security review +❌ Force-push to main or release branches ## Notes for Modifications - This is a production-quality application, not a PoC -- Code quality and testing are non-negotiable -- Cross-platform support (Windows + macOS) is required -- User security (path validation) is critical -- Documentation must keep pace with code +- Code quality, testing, and documentation are non-negotiable +- Cross-platform support (Windows + macOS) is required and tested +- User security (path validation) is critical - be extra careful with path operations +- Configuration must support both .env files and JSON files +- All error messages should be meaningful and logged appropriately +- Documentation must keep pace with code changes --- -**Current Status**: Pre-release development (Phase 1-2) -**Last Updated**: January 2026 +**Current Status**: Phase 4 Complete (Jan 29, 2026) - Phase 5 (Release Candidates) Planned +**Version**: 0.5.0 +**Last Updated**: February 18, 2026 From de98623ee0fb1bf4b250e32b36dbea300cc45340 Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 13:07:01 +0100 Subject: [PATCH 14/16] Add application icon in PNG format to resources/icons directory --- resources/icons/app.ico | Bin 0 -> 122727 bytes resources/icons/app.png | Bin 0 -> 9447 bytes src/webdrop_bridge/ui/main_window.py | 10 +++++++++- 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 resources/icons/app.ico create mode 100644 resources/icons/app.png diff --git a/resources/icons/app.ico b/resources/icons/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..1768c679c1e16bfba03aadf0f8cf224745cb24ef GIT binary patch literal 122727 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
    YhmS5*A_et=6H?<)YW{46#Kh~t0Af!O}3waHVX zSz_pD-sUR_?Hn=?CFtl^JQn%;sx7}R53lsSs7J(hUyvqWA7Gw|@#S@&iY)&02qsSz z*Li5JUu=jf3;G`lfY;FZ#B)&wL|H)MjHg#wT*$Ay-J|`B>-a){z~w;Xqm;+5KB&&MEv`(0z9Gs01T!3L=*s>MQ*>60AVkN5Gq#s^5JO~ zbFvTT5nunM@j!mteT~n;9D&+6@f+Wgcb>nX?-%+4zNYA_$6^Beu$K=)gbGEN02sj} z!lC>vUJ4MM$*0mmb}(M0d>8qxm{$1a z=Yt}-P3g{wA6V+WBUF7s-LJG#L6q{P7%3p|wt(091;6&4uNg2w;O<%c0d61c!1)gu zc%A=NwxB zqFtBJl-hTd!Hw12Hg7EO zRo}ahIP=B%4L*x=d+rNiobMRt!>+r(K6@DD%I!_!egkm0ur`jW3v`hJSjx{& zPXX{6|D;Z^5%Qp1z9#!HatXK5*W~TFEVm25HvH2>7izN_(dTy0^k0P6); z1=|30zGx>F_I>ic;N~7f_VCKAe7mmLwuPakw9tIq5TtDTpPA8$)#dXAp2&aUP0$J8 z4W6tFzDG1{2D`Sq3fBW%1~~7%p(+ zNapBU>3@C-h`h@01q9ZJty17sJ~)}r@34F4CvqP1J>q=djxgVYod{z;^TpO z?5c-4=eq`r@KEDp#QN<16-RzI&`p7!80U$PNM6b>e!c$trnuKrD!)D@1w=mZU%b5X zgevocg!fE-#}D?7#-E$O;?Im=_qhLr&+)Qe-pfxb@?IF{A!B|EmzE(URQ`Nb6!;(h z2)uh8L}{9AzGpXs-v^Z~=Xby5rzM?%w*P9JkI(|Kdc8p9*Z&I&kiJ($hWM@+uKfPC zDM0*&UznqK!H!T^0%h1YPXXd_QHVe?lym!{sPk=x6VJ&n3JZ!Szo>M|@Nb6##7n9F z?Le=DR7n9P1(Xy}Qb0)oB?XieP*Oli0VM^L6i`w?NdYAVloU`>KuLl3qkv55loU`> zKuG~51(Xy}Qs6&F0q4FybuU}GN@EgB&-xhav}EYh2N~-n8x`mt~ZgQoDBN#4Z(EY`7K@?PnPvOoiCRY8T}!#RsU)6Fby(5Dfu}UFEV;aPXo(-f zWR{6GL1!!TT;&WtC{y(8az>$9`+Sr2Mm-K#v_4_;^4>DDwcj~a9oissT|(E+O;r+Q zIxQRr)pfjAUcJ=v-W6)7T51}FR;#s6)}du$`JfU(`#0sfHw=25X7g~IYdOuMYReP8 zU$G>s#&?#QagMeAiV8eYt$09Ys#<;nE0mqL&DZP7piUKjuC>tC-oAe+ z(EPn|5nASNrf3*=Xsr+iYcBW37y2p)F_Z-(?(bH1P4Q`zm(C*~bknRpP8Dod4jM7?M{;u<` zyW?h*={TWc>a4OQ2V7hESLucxf$kaiR+dTm)$dAV(@qy__vzw42lO%T(kCh-GIP$- z9F5RbI)T;N%4dY0i5+%RzUZl%$0V7%ao_c-XDZi{)$C$eVW7E6_p@fTrr7=3>T2Wh zexrLj)D71hmsyfI)*2`KO)EUl<9v|sh^AK$ZaZt(IdAUt=U#eoNlCSHeh)o!xNEuY zhZc3#S(0mUdujdc?Sq4o_8&6L(6} zL~~^AP1`IQtSgh-rL&Go=uZwKbhbZQFr}8Ox7VqGmH+WCJz&JCl+gS2RCXIzO!>{O ze&{Bnak5)Z^Gl8n|HnIfX!M^0%tI3oBuoo)lLeX{PF-@LxqXE7)CVQkn|BJ4wbHzG z&#i0g2hFRunCGOj!ztmc=GKg%$rGJcRE_Ur-OJ{W*0QxOeGPKLLoV!esCm(4!?d2| zb(Ym;n_A5M5!I9qlbu=MwX{B~f1rBj?{<%PxNlsFS*VFq%B3r(Sr2p8qK!jUOB44_ zW3*hR$#h4%pDceU_!o<G+f3==+CwL6 z^%{%6%QpT=d&Gb;bJUCy4Xq6Bn#RkMwk^uaRMQ?OpL)O{wA>6U`W)kAFV#$^*E-7uQRb6ywQBMQSeH#P1 zZ@2fIG{t!TlIAsk?zX>KwC)rWP2J763>J0jHn?vq{nZ0YcKf-veEXeQYo^58MaAlK zz}GC-=%IG);!1bFQnIap& zbF*QOL!8I_aYN12M7xV&bakx?<10C6=-U6%X!zc(J!_9Uu=u>MZf*6}Ng0M4JB7&( zY3EegFm&3+x*?Zs>nw^rxy)hSie)Ur#$}k*8JiaA&9!Tp_uXn@S;^A8y~T=-C61_u zdGvBL`gz21wSi$LZPYe)H#O)kzYrlC)v;5*F`iW$PG8W|^4!2$!<{v14qUo=@v0|H zFAj19mw$6z@SxSoa)C(;c(JL6*R{64vnlS@HZJ+6k{Y`{)!T0SypO@{dnf*BcET*l zP{qNqMtqv_TrU&5fsxx9Y1LTwV`SM1r8kzX5HQK#Eo1cb_Vx3AtjS9))$_pRt(CPM z>Z`Qq)ur(W{kvDJQkk1e>V@7__ja%TL+AO+S52y+V(2t}&R~-<)rYQ?+gg^}Y-9Ax zb8EW<<8x_)bzYQj=QPD6J*ARISC_dOEU9~^(|%jSeo(L5Fm$3(#KU;|NqWE3w=9`* z(lMTmER%g>*_8IFDUbFn4SeE~x_5S^?>o-#o|^7pa(-%<-r(#R&A-oUk=DYo@x_Q( z!-(5+=R7(#eE5w6E%jTeFKbe*!jEhAx(!WspYNu}{CNkoJ7oIZ(1#~(bo4l8_55bL zTcLA0R=rz^Q>LBe)wHhi12;nkv{OlF@o!tx`h8^8Mm1QuBzX3yHalw>$hz0Dbof5( z%8S~^tJumftkQLJNCy54NawWA^ zPVdVnhy5NhlkKW>s%OqK7rPgY4?UjdHKf!Ir*_|!@q1aV!MR00t=w~Z^lx1*>K}JX zxUHE|?)Oq|C1$xV^mgAm56|x3gV3uG>@pPJgGYl{L0py4}{lb@h?z)k`%!p~E`7 zu2MsuE)&7WUUui%vkGT9^>2WN9&2& zMsbFDnx3az>>?Yb~&^_Dsdg9oIkn_fQ6X7CZhUyhH5^~u|OvsZ&i ztDhb;wJnt->oe5WMWvEnjlicj1hj4iou07rc31Dbf2S^&Yd^>{zw&&_>AnL>2Dyan z*l?tJy~+kEH%83YtM6bo{c4A*wOwlE92pW4u~OT_>h~R$O6}?0&GzK-G+vdpb_2IT z-Y3@`tvakxY73u5dh?uQ=Iwual6&V$@Gv*6l*5Dd4BOVM=R9!Fcdf@5x{X@v?{V0z zTEol!sV`qvR&`6eTqd@|mJ=NYYnZE~1-a$Wz-T~*^YIxZPD)FpM<_mw6;k?qjXFf;k_!Mg1k zKUb^QXH~lXgP%HI=rVo$*@=zCib{tz_er8jW)#RC{4R#pK-8^6GxBcUcepek{?-%1x zG?7;G_NLryJZgR2(|an#T{|1hOU?2AE5W*UrLG<4_G#DAz1`l6V<&y@nQ>~mnxSE5 zwU?_5R4N1o>^j-Mf?faCS6cpSJM8kl&b$>YU0*Wje91l2GhP2mwTbt2DiOQXE+;m$ zYtH^r328TiO`LR6z5d-&^U$6aCgv7n>qYIod}O-Y?}5)+U9Z`*(uM?!S?YVfFQxNC zkm=$StH*bf#*Mn663SBhsSoP9p!I<6M*A8%b@ge}@Y2*-ysq6w@#}J;muoiJw6I0) z^u+F_8`R`!Yvu(WJ(#+;M5%d3^FviCl-BpFb7;$Dr>l)7Z~be@{!OlP>&`cv;<~5O z-8Qo;83z5)xofupDgF9&A2PtaJ2Y(C zxpuWfbKWtHxa3`treJ zTf!k-vP@NXuEpF4cKj#~&+~7#!_U zd6BBR+v(kBo8Hk0eA-r{maDthHgBIB`&#Pj*K=89mO5D*VmWrkl%`f^yaHx@pVe%w zs_VpZfk*sOmn_k=ZybPDotLa8x((I*inZ*S>6#fch~!Q*sAJ#X7?$d z8qw4-^61OKXKVF6U2{w5?sN9-RPF6M%ndcmo9^LMV_@0lmYXdn+&|=y#`}oB)Yqxc zmg&rDliO`#%|yd_b^c9n7WRWYbkDrri-MOJ*ld5%$ig{ec89uoiLLi}UixKmTAdmf zyiS(szR~5+GtcVlrEjlQJ!)@@H8%0%##*QZ#aX%zXcH9s$B%JSwoWo0zoFqn|I&jt z{Lsbkw!WVg=YEs*O=fg6%RHsJ-121$S$!*qVb=BK!z$IbYq35qQZufbR!#HEcS07| zc$B-Z?iOdQVVw>&zxeRJ&(kdtBbre)zkpsB^V<;#Y2?dsM?M|Hiq;kZc-lS^vn zJ*+i$;;>drY<>=3u+*`|<`#~vpta`JNuSpA)Yyd9Gs}Bfy7mi7y%cnMPP@wSg9o?% zsa}WM#%rq^WM-L_-`z2+e4K`$NXIlgTJd6G{C~)(swGln^uo5qdThq z$`@{2Ri<(#G@~A-gNr}woZp9zOv)S=>Q0(cR51tA5ghYFTT0tPh&reck#~r!MGCI~!JGYqbvX zh6xS{o1O#@*E*(N)&Eq=nB5M!bL&-EmH0~y-lJ4pp8S22IF*XlG3rawW}K0=aY;G0 zZ3%m1I%QL zzi@PUN(VlOi(dZSP8H|gjmN50YZkO|#Ui%1#mQLfYR9Y0G{~x{8n&%gOm2hItz&K; z)iKgP+Nha(^-kVtm(4frZKAU3Z?yw0Ld<%5YjwA2^->nLZ&=@c;rhot*L!aK_560r zYDbe&(s>0{Ojfzp?)G3sj~a6muAS|@a#lT4gVQwwYL3=yr#jA}*5e8O%}SL>h`8*& z?P!0KPQO+OdKurvFCaa&eN^nl9iv0?#&&C2t4S}Xtwu*F1{%O2Iba;^Wii)MO9s&T5;8$DR&RMjfr!0epdYifbV4{{;tc6mu$ z_tdZ^^-EsTIo!CnM{9Y5t9P0;Na@yHK2UYN>tNl+4z>4KMW&U=c@)$+sPeDs6$br& zGGV*x{ym+Bw4H0%rdycoy2H`!4U=rXpPpK&e(jv+mUE+O*O=Y0OQ(CbdVd|&c)ICN z?Q@qy>KP5X>|9MYWaganI>x=#TC~a@<81V|-?4PHeHnM7UFYPsuh+5D%(3#8qn1Zj ztrM6n%d&Ths$FW1?fDU+f!!^-F1zxO_#6YfFbW^tHYt z3w+X!*MBvwi%q}dTPw6~5_C?tYFhZV!~0Sv{`}H|^+|aCVB$r4+v@wn)$f0=zA<-t zjO~RUeU4~2hU}``qEoNZmbJOYjZeE0Jh@cUwv&u!n>ESaxpd{N(K$ElxxhbJzp`Oa z--to&n^nHiz|+ZeQB_N$krDmx?C;ZY(%ssgTN>#6*~RXMI~4NY)e>V^GIzF$Uab~eOccT zUej!{vQD*IULkPV&4dQ0w&-T6ml)Y1)<102*k`RCm9>~Zy}?>p<1q(A4z6sTpj9)z z+PVG%A6IOd<2Uwh=+av0P5$ncyg#j(ZuYtK4IWyz4WWsT)LGo;)|BSed&`f=x^B(4-FRICwIJFBu?QzVq@s+E4I_O2unmxb8`fUwv z&QNKPoAJ*rqlhXE=CZ_bNrn-JMxVQV_K=pgcE@;QPp!+nOXm4ay6e%T%$A?aMwV@5 z_4iowl9m&EBZ3~f>G>{pQ@L%n^7qL)@ z`e#7pS?Y=ZWDeZ9s=?}3-M7t2{r>FIHkZtXYPHS?n&Ir&-0b|E>r2m$UAe%0{-s}5 z`dfrAjO&%Cvs89Qx9*Hm70z6}GAL(fzo0oWjqdumFQ{L)n)=e6n%^yJxblMK`M+N5 z-`=J>7v{MS`fHhW*Izi?!qMBfuR+(I15%E7I!;@-qQhg5~^9IzPUv+BoNWEj5 zTU%_J*{!6}=wq>eX60C>s4p75X;#A(7k@CYXjEfrkHr>kn;IQ8*O_{CY);3>(sKXb zL!PxME%)@?vDIs?{QKDM1`DsWs`ih&X1$>A4j4@v>sQ*cV6p;}|r zIW?COj+25~mQ7x$GN@m_t7e!@LpoMB>EHqO7pz(EW4{ApaV$Ic|O4#qlrOM8&O( z&h+ShGcB-Nrw)PjN}RpA`1x|aHou=*?=kWb*FrA;S}|Z!rIh{KcZ{{Lw!dsWRsK|G@*jaG?;f)@ zSQ=`)wwF`5V{hlnQ?Ea9cB|y;#7C-nrwl@M)N0mSrP==SyzM5&rRPn0XzX3r*ursJ z!||020y?S0Y1yj{ZL6Dewrfy=fy0rfTP)mOY>(St*Kt?N6;D;_Wv7jE9nhnHR)_wH z!ydZ6bUts;cn!>V^)IPnjMwbXdRCbn~)t|bpxGAW~UsmbM19;P)KYUMwC@nYkB z_D%mZ-yc2iz7dz;-93E6&t0$9Y*LL}`x@TfUHY|ZSw;8!z3*BMns+Kb# z&YA6+SZV5@;cengW~g`cTrc+@5>(^g#q#50w~rX#_ZQz}-mDT52H07rRMRdOyY1Ga zlT+&(uU8+sEW7)}Ra>W?yS#B!T>!VI=G)(Yw2&l1jt3}G0^ycYi7HiEG2zX?fsEK#$bMVhVI%s%6v zJO8!x()?RLRW7MnGWD18&fQCo9I3?(R{@iLmA|-CKF_aTNWx0jxorp4KeMBGjN$Q< z&+M~*8|pSJXv2-T5&G5@dJS}XJbc*NwA!JI`FQS!<##tG83sHHKJ>ToV$~l9j4){H zeXXIz0a>?xefti2T*<+&q{}>=!S=FEM@=`HPdTOG9oNk3M4wtE1E&8n;XsGL^tzV2 zm&D9odAMf434IMW6Mv1Vz4q=hu^T->KH^eH)b3jLk$x#>!$){Iht;r2PAh5Jw(Xf? z!PZAi+lAV+{~@ka&(c#Za|dqj>Uyj*x07gud9+qBvhq3I+Hq=m1GV`hoUCS*S~$XC z-nOlcY=(xXj?xdR+`V(Zlov~y|FBA9c;au1`nGXD^YuRmpCyK|e44Cy(g*Xr|ATcG0@?s6$-WA$a44Hnn3*}UlLc>Qr9UJa*5nD6Sp zV9$gr`s!m!*px2eR$|rigi~c?vfn$-ucX?r&f(vps~P8oX&EiAl~b|9>6dke-0$P1 zGTn5vq5t-+yM5%_|BQd+zMw`M-g8v;Oio?-Vs?#$t1|vaJWcJgD~5)K8kEh_u$yz< z-*V<3?fBTY=IG`ss!4{MlSY^r)M}c&!$_-3%Z+6{7prvY+_S{ywPkZ!E`D*p$)Jg>xt?SS+aiRPc=IB!lY`K#?P`X+a8Fk+S7Ptm%N4J8$Mq(dezHj zDYdtbHOV=`hZPgorOdc+D)inFzxpR<{BCG?{It*D*!X0*{*F`~dmW1kE41R1%BZgA z*Tq{c{SU|_Ss+R=2GX5p-;G3rEL66t?b4< z${T2xXfT{&FW~ahGkPL z|GjCboz9rh^&y7tt*xEq4|MkFp9|UgUC9CCZ2Cv0a=vVNqMzFBn)$JaD5J`{j&=51Ra)GGJ+^k=iZpJ0w;a@MEVH zZmNb_rA=kcO81KxGBM?^A@Ut#b+-&E&6Uhb(;B=bBT0B6vf)PGnGWvX8TijN@!m1MLc)2| z(p@hMOzrlsLr;S?5hnWTu{W!i>}+Vfvxj9?q1px;a<9)zy4#!bH`ThC`?8 zB@A=h<5soI9|eCp)tBc|@|6Z{62(Uo-_bEDCp za@}S|rOmX?n3V3~GIz?KSqT=m+h|AJTOK^o{BX^qW}4*}40k`XXkh%R(^@+xnB+`_ zu&hlfwN$nGkhT`Bys8G7EezKEBhYr~uV11+b#+uIux%XRGx$c>Y z-WCm7)r`%Z4C7qo{i%~<#%&lLai)snv*7BthA%EX=<4wCCHp3rSm^O##~{b=dk4?) ztYqSB+SXpSK*i%HS-ZHtD-R_`HO$;u!Ty|Qn90krHW;Za7Fv2nfg_rMtUBIz$2KmQLxA2I)@e z&LiFW7We!9g7@y{{Bjk}UVH61$DDJFv9=ettc@)qOD%#Ot4)%r85ssvg6N%$24WK_ z7|;Do32cv${-aQy4@RIXa3Ni@1;3;b9Kqp}?tDV%wEk8?CDg?8aZyn~V5gdYjGRF0 zntj@RcJ_*|PNe>=j}C($2m2}ubQ+0hNPg0U8Zy)ardm+YUu^5>#1B){acRa3>M zz96%UrSW3q(U|ep$PEl}ju%tjUtLU*r>3TNxX`0_vKKD&!jK6#()boNkBCx*KNV99 zR0@FkCMJmi3H@`J*0 zKhBB+-T;I9^*f?qeAi4G(#PM89!c0Gg$E>Ltz8z;`^+=1fm!am@w3fbU%i!EXcQVB zKY!0d?1+eT*l`CT9#_9?QXODtm1B@4ZWlT=rX_AG?HXd)= z&Ksvw1r=uCmrae1!kwbnri?`V9Z!^H|BoAz4PjR1%PUxKPvySkJIJ$lohy?A<+RtH zpQ8d^yye-NNEY$w8e=EC%QaY2TA;hDS8toB^8Y-0O?nU(IAe=_d^7Zlf-1vMi0pZ+ zYSQN2ebNy>p$fn3H*%|_^d<_|KoH1Kt+FEbX-mA+7n`eZ3{ZQW+e0Mn^8Gj-<(o^W zFn&zRvM6v<;aQ6{z)tmit^;v_(yUQnWZXdVhuaFh#qi$bdq=%SQ^S0e@x}R0Wuk+s zj-|OIsenPy0@n}&-1^+k=Q4p-((mY}fp*@};G8a@)36)q6(*dJazs{LmFX?3?@60w%%` z107?{iFT;6;(VM^2MwC8AF|Pv2{dy*Sh+2$%3NPrb@1u)2weNNbusw+KgM$%R5i?Z zKZtk{1-oQlx&l}Dc%oz8+W%SnkV^5)Xm-EdQTXNYOZ95sEiID_^@PyzW4x1}6t15h z@FlWRQ&j119%-Axr-jMv?->rE?A~ydBt}NgZwgXV%-Gn9yvox~ekl3K2T5c;- zjCYjTF?y1M@H!Z4IT*vj@v6EG6ALKOpp-=1cd|`Eq6;tAKGh(*Jgw{nurI!oGqb;T zUOeM=x`)-cC?Aymn6`9yvpLX8*0fcIZ9&fLXf5fnx|&k|@!yAR$}%Uz2u8;2#Fa|d zKq3ySn=@o5@i{f`t3c^_m>GESU=*-{fc(Ny=qUGVFWpydQGU9(;-;97*CF1bG8RwR z$CPA{lxH6JwW}h$@8>=$uQJVhx^N(6`>S_rn6}HU-DAgsPGzjC{5&0u_@BF)!$l&5 zo@@20tHZLC#Y#0KbH>E7l>5{J1SP;2jNSuvf`i#O$XILC`B~XMve=f5C>y^_kI+{d z%yl0WH5JBi8Z$w`QSPS@{h~G>m6#XTe4p|dL+ep|QR@vt(J8|5$sc0{uWO9~uIS~u z(mb7))+_yGIXO2+Y^k;a(XRdXwtx19Ug_xQwjEp9QHuXy(ef8b6n3THENQD^p4@jn zI65+PbtTDDeK`@FoE}dx@E~DHPH<}`1Z$Uu9J^umK&=*}aZ&3Um%Kd1*TSo<&j=qC zWw%YfhERZnSAU(vRPk=~kEb&i$H~jtS7y*Fq~DBXe=LhI_Cf+inCSoJK8v^)e1!Gr ztBViGy$udxsim{Y6=CP&g0xV(j;psK?R-}?`TTlLO&wOL*|oGpnJ(-Z($dn>)f&gP z&?F%eZyA!!$c~8;C5;lvVU`>+F)_i%Ox&mTq{`>k1aZjGt+denord(g(E&p>fWSN?l z)=|k2DuIng6e-&-r7!P#Rk9w*A*F($puF2PF+mf_wT zM#>~d@q5yy?)Yr?osjnp;qC41_3%%$4X#GCkE9o7D!gP^cEcdBhlhu8QQK=G+nrW% zuNzOc&2a+sX%zgaq@b9+kawAkm@0(XV`d%PEbQ!Nk&;tuH0cRNZA#33A^pnZgk~=# zo2(by?r&b}{`3i+SSOgx;Rv8sKo@ztM`SQa<74;wm$XPet-m6GR5sn_w(l7};c01U z4TAre95akW9w__4M#S1Oi1R|VfNVze&U!eXGrG1KJNE!@x(Tuq6|JFDAHUa(eD))x zuP@o135v2MrktMJq>u#`B2NeyR3Fv_um|n)@rjD|x-NTvs(Wn90k}0!5eUX~oLro| zaqs&)$p(qf2GD{j3%9c!ymV3DXa=S9HtwOXp#m{@_55HBXoIKcDDM(As^=nE&XYq1 z2IT9@nnuaeTi=>MIpo)F^Nd=l^!X~XDkupR-?Z^tOp#%}IO2_c_DNKjz0&$&4oX2R ztz2yo2Ig8PhHfQ3MQ3L^a$iKNkgK6ooKP;+I9}_1>UHvqVRm`>r}SNf%azym&xZ>e z#6GW3E$nKzOYO9$z?*y1BYu6?VBYB%8ALc++_UMns-OdrWT%@ucSdQ`8szw3A>0y^i*mU)MeW?zQy}PaAM5f9F_OOZ5#jmdRF?*fKoP`#^K8gj=es%M}QTY||P85lUg))3I zSRf4V=-^-_UP+{VAdyXlH8HRESCq#-uu&Z0(cW)9T81vO!k;jMfgzR1na3$MH5-iL51nzEqW{|R*Wm2~mX0H_1A8381}dGjXw z{Yo|wq_MFi^h@CHhy~~a}!~lD&;+ZBLReqMFv3)zc=%2Cub2}H8km`6Y zC$^?PG)hj$;kM#WdQ_7+r8I1-_9Y`ZEbqdzGnE-Lrz}<{ltjB3GQ0w`OL^*CC88fH z_6JBfUMxy5)Cd~WJ0lT7!ho#^x5Le@my*H^rNI{cv#LO$U_~CiF7}?NK<>a4ClI&R z+K7+BkejmI9&Sg69G?>))4X!B%%PzICxnR?s&yP4u|B|QZwyt=ltt;m{)oLRD1!-p zF8?fQFoyX7=HjAq`AZBKZm&P%`YZfSAhXdc@;wQMDYuSz>=2HKj@tG?)h)segc6_oa&z&-4Y3SQ}YKz8?GE1Yp^+DjH8z*pS z;q=EEA#Y8Q)L*3KYJEqzR~Mnh96}7BJsz!`!mP_rW4QzP*Jb~)e7X{GKS5Q>5b0i8 z(jC9%>-B!vj}WK6ilQO_37@PNAPKG=zvlh7wZ^wOLQCX&I0Nxe(-`k(0^(FZL+;OgEme&;!8~!i6uxZdRNH4spKm@4}Vt$fzOLxs__Alx0D)v&6){xHd zN>xh)p|XB`x=qvN`jsz4Xx+9N>#gDYADD}jQ+~wPuclXR|8KzHV7M^UKv&4=t z)a4m#(R5vc%NY(vq}wko^G`>lB6(5c5_u^Q)M&H3otHg;tRF*L5z9$##A&|qtU|lt zZ@?f9Ib-o!<-?z=D>r&meJ#N%b@UXBFH$PpbxIk7 zw0$$nRy@aZrWvc?YT5V>(T89=MZ}cppLL@vTZ?mImu1VdVG`&bH#ziBhTS`amgk!{ zy#2ZPa1S&CZ9L`ub}hPZ2w|hna5d8W({0$c@x6#G#c8DX310yMY4@&FLq6zg3%|_; zum@D=#OkE^V$bEsFaq>#*6{?SMYnfvs_^V6b281kJz7}9J%rp(HnhL!{kM!-=&6p2 zc?v`pS?xo`#I8Q)H|FLe#oo9OouB_6pskn6Hxg7#p?(AkAdqS)20~vi+JfgWW-n7l zXP096edH^a55RiWLVa6XoD8p%Cm?g_i>|higsHHBn@`wJ$GBd%ywiCx_<3ism(^E~_wo00wSuQ1#;PqjUsLCFMMyX)_&p*5!d zH_D{_uzmmE?&zVe2h`{$;bFf`hKXQ!=eMG_2V6qFf4xGkP6H^Z=z;Uw6TP9of`akm z0*}y0lgr=iX(I=s<(3v<@KY32-e-8)rPFDI)W7V1);pqA>7jvc$X#i;aM2)8&JDO^r>{GU0B|=(zsn@J|QdUxpIKPFG1x8Rm zCL@Y~;jP^s%yHqZr5N$oM$J~+ZQD95P+et-zoo?J#7)j$y}P3jyEtNNy2b1S4R{-l z&mWX#w`1fX6E23oMD`2BTPY@F}L)8jicNwToX#s ztjvi6ZpUkn(uLeV&mex%X}~W}u2Zu8qc|XPY-#UDRk1l$)L)w-yF%QtH|xENm~!`T zYFu<29F#!gpQkEbJk`H`yNn`aJ}7q#by9eHN#E17Tk{hSO{`1lDcb-(mI6tkdGZWB zjVt*QWtDi0M98yVf5h#m^S!o>zp+*}wn3rV!#5BR#*3HDIwdd!+oj}(No4)GH-bQd z1dBk_@llx6#&BYy0Il)d1I~`*p6%c|aTv=wZLUS^1Se4s+HlW@A`kP2cN@OFLp)J! zXk08S`28ip@3o>N7*J}ogY(VFzt-~5BjKXd*Vngocf;sW0E|fJ4dE-DF8;fJUaa+F zbmKGp$LaT&0Zk2i#@>=pL?f#}8n#DEy3AYxFSlHYVI)gQMm@Hb+9PU}zFKRLz13}@ zuRLU{`@QP)sEA3m!^D|yeg67r0AbkyqG*493|+y`8bV4F@eA{8o^atNO&_3ZBryyVkVGx z{7qTxveIDwDgCacB-&k3W!!u|C86jjPv4VOV5^U#+k3pT2ijJ2GnBpLRM)LYrek&D zlOmDOSX1!6&Dv?qSQh3QY@M=j=^hPROyvpow4JLe;5>xG|Iu%I>wRkz=uIa0cN=rx zI$7vx4B7X|D&DI)b=p`kBtIo3=9s&DhG=$g+IOGrac_CrhuHG8MA4j@tt=qzQAD_= zSPnW$vhL@eie!|NBP>5^8{bh$*vBq3e{ybV;^t~9e_iF=kmlOGYO8(Xk#^aHfXiRZ zEt@bG-`pW?`Dgr{g(qsI-q{E}bJP7`e&eO`Xu*BvOZmBoQHpDtlhy&ZjpK4kKbVgX4QH?YDB=>9KhIQeol6ZfC@jJN&6;^~ zXn#N1x}p>}|NY#hcN})|+b3{twv!qS{+MAEb>}TA;h9@MM^^3VF-MhO$BJUnxf9Ot zjqu+a4dY!IKD?)@Q??kL?E1-D3aTTuoTHv$sDlDMP)TSA6ca6bjxG|#O4#w$J%3d0i`>HNsq?j{tn-}pz!HUw|5$z}Sq45Oc(OOYVo z9E@6VIyKAls0$ z`gpzHs1Sx>PCGPU4pb4#wJhkfw~(M!6ugJvg{87{m%G5HYc^mt3dl0WkSgv`i<(h0 zZe>P74MXG~>)={WO>lAM$?t5%G=p$<|B~%GYftGrUQCp3Lp(61fzOnM`a!A|{8Jd| zV{yMRv!IBGr)M6*v$ZTIbH;JkJQtYPckbdGyW6?0JQUcVdj*>W;1&!O+dPbMA5s+Q z3C?c4!BsHPQ0mJf*fCB2<}G9`qJCM`W=-s_*>Fws>9N*FzNAMQ^wgT1_8{W0SK0mz zlWu|;jXOUBt56~I=Li8b0+?IuKCPy9I>SNN;Uw@(de?3$u!E7w2C&+7{PRJMXUZuoI2@3C`E-3wsxuoEFIPn z+^9RDvi&tsM7FoLK|FIglWj%q^YqNB3(DlhJQDXic4KxrvZxVwzg=rh-lB^`LPE0R zBmj+)mhg~8^dPER6QVrl_%HFE+Oc8kt4Pf^!_v=PJudrBTut!~IZ^a*Q6;1%ou7S` zgN07)21|5}BQ77*urV_yYXz;57BkG#bOXu+;TQ#WNY{}qw5J9hpUb{b8_X?f-6Lz5 zw5^)LyM5ZzU?q~!00$A~arcqazifXdGC+k8xGdJz){qku6T!j_x8JKf7ZUQ@dYzu! zZ#SHclh0*uFVNQZpvzDjkyRv*@a$h@JCd~UX}!-`G~g!7n%NKQN!mSSh=1bILJ9ao z_A{2M+GP_{&HPuxp802~v2isEhZsF3TGNAWzve~BqD&&aw2Wf6Ex%q?A1oTfUqobX zoOZAiqP0k5*_HOaEaX&+XG0ILbfncg%~y#(sW)Hj|2$rF5qDel=r=h^62o{r^s?1w zU;T*fAF7ES{o-=1wGJHK&aJ4T@}83G@Jr{a@&U7!fCy>DhUensvjpcdue1{CqB9B* z;;VM(6WqbrECh-Gbz*wIb#byyi^|_LDw_SFQKx_EL{-GYk~p934rH|TV@(++^B$S$ zzn54&S+_$G3|i-nI6YWNbl9qhYtSQU5uUM%2|mBB`jzx0Pg+@jT%TR6MR(*k5`3mx zpB>j3dzVl61q)^;dr*XmOE=5JYT}UAyoHkUcHc@O@yw2Y-LiCmQHts$Gdvh7nurH$@y7R76$*(cIGUbalfWyShG$~9V^>5?8h2@ zSmMTKt~*5|ZUa^}Oh12na=v3(qn*7l%+!LDush1xr?>pyoJ#K^#J|GG!h-9MCz-vT zzWpR;SxEUGWDd&|+9P4KTZDKFq3CU&n)+Ih-xNw}ycjrHy=upIYO~=v>PfSgtsQCJhIW&=tIs zO;hi;cs^QBLjv9xbiQrTC1EXYQgf6+2D4h^rzGT|hP!yEfk5mh%#f4ll=CvcZGh&f zX;C_73|DmlE==%fESAt@;iASdfkQ^?wdNT-{I=fwpclR(PL4G3)GA$Nw@h4+^ALaX zb)t{SVH@Gq=W|TjfY1Fj@<@$k4;5m}EE9ZmSj&y=<79LJhhoOPax2oaZofrA5I4@1 zN9spGC^-`EqYHmP!b&hrfyiC2zcW@=SO39-aA}%pW*e}=boiKmx3poJ-Iiui6aFTK zGE2J@L)~OCsG+Z~LH4&NjM|-ZI=BQTq2MRsht;Xcm~6~KFJK$z_C`vJ>1&W#L)jzg z%9AMzqD=>_(9Bb>8LdH=^m74WU3GQf$_mPLnUVhBLELYo$(`V?nNT4$Vx~n5d>V3j ztJFj*-cJ1~erH;$_8Z$HCondTXqZ83 z%I}6MA*S8siS04L4+)hrOW#V~{@VA+FUd&h(LhCJxxMu+Yr(8qupb*x>TNA=RPO6& z#*UFDhNhVC=Re_*pVD@ITb;y+2MwEWeUfb3FIsP2P~jD&-eEU$=#tL6b(_F*1X!~E z$`t*tPvC@nlD^+vQ-9d%QCcQvYS`af;qw|-MBH6FG(#TEGB@@ruI(Eu`zifgCc%q5 zxg>$bn=G8?%{zy}C=m?G>CxhpvQ!Rn^|Z4#XjWx3dg;y>-D9_o?)%KR&1v-(lX z!7P0RbAr#;?Y{X!^H22ejc%G#hO?OQa--ZM)LzM1Xy-BZ@!)!TP+STpl**$aAC<`= zisGC4?}?40h6H`dV{7^}@c7?X62#gc(c@3j#0*wBZX%Q;gAWW9$welNnnql!tZa@V zNPl~BD&Ne)*YYE2EuWhb9qTy#bZ2u=r*}-TXKPwgJY}OLd8l?&np2#uJF~S1H>W-SL|h>J_)5|CGEE~kWaU2(nwf1e zr-VzUEX;UPXm-q(X5YKFehM!&gj{w9BLt3CYDX#yL;F&CAfrm|b??>HHU7`CV=}V#_I=!|~m$27R?UIC0u_yVJ7Ji2Hu*eW93KgU)dC1$<6# zqk#rZUn>D!QdiBLcVGLoSqX;-oKS94Y&b9Sm%5B{Z(3!NisQ*94e?J)d-g-SG>UYV z(J(&TCO67QYgWo(?h`oioNGq_cUQOc(0CE6DAa3R*?+p<7!5MbrOBH`(~O`#Oy%|d zrS(&^Mz#%12rpYFi;*r*CI#oK_dm3Ft4^*b{vw=05zWl)#;3G;+aiX$wgDxK9k*XF zYrAhN7HtpH?cT)+zxweN7Zr)3S4-PqyG)GZtZbk4i%Ej8o(7>oviWb=ryOU454|I!X_^dGY5*?yb}L-Q+2N5cCOmx7i<@eNcGf~G`n33s+o9k zlNoFLZm{k9Ji=2_3RB(kj<&UCYAd1{z71V=jaoQJ_|dk3oMA3eX-y8kAZJWwvg~{P zd6PEomvqa8S~KsjHow2M>Jj@Z3mj+v_5V`B1;v!QE>X-AG!-78p&2PMsKByhu}obq z{+OA9|F>Bz#rbVN&GMh3)oM^0lo>Nr-D=rZJ9G|G9OrVSrZ zGU_htFC-(Ey{)^fkO7_Qma8VmErO=m*d`lTUCirVtAsCQM!kv24A2tt(dz zpWIE_3Z1u9;Xih!-uG61c>#kbRaYoZ6&#UYPD6t#c8p8*VM7a#Y>K`k;~M`=8Ooo% zFO0{+%TxGWe6VLOIa75!Qrs6WQf8i9;J<8Br@}~BCQROd#4;dJJS71Q0sLmrwmZbm z$lQ3$(Zt(z>Gihky8+e2iGSk=Llw_j#g*2nn9^9tc=i?jd6qi5!`!9$`@Cnd0^_+P z%SAJ~ut(b6d9WTB=Ydv2ut{|o$m7ZV^I#j132snMrOa6|OowTh+y^5<-L>?`tFmA1 zMs^w3Bg}rDX^#IC3$O=bGSMuW%7Jpljxy&kaf2#oy(9 z!q>R#DW~_&$5aa*w<{#TP~1>(cke7gS)NKFvG?wVpC=5^HT-E4+(UzjaYxCXGfyqP z+UI%yQXzcu^PVPOvIye-2(-GaY-}79*f7RFLTGgg2Sa3BQBf`+w>KzX5k~}ohKdXn zV~k)469r$gk54n#$U%fI+K}A)=%M~CQFZiPs%G}Ai-4vnC~wFO6=5*{1bDlnZ2%2N zluyu$RTBL;GM0SlEj?t;W1TXKjzWE3t;V!Emh*<@lhC~cR;O6?9N|;jrJAFv9VtM| zfG){8Z9>a>W35P|SiY%8wUH?*?PmB_*UwO-!587~_z-V4c{YX)x+l~FYZJH>bH}JfSxFoLEX9~SsYD`2z9%tz19>VXCsa>sd#xyM<1kRMKU*)(yQ(HTz zP1S1@v#?ls%;rrtwzcG;*hYBL z3+FfSnWh=d=JPLf33@H>+fH~{zu;j-`t5Q#mjK_VZ~AMko++ei?c9veDU}buRjzpb zNL+;QcY=NCDJ2fk_0iwIvjwOXW$-)trZ$>r-}f^OmL&pZO#-$ba!W78Sz)CiQL>PI z+P_U#!`2;~ybUOew0F)6J+0p?&KY z$0+1Kkgi&BW}HD}w)4L!)kraCZa@bU9_P2gFN+r3WM%!0Nd=38>Xpw(62ic5F(wYs7+J&X_d6)|>!-cv zQ>Xo-*!>v8foB9Td)PQQKxGsGRBt<^ifN@Uo;c(=NRjC1=s$Z?uV!nb*jlfU^?a`B zn7;M{8t%5WzQi;Oe-&6;fpqhxcnY7cX5tdAW@R*Wi>EWwJvT=RzR}*zE!3eQvb^;M zBf)rRkSYqXvxC0ub&A!&-j%556Sh5)0lM`Oc0cD?J{~%k<>}2PW@k?o@p$!~8gr{i zq)}`qXJA_F6@JB64hI;8IpPSa0c74BgeQJ!#(c|LcSAY!Ri$)o!GHJcUtt?6X z7MYUj3YO!0o#-o`Gu9U4d$*1tL6Ld!N;HiYWyZOizl)La%?EXLzUUWDGmDFGVEIWa&B}hG+$C2vgAj}5clV-9Ct?koxvxCac{?G5 zu=b^;bVk+=ev)8jnacz^)N2oiO}ND!$0<#`2+FQD#;&Vauy6piSlvSR)D%(CR1vk) z$f+IJa@yBSRMM9hLO?eT7eS>JR-U3AZeR>dXyt8rXUKEbd~!EI@$NZ(6_qj3cqae% zB?vdK!_X6V76>llU2AzWJrV5NO9JU`0d7SCOW4tItj~5NM`gab*GctQGWb?Nu(+M? z5xKay{JFb!B*X{~W|;%g_ycWV{+!h4kC4(%hg59(HXxRybDp2w`uvP}*v2L}NDRR8 zF%tt)aqNHO319<8PNfuEL2=?H!!k=on{ggHbOE1ket_1Xi;i9oMN8y#%skNr@)UU1 zksM#-=zt+6d(kG4BjkU48K9UtoZ%pEJX)ePiXXveg8LT1H#ojgK~cnLEx>?*0r5#sy+XeOoCMrpwj6uAJw-dD0G}N% zWxsLf@|I6fkifbfc$1Qc9vmm22??NXTU~uFkQNOET3_}FHivBw6yzfK^sFBbdgi&l zVVbzU<_G_8wUGXD449UILJnk`QJcC4KZ(10n9om~UVBKFH#yi#z#V25G3m^}K#YQs zekW5w3XHx6P&BKB9w4jdn-{1tnbj=-C;$_CU^@h`MQgVwg4y9BzW@xb?I^7AxrQ7bn+}OT9Qx4-L`-}z} zs=adoaChKv4MnfN^}-)-z0e6DP$<+YbR`%+!M4#N2K4Xl?#rbX&rj6ofa45$)I77l z3Yfadu^a%tHVZtUCCg@%C{)AG>jV%1V)Ct$5=dQ)UVDA$!xe-^2vFr8am)TFo7o4q zVn^{9kZ=GAfpG{Z3=9km^^J}AX+DH7aQ*BbPzYd^R##RaAi;4>2w?%c1x4Wi80%mp zhnnE$!MU3jU8O<(ITQN$UxV1vQbJJ7AdXr^eXakemn4L3}~Fny97Ej}`^RC%aZcWeP70Kr#XI6FU?W)qWyrqs9(Ue08?V z9hcsfmhXO@8CHe@9??Sxlxg-JW!6NFZwht*463WENzudxp%4%i0Csg-JRbGhgGdja zR4LT}KLe=y9VnIp)(0&j#uzdZmr8uigPuV_8IY>IM-h_9uv+ zTwVaks_JSk2=4h+Va}hiF+30v09#_2z+!+|0g}0l0FH+6r9dh0W+-R)7xwS=fO!Ts zw1rv&Fc`UTl0`uj@?Ouw`L{u=vMUl8Kp)7>T5CY-w6SCUkFNf)U=6kqfZ|f(I^VF4sB1|4FA&wpem5(Mlp$U$(YcJ}uEpjfyx zVFBFL22}3s;^Kov&5lp1ZUMNQB!F19&L^>VB82lFv9!LohXMQ|5*Xl1d+U~coT1scU5>m*t@6$yq+PIWDim&j2v7b9FK=C#5m z{%-O9Y}aRR%Xt&?XJ_mMxSuZr#sJ2CXtRJ*T7c|(0H#attT)aA&zik&HPkAMBeUkL zFf6?QQMQ)6-xz@Vp_KUI^1)oT(#R^E1N0SOK5YB@R|FDTy8u{JwfonK1fUGs-OQBh zt%FP`Dthn?fK^F2%q}cQf~FJoH4sNavNR6>A{@GqkE9EuY*T=H1k^T2Z~y&9@BqNs zCeCkd+={@e5cWtiVwfEhAZYmKCDgfERi>}tw`W!UM;?QX4e89Hrq+tN^riy6G5}l< zumA*{Bt1^IfP0hmp~C={#CEmtZWY znmUo-wLUv|Nm&p0TcA;7bqYoYg4)W1Z^)>WE(Elsp5tSVw{PFt0fJwMEVnapz*xxV z4!UUbUd?8j8z>}-?3=ELrLetW@HpJSA}%g2{s6be7{ST%zx!o-J3B}xd&SS6F~2?{ zFVuE>(5asSxdFfmum%CI3ei#)2oPXG1B$)vdPrLoJ&Liv6vH^IFt;|F5h$CH9GVGq zw2-w_pl@Oe(IRUUs~d3BqPzz!Q7vSCsPGl`VSpNC^6>uUEqbSN=n_zEfp$<-H2fu} zOrse55j$4+`^-hDqa=AFaO9o+Cp#twWz5*FQ`a?Z*ys^3(E^zS4W<&5Ua~JBm-l%m zR33idgtO1Px zAXNe+kH6{JTTq3@u8XIbv`dL0=q8fAz)HhB0Q_1KhMAs(7>_`sQ|bRd@kvdc^K)|k z%LCFbLPqQmrq^7=ya|Of`!j3It-P3i#CjUIsEUw=3o}*b0sKZ9Qt!S*T}Hw#8>t?; zfvOJ6*=icS@Ia&JRF{gkn*@rQ* zNMg+MA}&m${POgJY!!8NJ6Q|tmrXx+gysv!k0@^598m=vu+psMB}%cqw;_`-tOh#U z>s{k=e<4ak#*LHseVkF~CK}$^GYfrj)>m_fw61?S6dil|1aNjKF(Ww)jH$3cb@0h; zk7j>|`|?p>uwe|zCd|q;vb4N&2>d+zHfxj?3mWoV$4!mw!_kf;E4QlqN7rvwn)GP0 z{;z4jVSSP`Ac=z~I@uZyo{(i;D>cOxCQ5-S-lJ{E#4X~XBsKXX3lAk$zGlE*Go_~o zA;8A%KpLvN@VKDR6Y%XORGp67;x za<}WW#RHL*mzZn|{0;ljCv%pnKp()LM_%~b340R}??ix#^l++6x12#O(xHJlG>n#U zq4pTeFF=w6-V`0w*8oXZhctPxlc^jQff)r*4-Y0iBI#P7LxzSVwXE}jxCfZdg-Gf# zfrcpK4FG?nh~N1dL{@*yMC z#s4&`{K(=2Q;`z9JUfP^vqYBOncn z{W!bpB(RdYesG}RG?_=8NR8e#a)0j)>d2GqSBV3l1QGmoKH*l*#C6939c)m9Io=C+ z7wQJTIw4Fd{nufqMNM*O#g`NQaOA?(Ml?I5t zDL-lPnu3C2QbTEcGPQr%BAJ1rCD{3^2TT)wEtLkfleANy2 zz4`8o&vMX*t|0w!FC{6OJU)psXlEd;nK`4~^dJ+#KEw-|iTpb3YwwzwhnNv>mXQ@!NN=k>ul@|@Kt{@fMyq( z0f30J0f$cE0Ua9Vc!viEnTs}0*Rc%O~)OcjZLn_EP zVL?BtCnR#yf=ULvoOYT4uMCDZpc#O4hdyPYt-6Sp6W7I3eQJgj?;nK@!xrcdr_G6- zRV}2&Ozd*<{S!#Fa=hN#5esYDjPW^pfoCKhG7W~u)wem%l(9eLFA_sQ9GVdSBhmBn z|E`pgJ!SDE#~9Rdd^C;^9*Lklrf)xx?-UrS!O}T#=tFd!zbC#cW$ias;EdK6xS)1F z@Gt*SrOtRq;2+&Ks(f2;@nE&#G3*Nao{-V}3;G=y_iY9|blZDiV07hOw3*mNf?X$Rn8-`v$TlU*iA) z8Ogx=?69lq+;_|yqSG|eY!Bc}keGvlQ#%j-ZsToE+Y2(i8;^~*@wamWtnDzExQ1-G zuG4Ulqh!hQcGt-zfDTIc6Qx2`CkY=*$h#xXWe`ZNGsLC zAixZSL0E5Wk~vyzvAWMLjkkX8Ihsz1=I-}h&Yd+a382}fG}>LlXd_OJHZyjY>yEXS zF2C7&mbyDi;^WPI+@X*|(I5mb7kM}9JnqGXBN#RqT#cvWW@w#GA`+ z;rD6l^yuSmNGn@Vt<6^L%dLj8Mb>>se#Vz+rjA#He0Bu!GCz2@pKH%3*(pI4ab(?tpES-`-=acUUK|T^!Mln+Wj>P-ox9iWnpin J$|Upy{tw7h0`vd? literal 0 HcmV?d00001 diff --git a/resources/icons/app.png b/resources/icons/app.png new file mode 100644 index 0000000000000000000000000000000000000000..ca52173d6ddef03ba9ef44c7c56f270ca4ce5a2b GIT binary patch literal 9447 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 Date: Wed, 18 Feb 2026 13:14:17 +0100 Subject: [PATCH 15/16] fix: Improve checkout data handling in response processing --- src/webdrop_bridge/ui/main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index b4ac0b0..fbfc1b3 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -626,9 +626,9 @@ class MainWindow(QMainWindow): const data = await response.json(); console.log('[Checkout Check] Full data:', JSON.stringify(data)); console.log('[Checkout Check] Checkout field:', data.checkout); - const hasCheckout = data.checkout && Object.keys(data.checkout).length > 0; + const hasCheckout = !!(data.checkout && Object.keys(data.checkout).length > 0); console.log('[Checkout Check] Has checkout:', hasCheckout); - window['{callback_id}'] = JSON.stringify({{ checkout: data.checkout, hasCheckout: hasCheckout }}); + window['{callback_id}'] = JSON.stringify({{ checkout: data.checkout || null, hasCheckout: hasCheckout }}); }} else {{ console.log('[Checkout Check] Failed to fetch, status:', response.status); window['{callback_id}'] = JSON.stringify({{ error: 'Failed to fetch asset', status: response.status }}); From dffc925bb6bb11a8d771d1f481a19d509389c84e Mon Sep 17 00:00:00 2001 From: claudi Date: Wed, 18 Feb 2026 13:19:38 +0100 Subject: [PATCH 16/16] refactor: Change logging level from info to debug for download and JS messages --- src/webdrop_bridge/ui/main_window.py | 50 +++++++++----------- src/webdrop_bridge/ui/restricted_web_view.py | 14 +++--- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index fbfc1b3..dd1bf0d 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -238,7 +238,7 @@ class _DragBridge(QObject): Args: message: Debug message from JavaScript """ - logger.info(f"JS Debug: {message}") + logger.debug(f"JS Debug: {message}") class MainWindow(QMainWindow): @@ -327,7 +327,7 @@ class MainWindow(QMainWindow): # Set up download handler profile = self.web_view.page().profile() - logger.info(f"Connecting download handler to profile: {profile}") + logger.debug(f"Connecting download handler to profile: {profile}") # CRITICAL: Connect download handler BEFORE any page loads profile.downloadRequested.connect(self._on_download_requested) @@ -338,9 +338,9 @@ class MainWindow(QMainWindow): ) if downloads_path: profile.setDownloadPath(downloads_path) - logger.info(f"Download path set to: {downloads_path}") + logger.debug(f"Download path set to: {downloads_path}") - logger.info("Download handler connected successfully") + logger.debug("Download handler connected successfully") # Set up central widget with layout central_widget = QWidget() @@ -820,17 +820,13 @@ class MainWindow(QMainWindow): Args: download: Download request from the web engine """ - logger.info("=" * 60) - logger.info("🔥 DOWNLOAD REQUESTED - Handler called!") - logger.info("=" * 60) - try: - # Log all download details for debugging - logger.info(f"Download URL: {download.url().toString()}") - logger.info(f"Download filename: {download.downloadFileName()}") - logger.info(f"Download mime type: {download.mimeType()}") - logger.info(f"Download suggested filename: {download.suggestedFileName()}") - logger.info(f"Download state: {download.state()}") + # Log download details for debugging + logger.debug(f"Download URL: {download.url().toString()}") + logger.debug(f"Download filename: {download.downloadFileName()}") + logger.debug(f"Download mime type: {download.mimeType()}") + logger.debug(f"Download suggested filename: {download.suggestedFileName()}") + logger.debug(f"Download state: {download.state()}") # Get the system's Downloads folder downloads_path = QStandardPaths.writableLocation( @@ -850,14 +846,14 @@ class MainWindow(QMainWindow): # Construct full download path download_file = Path(downloads_path) / filename - logger.info(f"📁 Download will be saved to: {download_file}") + logger.debug(f"Download will be saved to: {download_file}") # Set download path and accept download.setDownloadDirectory(str(download_file.parent)) download.setDownloadFileName(download_file.name) download.accept() - logger.info(f"✅ Download accepted and started: {download_file}") + logger.info(f"Download started: {filename}") # Update status bar (temporarily) self.status_bar.showMessage( @@ -866,7 +862,7 @@ class MainWindow(QMainWindow): # Connect to state changed for progress tracking download.stateChanged.connect( - lambda state: logger.info(f"Download state changed to: {state}") + lambda state: logger.debug(f"Download state changed to: {state}") ) # Connect to finished signal for completion feedback @@ -875,8 +871,8 @@ class MainWindow(QMainWindow): ) except Exception as e: - logger.error(f"❌ Error handling download: {e}", exc_info=True) - self.status_bar.showMessage(f"❌ Download-Fehler: {e}", 5000) + logger.error(f"Error handling download: {e}", exc_info=True) + self.status_bar.showMessage(f"Download error: {e}", 5000) def _on_download_finished(self, download: QWebEngineDownloadRequest, file_path: Path) -> None: """Handle download completion. @@ -890,12 +886,12 @@ class MainWindow(QMainWindow): return state = download.state() - logger.info(f"Download finished with state: {state}") + logger.debug(f"Download finished with state: {state}") if state == QWebEngineDownloadRequest.DownloadState.DownloadCompleted: - logger.info(f"Download completed successfully: {file_path}") + logger.info(f"Download completed: {file_path.name}") self.status_bar.showMessage( - f"✅ Download abgeschlossen: {file_path.name}", 5000 + f"Download completed: {file_path.name}", 5000 ) elif state == QWebEngineDownloadRequest.DownloadState.DownloadCancelled: logger.info(f"Download cancelled: {file_path.name}") @@ -959,7 +955,7 @@ class MainWindow(QMainWindow): # Map JS log levels to Python log levels using enum if level == QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel: - logger.info(f"JS Console: {message}") + logger.debug(f"JS Console: {message}") elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel: logger.warning(f"JS Console: {message}") logger.debug(f" at {source_id}:{line_number}") @@ -982,11 +978,11 @@ class MainWindow(QMainWindow): # Check if bridge script is loaded def check_script(result): if result: - logger.info("✓ WebDrop Bridge script is active") - logger.info("✓ QWebChannel bridge is ready") + logger.debug("WebDrop Bridge script is active") + logger.debug("QWebChannel bridge is ready") else: - logger.error("✗ WebDrop Bridge script NOT loaded!") - logger.error(" Drag-and-drop conversion will NOT work") + logger.error("WebDrop Bridge script NOT loaded!") + logger.error("Drag-and-drop conversion will not work") # Execute JS to check if our script is loaded self.web_view.page().runJavaScript( diff --git a/src/webdrop_bridge/ui/restricted_web_view.py b/src/webdrop_bridge/ui/restricted_web_view.py index 7f20ad2..bd77741 100644 --- a/src/webdrop_bridge/ui/restricted_web_view.py +++ b/src/webdrop_bridge/ui/restricted_web_view.py @@ -74,7 +74,7 @@ class CustomWebEnginePage(QWebEnginePage): ] if any(url_str.lower().endswith(ext) for ext in download_extensions): - logger.info(f"🔽 Detected potential download URL: {url_str}") + logger.debug(f"Detected potential download URL: {url_str}") # This will trigger downloadRequested if it's a download return super().acceptNavigationRequest(url, nav_type, is_main_frame) @@ -90,13 +90,13 @@ class CustomWebEnginePage(QWebEnginePage): Returns: New page instance for the window """ - logger.info(f"🪟 New window requested, type: {window_type}") + logger.debug(f"New window requested, type: {window_type}") # Create a temporary page to handle the download # This page will never be displayed but allows downloads to work download_page = QWebEnginePage(self.profile(), self) - logger.info("✅ Created temporary page for download/popup") + logger.debug("Created temporary page for download/popup") # Return the temporary page - it will trigger downloadRequested if it's a download return download_page @@ -127,7 +127,7 @@ class RestrictedWebEngineView(QWebEngineView): custom_page = CustomWebEnginePage(self.profile, self) self.setPage(custom_page) - logger.info( + logger.debug( "RestrictedWebEngineView initialized with CustomWebEnginePage and persistent profile" ) @@ -170,9 +170,9 @@ class RestrictedWebEngineView(QWebEngineView): # Set cache size to 100 MB profile.setHttpCacheMaximumSize(100 * 1024 * 1024) - logger.info(f"Created persistent profile at: {profile_path}") - logger.info("Cookies policy: ForcePersistentCookies") - logger.info("HTTP cache: DiskHttpCache (100 MB)") + logger.debug(f"Created persistent profile at: {profile_path}") + logger.debug("Cookies policy: ForcePersistentCookies") + logger.debug("HTTP cache: DiskHttpCache (100 MB)") return profile