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

@ -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...');

View file

@ -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;');
})();

View file

@ -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);
}
})();

View file

@ -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);
}
})();

View file

@ -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;');
})();

View file

@ -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);
}
})();

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:

View file

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