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

222
QUICKSTART_DEBUGGING.md Normal file
View file

@ -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: <div class="...">
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!

View file

@ -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
<!-- Drag Group (oberste Ebene) -->
<div cdkdroplistgroup="" aydnd="" class="flex h-full flex-col">
<!-- Drop Zone (Collections) -->
<div cdkdroplist="" class="cdk-drop-list" id="collectioncsuaaDVNokl0...">
<!-- Draggable Element (Asset Card) -->
<li cdkdrag="" class="cdk-drag asset-list-item" draggable="false">
<img src="./GlobalDAM JRI_files/anPGZszKzgKaSz1SIx2HFgduy"
alt="weiss_ORIGINAL">
</li>
</div>
</div>
```
### 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
<!-- Asset ID im Element-ID -->
<div id="anPGZszKzgKaSz1SIx2HFgduy">
<!-- Asset ID in der Bild-URL -->
<img src="./GlobalDAM JRI_files/anPGZszKzgKaSz1SIx2HFgduy">
<!-- Asset Name im alt-Attribut -->
<img alt="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

View file

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

171
docs/SCRIPT_VARIANTS.md Normal file
View file

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

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)