diff --git a/src/webdrop_bridge/ui/bridge_script.js b/src/webdrop_bridge/ui/bridge_script.js new file mode 100644 index 0000000..aa5b8a3 --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script.js @@ -0,0 +1,73 @@ +// 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; + + 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; + cb(); + }); + } + } + + if (window.QWebChannel) { + init(); + return; + } + + 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(); + } + } + } + if (!path) return; + + // 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(); + ensureChannel(function() { + if (window.bridge && typeof window.bridge.start_file_drag === 'function') { + window.bridge.start_file_drag(path); + } + }); + }, false); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', hook); + } else { + hook(); + } +})(); diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index 0a0f0bf..1bfcbcc 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -5,7 +5,9 @@ import logging from pathlib import Path from typing import Optional -from PySide6.QtCore import QObject, QSize, Qt, QThread, QUrl, Signal +from PySide6.QtCore import QObject, QPoint, QSize, Qt, QThread, QTimer, QUrl, Signal, Slot +from PySide6.QtWebChannel import QWebChannel +from PySide6.QtWebEngineCore import QWebEngineScript from PySide6.QtWidgets import QLabel, QMainWindow, QStatusBar, QToolBar, QVBoxLayout, QWidget from webdrop_bridge.config import Config @@ -170,6 +172,39 @@ DEFAULT_WELCOME_PAGE = """ """ +class _DragBridge(QObject): + """JavaScript bridge for drag operations via QWebChannel. + + Exposed to JavaScript as 'bridge' object. + """ + + def __init__(self, window: 'MainWindow', parent: Optional[QObject] = None): + """Initialize the drag bridge. + + Args: + window: MainWindow instance + parent: Parent QObject + """ + super().__init__(parent) + self.window = window + + @Slot(str) + def start_file_drag(self, path_text: str) -> None: + """Start a native file drag for the given path. + + Called from JavaScript when user drags a Z:\ path item. + Defers execution to avoid Qt drag manager state issues. + + Args: + path_text: File path string 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])) + + class MainWindow(QMainWindow): """Main application window for WebDrop Bridge. @@ -221,7 +256,6 @@ class MainWindow(QMainWindow): # Create drag interceptor self.drag_interceptor = DragInterceptor() - # Set up path validator validator = PathValidator(config.allowed_roots) self.drag_interceptor.set_validator(validator) @@ -230,6 +264,15 @@ class MainWindow(QMainWindow): self.drag_interceptor.drag_started.connect(self._on_drag_started) self.drag_interceptor.drag_failed.connect(self._on_drag_failed) + # Set up JavaScript bridge with QWebChannel + self._drag_bridge = _DragBridge(self) + web_channel = QWebChannel(self) + web_channel.registerObject("bridge", self._drag_bridge) + self.web_view.page().setWebChannel(web_channel) + + # Install the drag bridge script + self._install_bridge_script() + # Set up central widget with layout central_widget = QWidget() layout = QVBoxLayout() @@ -248,6 +291,7 @@ class MainWindow(QMainWindow): """Load the web application. Loads HTML from the configured webapp URL or from local file. + Injects the WebChannel bridge JavaScript for drag-and-drop. Supports both bundled apps (PyInstaller) and development mode. Falls back to default welcome page if webapp not found. """ @@ -282,15 +326,55 @@ class MainWindow(QMainWindow): self.web_view.setHtml(welcome_html) return - # Load local file as file:// URL - file_url = file_path.as_uri() - self.web_view.load(QUrl(file_url)) + # Load local file + html_content = file_path.read_text(encoding='utf-8') + + # Inject WebChannel bridge JavaScript + injected_html = self._inject_drag_bridge(html_content) + + # Load the modified HTML + self.web_view.setHtml(injected_html, QUrl.fromLocalFile(file_path.parent)) except (OSError, ValueError) as e: # Show welcome page on error welcome_html = DEFAULT_WELCOME_PAGE.format(version=self.config.app_version) self.web_view.setHtml(welcome_html) + def _install_bridge_script(self) -> None: + """Install the drag bridge JavaScript via QWebEngineScript. + + Follows the POC pattern for proper script injection and QWebChannel setup. + """ + script = QWebEngineScript() + script.setName("webdrop-bridge") + script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady) + script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld) + script.setRunsOnSubFrames(False) + + # 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()) + self.web_view.page().scripts().insert(script) + logger.debug(f"Installed bridge script from {script_path}") + except (OSError, IOError) as e: + logger.warning(f"Failed to load bridge script: {e}") + + def _inject_drag_bridge(self, html_content: str) -> str: + """Return HTML content unmodified. + + The drag bridge script is now injected via QWebEngineScript in _install_bridge_script(). + This method is kept for compatibility but does nothing. + + Args: + html_content: Original HTML content + + Returns: + HTML unchanged + """ + return html_content + def _apply_stylesheet(self) -> None: """Apply application stylesheet if available.""" stylesheet_path = Path(__file__).parent.parent.parent.parent / \ diff --git a/src/webdrop_bridge/ui/restricted_web_view.py b/src/webdrop_bridge/ui/restricted_web_view.py index 4e03a8d..d7b28cc 100644 --- a/src/webdrop_bridge/ui/restricted_web_view.py +++ b/src/webdrop_bridge/ui/restricted_web_view.py @@ -98,3 +98,4 @@ class RestrictedWebEngineView(QWebEngineView): return True return False + diff --git a/webapp/index.html b/webapp/index.html index e4ace2d..ac302bf 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -163,13 +163,13 @@
Z:\samples\image.psd
+Z:\data\test-image.jpg
Z:\samples\document.indd
+Z:\data\API_DOCUMENTATION.pdf
WebDrop Bridge v1.0.0 | Built with Qt and PySide6