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.
This commit is contained in:
claudi 2026-02-17 19:19:14 +01:00
parent 88dc358894
commit dee02ad600
12 changed files with 2244 additions and 65 deletions

View file

@ -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: