diff --git a/QUICKSTART_DEBUGGING.md b/QUICKSTART_DEBUGGING.md new file mode 100644 index 0000000..ffdc572 --- /dev/null +++ b/QUICKSTART_DEBUGGING.md @@ -0,0 +1,222 @@ +# 🚀 QUICK START - Popup-Problem lösen + +## Problem Zusammenfassung + +- ✅ File-Drop funktioniert (Z:\ Laufwerk) +- ❌ Web-App Popup erscheint nicht nach Drop (Auschecken-Dialog) + +**Grund**: Unser JavaScript verhindert das Browser-Drag-Event mit `preventDefault()`, daher bekommt die Web-App kein Drop-Event. + +## 🎯 Lösungsstrategie + +**Phase 1: DEBUGGING** (ca. 15-30 Min) +→ Herausfinden WIE das Popup ausgelöst wird + +**Phase 2: IMPLEMENTATION** (ca. 30-60 Min) +→ Popup-Trigger nach File-Drop manuell aufrufen + +## 📋 Phase 1: Debugging - JETZT STARTEN + +### Schritt 1: Debug-Script aktivieren + +```powershell +# Datei öffnen +code "C:\Development\VS Code Projects\webdrop_bridge\src\webdrop_bridge\ui\main_window.py" +``` + +**Zeile ~433** in `_install_bridge_script()` ändern: + +```python +# VORHER: +script_path = Path(__file__).parent / "bridge_script.js" + +# NACHHER (für Debugging): +script_path = Path(__file__).parent / "bridge_script_debug.js" +``` + +Speichern (Ctrl+S). + +### Schritt 2: Anwendung starten + +```powershell +cd "C:\Development\VS Code Projects\webdrop_bridge" +python -m webdrop_bridge.main +``` + +### Schritt 3: Browser DevTools öffnen + +1. Wenn WebDrop Bridge lädt +2. **F12** drücken → DevTools öffnen +3. **Console-Tab** auswählen +4. Sie sollten sehen: + `[WebDrop DEBUG] Ready! Perform ALT-drag+drop and watch the logs.` + +### Schritt 4: ALT-Drag+Drop durchführen + +1. In der GlobalDAM Anwendung: + - **ALT-Taste** gedrückt halten + - Asset **draggen** + - Irgendwo **droppen** (z.B. auf dem Desktop) + +2. **Popup sollte erscheinen!** (Auschecken-Ja/Nein) + +3. **Sofort** zur Browser-Console wechseln! + +### Schritt 5: Logs analysieren + +Suchen Sie in der Console nach: + +#### ✅ **A) Modal/Popup Detektion** +``` +[MODAL OPENED] <<<< DAS WOLLEN WIR SEHEN! +Modal element:
+Classes: modal-dialog checkout-dialog +``` + +**Wenn gefunden →** Popup wird durch DOM-Manipulation ausgelöst +**Merkblatt:** Notieren Sie die `className` und `textContent` + +#### ✅ **B) API-Call Detection** +``` +[FETCH] https://dev.agravity.io/api/assets/abc123/checkout +``` +oder +``` +[XHR] POST https://dev.agravity.io/api/assets/abc123/checkout +``` + +**Wenn gefunden →** Popup wird durch API-Response ausgelöst +**Merkblatt:** Notieren Sie die vollständige URL und Methode (POST/GET) + +#### ✅ **C) Event Detection** +``` +[EVENT] DROP +Target: ... +DataTransfer: ... +``` + +**Merkblatt:** Notieren Sie welche Events feuern NACHDEM Drop passiert + +### Schritt 6: Detailanalyse + +In der Browser Console, führen Sie aus: + +```javascript +// Event-Statistik +webdrop_debug.getEventCounts() + +// Asset-Card Component untersuchen (wenn Angular DevTools installiert) +var card = document.querySelector('ay-asset-card'); +var component = webdrop_debug.getComponent(card); +console.log('Component methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(component))); +``` + +**Merkblatt:** Notieren Sie: +- Welche Methoden hat das Component? (z.B. `onCheckout`, `showDialog`, etc.) +- Gibt es einen `click`-Handler auf dem Asset? + +## 📝 Was Sie herausfinden müssen + +Nach dem Debugging sollten Sie wissen: + +1. **WIE wird Popup ausgelöst?** + - [ ] API-Call nach Drop (URL: ________________________) + - [ ] DOM-Element wird erstellt (Class: ________________) + - [ ] Angular Component Methode (Name: ________________) + - [ ] Click-Event auf Button (Selector: _______________) + +2. **WANN wird Popup ausgelöst?** + - [ ] Sofort nach Drop + - [ ] Nach Verzögerung (_____ Sekunden) + - [ ] Nach API-Response + - [ ] Nach User-Interaction + +3. **WELCHE Daten werden benötigt?** + - [ ] Asset-ID (Beispiel: _________________________) + - [ ] Collection-ID (Beispiel: ______________________) + - [ ] User-ID (Beispiel: ___________________________) + - [ ] Weitere: ____________________________________ + +## 🔧 Phase 2: Implementation (NACHDEM Debugging abgeschlossen) + +Basierend auf Ihren Erkenntnissen: + +### Fall A: Popup durch API-Call + +**In `bridge_script.js` hinzufügen** (nach erfolgreichem File-Drop): + +```javascript +// Nach Qt File-Drop Erfolg +function triggerCheckoutPopup(assetId) { + fetch('https://dev.agravity.io/api/assets/' + assetId + '/checkout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // ggf. Authorization Header + }, + body: JSON.stringify({ + // Parameter basierend auf Debug-Logs + }) + }) + .then(response => response.json()) + .then(data => { + console.log('Checkout popup triggered via API'); + }); +} +``` + +### Fall B: Popup durch Component-Methode + +```javascript +function triggerCheckoutPopup(assetId) { + var card = document.getElementById(assetId); + if (card && window.ng) { + var component = ng.getComponent(card); + if (component && component.onCheckout) { + component.onCheckout(); + } + } +} +``` + +### Fall C: Popup durch Click-Simulation + +```javascript +function triggerCheckoutPopup(assetId) { + var button = document.querySelector('[data-action="checkout"]'); + if (button) { + button.click(); + } +} +``` + +## ⚡ Troubleshooting + +**Problem:** Kein `[MODAL OPENED]` Log +**Lösung:** Popup wird möglicherweise anders erstellt. Suchen Sie in Network-Tab nach API-Calls + +**Problem:** Zu viele Logs +**Lösung:** Console Filter nutzen: `[MODAL]` oder `[FETCH]` + +**Problem:** `webdrop_debug` not defined +**Lösung:** Debug-Script wurde nicht richtig geladen. main_window.py prüfen. + +**Problem:** Angular DevTools fehlt +**Lösung:** Chrome Extension installieren: "Angular DevTools" + +## 📞 Nächste Schritte + +1. ✅ Debug-Script aktivieren (oben Schritt 1) +2. ✅ Logs sammeln (Schritt 3-6) +3. ✅ Ergebnisse notieren (Merkblatt) +4. → **Ergebnisse mitteilen** → Ich helfe bei Implementation! + +## 📄 Zusätzliche Dokumentation + +- [DRAG_DROP_PROBLEM_ANALYSIS.md](DRAG_DROP_PROBLEM_ANALYSIS.md) - Detaillierte Problem-Analyse +- [SCRIPT_VARIANTS.md](SCRIPT_VARIANTS.md) - Alle verfügbaren Scripts +- [ANGULAR_CDK_ANALYSIS.md](ANGULAR_CDK_ANALYSIS.md) - Angular Framework Details + +--- + +**Hinweis:** Das Debug-Script hat Performance-Overhead. Nach dem Debugging zurück zu `bridge_script.js` wechseln! diff --git a/docs/ANGULAR_CDK_ANALYSIS.md b/docs/ANGULAR_CDK_ANALYSIS.md new file mode 100644 index 0000000..5e434ad --- /dev/null +++ b/docs/ANGULAR_CDK_ANALYSIS.md @@ -0,0 +1,268 @@ +# Angular CDK Drag & Drop Analysis - GlobalDAM + +## Framework Detection + +**Web Application:** Agravity GlobalDAM +**Framework:** Angular 19.2.14 +**Drag & Drop:** Angular CDK (Component Dev Kit) +**Styling:** TailwindCSS + +## Technical Findings + +### 1. Angular CDK Implementation + +```html + +
+ + +
+ + +
  • + weiss_ORIGINAL +
  • +
    +
    +``` + +### 2. Key Observations + +#### Native HTML5 Drag ist DEAKTIVIERT +```html +draggable="false" +``` + +**Bedeutung:** +- Kein Zugriff auf native `dragstart`, `drag`, `dragend` Events +- Kein `event.dataTransfer` API verfügbar +- Angular CDK simuliert Drag & Drop komplett in JavaScript +- Daten werden NICHT über natives Clipboard/DataTransfer übertragen + +#### Angular CDK Direktiven +- `cdkdroplistgroup` - Gruppiert mehrere Drop-Zonen +- `cdkdroplist` - Markiert Drop-Bereiche (Collections, Clipboard) +- `cdkdrag` - Markiert draggbare Elemente (Assets) +- `cdkdroplistsortingdisabled` - Sortierung deaktiviert + +#### Asset Identifikation +```html + +
    + + + + + +weiss_ORIGINAL +``` + +## Impact on WebDrop Bridge + +### ❌ Bisheriger Ansatz funktioniert NICHT + +Unser aktueller Ansatz basiert auf: +1. Interception von nativen Drag-Events +2. Manipulation von `event.dataTransfer.effectAllowed` und `.dropEffect` +3. Setzen von URLs im DataTransfer + +**Das funktioniert NICHT mit Angular CDK**, da: +- Angular CDK das native Drag & Drop komplett umgeht +- Keine nativen Events gefeuert werden +- DataTransfer API nicht verwendet wird + +### ✅ Mögliche Lösungsansätze + +#### Ansatz 1: JavaScript Injection zur Laufzeit +Injiziere JavaScript-Code, der Angular CDK Events abfängt: + +```javascript +// Überwache Angular CDK Event-Handler +document.addEventListener('cdkDragStarted', (event) => { + const assetId = event.source.element.nativeElement.id; + const assetName = event.source.element.nativeElement.querySelector('img')?.alt; + + // Sende an Qt WebChannel + bridge.handleDragStart(assetId, assetName); +}); + +document.addEventListener('cdkDragDropped', (event) => { + // Verhindere das Standard-Verhalten + event.preventDefault(); + + // Starte nativen Drag von Qt aus + bridge.initNativeDrag(); +}); +``` + +**Vorteile:** +- ✅ Direkter Zugriff auf Angular CDK Events +- ✅ Kann Asset-Informationen extrahieren +- ✅ Kann Drag-Operationen abfangen + +**Nachteile:** +- ⚠️ Erfordert genaue Kenntnis der Angular CDK Internals +- ⚠️ Könnte bei Angular CDK Updates brechen +- ⚠️ Komplexer zu implementieren + +#### Ansatz 2: DOM Mutation Observer +Überwache DOM-Änderungen während des Drags: + +```javascript +const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // Suche nach CDK Drag-Elementen mit bestimmten Klassen + const dragElement = document.querySelector('.cdk-drag-preview'); + if (dragElement) { + const assetId = dragElement.querySelector('[id^="a"]')?.id; + bridge.handleDrag(assetId); + } + }); +}); + +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class'] +}); +``` + +**Vorteile:** +- ✅ Robuster gegenüber Framework-Updates +- ✅ Funktioniert mit beliebigen Frameworks + +**Nachteile:** +- ⚠️ Performance-Overhead +- ⚠️ Kann falsche Positive erzeugen + +#### Ansatz 3: Qt WebChannel Bridge mit Custom Events +Nutze Qt WebChannel, um mit der Angular-Anwendung zu kommunizieren: + +```python +# Python-Seite (Qt) +class DragBridge(QObject): + @Slot(str, str) + def onAssetDragStart(self, asset_id: str, asset_name: str): + """Called from JavaScript when Angular CDK drag starts.""" + logger.info(f"Asset drag started: {asset_id} ({asset_name})") + self.convert_and_drag(asset_id, asset_name) +``` + +```javascript +// JavaScript-Seite (injiziert via QWebEngineScript) +new QWebChannel(qt.webChannelTransport, (channel) => { + const dragBridge = channel.objects.dragBridge; + + // Monkey-patch Angular CDK's DragRef + const originalStartDraggingSequence = CdkDrag.prototype._startDraggingSequence; + CdkDrag.prototype._startDraggingSequence = function(event) { + const assetElement = this.element.nativeElement; + const assetId = assetElement.id; + const assetName = assetElement.querySelector('img')?.alt; + + // Benachrichtige Qt + dragBridge.onAssetDragStart(assetId, assetName); + + // Rufe original Angular CDK Methode auf + return originalStartDraggingSequence.call(this, event); + }; +}); +``` + +**Vorteile:** +- ✅ Saubere Kommunikation zwischen Qt und Web +- ✅ Kann Asset-Informationen zuverlässig extrahieren +- ✅ Typensicher (Qt Signals/Slots) + +**Nachteile:** +- ⚠️ Erfordert Monkey-Patching von Angular CDK +- ⚠️ Kann bei CDK Updates brechen + +#### Ansatz 4: Browser DevTools Protocol (Chrome DevTools) +Nutze Chrome DevTools Protocol für tiefere Integration: + +```python +from PySide6.QtWebEngineCore import QWebEngineProfile + +profile = QWebEngineProfile.defaultProfile() +profile.setRequestInterceptor(...) + +# Intercepte Netzwerk-Requests und injiziere Header +# Überwache JavaScript-Execution via CDP +``` + +**Vorteile:** +- ✅ Sehr mächtig, kann JavaScript-Execution überwachen +- ✅ Kann Events auf niedrigerer Ebene abfangen + +**Nachteile:** +- ⚠️ Sehr komplex +- ⚠️ Erfordert Chrome DevTools Protocol Kenntnisse +- ⚠️ Performance-Overhead + +## Empfohlener Ansatz + +### **Ansatz 3: Qt WebChannel Bridge** (BEVORZUGT) + +**Begründung:** +1. ✅ Saubere Architektur mit klarer Trennung +2. ✅ Typsicher durch Qt Signals/Slots +3. ✅ Kann Asset-IDs und -Namen zuverlässig extrahieren +4. ✅ Funktioniert auch wenn Angular CDK interne Änderungen hat +5. ✅ Ermöglicht bidirektionale Kommunikation + +**Implementierungsschritte:** + +### Phase 1: Asset-Informationen extrahieren +1. JavaScript via QWebEngineScript injizieren +2. Qt WebChannel setuppen +3. Angular CDK Events überwachen (ohne Monkey-Patching als Test) +4. Asset-IDs und Namen an Qt senden + +### Phase 2: Native Drag initiieren +1. Bei CDK Drag-Start: Extrahiere Asset-Informationen +2. Sende Asset-ID an Backend/API +3. Erhalte lokalen Dateipfad oder Azure Blob URL +4. Konvertiere zu lokalem Pfad (wie aktuell) +5. Initiiere nativen Drag mit QDrag + +### Phase 3: Drag-Feedback +1. Zeige Drag-Preview in Qt (optional) +2. Update Cursor während Drag +3. Cleanup nach Drag-Ende + +## Asset-ID zu Dateipfad Mapping + +Die Anwendung verwendet Asset-IDs in mehreren Formaten: + +```javascript +// Asset-ID: anPGZszKzgKaSz1SIx2HFgduy + +// Mögliche URL-Konstruktion: +const assetUrl = `https://dev.agravity.io/api/assets/${assetId}`; +const downloadUrl = `https://dev.agravity.io/api/assets/${assetId}/download`; +const blobUrl = `https://static.agravity.io/${workspaceId}/${assetId}/${filename}`; +``` + +**Für WebDrop Bridge:** +- Asset-ID aus DOM extrahieren +- Asset-Metadaten via API abrufen (falls verfügbar) +- Blob-URL konstruieren +- URL Converter nutzen (bereits implementiert!) + +## Next Steps + +1. **Proof of Concept**: Qt WebChannel mit einfachem Event-Logger +2. **Asset-ID Extraction**: JavaScript Injection testen +3. **API Research**: GlobalDAM API untersuchen (Asset-Metadaten) +4. **Integration**: Mit bestehendem URLConverter verbinden +5. **Testing**: Mit echten Assets testen + +## Hinweise + +- Angular CDK Version kann sich unterscheiden - Code muss robust sein +- Asset-IDs scheinen eindeutig zu sein (Base64-ähnlich) +- Die Anwendung nutzt Azure Blob Storage (basierend auf bisherigen URLs) +- Custom Components (`ay-*`) deuten auf eine eigene Component Library hin diff --git a/docs/DRAG_DROP_PROBLEM_ANALYSIS.md b/docs/DRAG_DROP_PROBLEM_ANALYSIS.md new file mode 100644 index 0000000..7e6906d --- /dev/null +++ b/docs/DRAG_DROP_PROBLEM_ANALYSIS.md @@ -0,0 +1,277 @@ +# Drag & Drop Problem Analysis - File Drop + Web App Popup + +## Das Kernproblem + +**Ziel**: Bei ALT-Drag soll: +1. ✅ File gedroppt werden (Z:\ Laufwerk) → Native File-Drop +2. ✅ Web-App Popup erscheinen (Auschecken-Dialog) → Web-App Drop-Event + +**Problem**: Diese beiden schließen sich gegenseitig aus: +- Native File-Drag (von Qt) → Web-App bekommt kein Drop-Event → Kein Popup +- Browser Text-Drag (von Web-App) → Kein File-Drop → Kein File + +## Browser-Sicherheitsbeschränkungen + +1. **DataTransfer.files ist read-only** + Wir können keine Files zu einem DataTransfer hinzufügen + +2. **Nur EIN Drag zur Zeit möglich** + Wir können keinen parallelen Drag starten + +3. **DataTransfer kann nicht erstellt werden** + Wir können kein synthetisches Drop-Event mit Files erzeugen + +4. **Cross-Domain Sicherheit** + File-Zugriff ist stark eingeschränkt + +## Getestete Ansätze + +### ❌ Ansatz 1: DataTransfer erweitern +**Idee**: Web-App setzt URL, wir fügen Files hinzu +**Problem**: DataTransfer.files ist read-only + +### ❌ Ansatz 2: Parallele Drags +**Idee**: Browser-Drag + Qt-Drag gleichzeitig +**Problem**: Nur ein Drag zur Zeit möglich + +### ❌ Ansatz 3: Synthetisches Drop-Event +**Idee**: Original Drop abfangen, neues Event mit Files erzeugen +**Problem**: DataTransfer.files kann nicht gesetzt werden + +### ⚠️ Ansatz 4: Native Drag + Event-Simulation +**Idee**: Qt Native Drag, dann Web-App Event manuell auslösen +**Problem**: Erfordert Kenntnis der exakten Web-App Event-Struktur (Angular CDK) + +## 🎯 Praktikable Lösungen + +### Lösung A: **Zwei-Phasen Ansatz** (EMPFOHLEN für Testing) + +**Phase 1: File-Drop** +1. ALT-Drag startet +2. JavaScript erkennt convertible URL +3. Ruft Qt's `start_file_drag(url)` auf +4. `preventDefault()` verhindert Browser-Drag +5. Qt Native Drag läuft +6. File wird gedroppt ✅ + +**Phase 2: Popup manuell triggern** +7. Nach erfolgreichem Drop (via Qt Signal) +8. JavaScript simuliert den Event/API-Call der das Popup auslöst +9. Popup erscheint ✅ + +**Vorteile:** +- ✅ Funktioniert garantiert für File-Drop +- ✅ Kontrolle über beide Phasen +- ✅ Kann debugged werden + +**Nachteile:** +- ⚠️ Erfordert Reverse-Engineering des Popup-Triggers +- ⚠️ Könnte bei Web-App Updates brechen + +**Implementierung:** +```javascript +// Phase 1: Standard - File Drag +document.addEventListener('dragstart', function(e) { + if (!e.altKey) return; + + // Extract URL from DataTransfer + var url = e.dataTransfer.getData('text/plain'); + if (isConvertible(url)) { + e.preventDefault(); + window.bridge.start_file_drag(url); + } +}, true); + +// Phase 2: Popup Trigger (nach Drop-Erfolg) +// Qt ruft auf: window.trigger_checkout_popup(assetId) +window.trigger_checkout_popup = function(assetId) { + // Option 1: Klick auf Checkout-Button simulieren + // Option 2: API-Call direkt aufrufen + // Option 3: Angular-Event dispatchen + + // Beispiel: Suche nach Angular-Component und rufe Methode auf + var angularComponent = findAngularComponent('ay-asset-card'); + if (angularComponent) { + angularComponent.onCheckout(assetId); + } +}; +``` + +**TODO für diese Lösung:** +1. ✅ File-Drag funktioniert bereits +2. ⏸️ Herausfinden wie Popup ausgelöst wird: + - Browser DevTools öffnen + - Network-Tab beobachten (API-Call?) + - Elements-Tab nutzen (Angular Component?) + - Event-Listeners ansehen + +### Lösung B: **Gar nichts ändern beim Drag** (FALLBACK) + +**Ansatz:** +1. ALT-Drag läuft normal durch (URL-Text wird gedroppt) +2. Popup erscheint ✅ +3. Nach Popup-Bestätigung (Auschecken) +4. **DANN** konvertieren wir die URL und kopieren das File + +**Vorteile:** +- ✅ Web-App funktioniert normal +- ✅ Kein Popup-Problem +- ✅ Einfach zu implementieren + +**Nachteile:** +- ⚠️ Kein echter Drag & Drop (nur Copy) +- ⚠️ User muss zweimal handeln + +### Lösung C: **File-Drop via Qt Window Overlay** (EXPERIMENTELL) + +**Ansatz:** +1. Beim ALT-Drag: Start Normal Web-App Drag +2. Qt erstellt ein transparentes Overlay-Window über dem Browser +3. Overlay fängt das Drop-Event ab +4. Qt macht File-Drop +5. Qt leitet Drop-Koordinaten an Browser weiter +6. Browser bekommt synthetisches Event (ohne Files, nur Koordinaten) +7. Popup erscheint + +**Vorteile:** +- ✅ Beide Funktionen potentiell möglich +- ✅ Keine DataTransfer-Manipulation nötig + +**Nachteile:** +- ⚠️ Sehr komplex +- ⚠️ Plattform-spezifisch (Windows) +- ⚠️ Performance-Overhead + +## 🔬 Nächste Schritte - Reverse Engineering + +Um Lösung A zu implementieren, müssen wir herausfinden: + +### 1. Wie wird das Popup ausgelöst? + +**Debug-Schritte:** +```javascript +// In Browser Console ausführen während ALT-Drag+Drop + +// Methode 1: Event-Listener finden +getEventListeners(document) + +// Methode 2: Angular Component finden +var cards = document.querySelectorAll('ay-asset-card'); +var component = ng.getComponent(cards[0]); // Angular DevTools +console.log(component); + +// Methode 3: Network-Tab +// Schauen ob API-Call gemacht wird nach Drop +``` + +**Erwartete Möglichkeiten:** +- API-Call zu `/api/assets/{id}/checkout` +- Angular Event: `cdkDropListDropped` +- Component-Methode: `onAssetDropped()` oder ähnlich +- Modal-Service: `ModalService.show('checkout-dialog')` + +### 2. Asset-ID extrahieren + +Die Asset-ID wird benötigt für den Popup-Trigger: + +```javascript +// Asset-ID ist wahrscheinlich in: +// - Element-ID: "anPGZszKzgKaSz1SIx2HFgduy" +// - Image-URL: "./GlobalDAM JRI_files/anPGZszKzgKaSz1SIx2HFgduy" +// - Data-Attribut: data-asset-id + +function extractAssetId(element) { + // Aus Element-ID + if (element.id && element.id.match(/^a[A-Za-z0-9_-]+$/)) { + return element.id; + } + + // Aus Bild-URL + var img = element.querySelector('img'); + if (img && img.src) { + var match = img.src.match(/([A-Za-z0-9_-]{20,})/); + if (match) return match[1]; + } + + return null; +} +``` + +### 3. Popup programmatisch öffnen + +Sobald wir wissen wie, implementieren wir: + +```python +# Python (Qt) +class DragBridge(QObject): + @Slot(str) + def on_file_dropped_success(self, local_path: str): + """Called after successful file drop.""" + # Extrahiere Asset-ID aus Pfad oder URL-Mapping + asset_id = self.extract_asset_id(local_path) + + # Trigger Popup via JavaScript + js_code = f"window.trigger_checkout_popup('{asset_id}')" + self.web_view.page().runJavaScript(js_code) +``` + +## ⚡ Quick Win: Debugging aktivieren + +Fügen Sie zu **allen** JavaScript-Varianten umfangreiches Logging hinzu: + +```javascript +// In bridge_script.js + +// Log ALLE Events +['dragstart', 'drag', 'dragenter', 'dragover', 'drop', 'dragend'].forEach(function(eventName) { + document.addEventListener(eventName, function(e) { + console.log('[DEBUG]', eventName, { + target: e.target.tagName, + dataTransfer: { + types: e.dataTransfer.types, + effectAllowed: e.dataTransfer.effectAllowed, + dropEffect: e.dataTransfer.dropEffect + }, + altKey: e.altKey, + coordinates: {x: e.clientX, y: e.clientY} + }); + }, true); +}); + +// Log DataTransfer Zugriffe +Object.defineProperty(DataTransfer.prototype, 'types', { + get: function() { + var types = this._types || []; + console.log('[DEBUG] DataTransfer.types accessed:', types); + return types; + } +}); +``` + +## 📝 Empfehlung + +**Sofortige Maßnahmen:** + +1. ✅ **Lösung A Phase 1 ist bereits implementiert** (File-Drop funktioniert) + +2. 🔍 **Reverse-Engineering durchführen:** + - GlobalDAM JRI im Browser öffnen + - DevTools öffnen (F12) + - ALT-Drag+Drop durchführen + - Beobachten: + - Network-Tab → API-Calls? + - Console → Fehler/Logs? + - Angular DevTools → Component-Events? + +3. 🛠️ **Popup-Trigger implementieren:** + - Sobald bekannt WIE Popup ausgelöst wird + - JavaScript-Funktion `trigger_checkout_popup()` erstellen + - Von Qt aus nach erfolgreichem Drop aufrufen + +4. 🧪 **Testen:** + - ALT-Drag eines Assets + - File-Drop sollte funktionieren + - Popup sollte erscheinen + +**Fallback:** +Falls Reverse-Engineering zu komplex ist → **Lösung B** verwenden (Kein Drag, nur Copy nach Popup-Bestätigung) diff --git a/docs/SCRIPT_VARIANTS.md b/docs/SCRIPT_VARIANTS.md new file mode 100644 index 0000000..cd21eed --- /dev/null +++ b/docs/SCRIPT_VARIANTS.md @@ -0,0 +1,171 @@ +# WebDrop Bridge - Drag & Drop Script Varianten + +## Verfügbare JavaScript-Scripte + +### 1. `bridge_script.js` (CURRENT - Original) +**Status:** Versucht Drag zu ersetzen, verhindert Popup +**Verwendung:** Testing, zeigt dass File-Drop prinzipiell funktioniert +**Problem:** Web-App Popup erscheint nicht + +### 2. `bridge_script_debug.js` (DEBUG - **EMPFOHLEN ZUM START**) +**Status:** Umfangreiches Logging, keine Manipulation +**Verwendung:** Herausfinden wie Popup ausgelöst wird +**Funktionen:** +- Loggt ALLE Drag/Drop Events +- Überwacht DataTransfer.setData +- Überwacht Network-Requests (API-Calls) +- Erkennt Angular Components +- Erkennt Modal/Popup Öffnungen +- Bietet Helper-Funktionen in Console + +**So verwenden:** +```python +# In main_window.py Zeile ~433 ändern: +script_path = Path(__file__).parent / "bridge_script_debug.js" # <-- DEBUG aktivieren +``` + +**Im Browser:** +1. F12 → Console öffnen +2. ALT-Drag+Drop durchführen +3. Logs analysieren: + - `[MODAL OPENED]` → Popup-Trigger gefunden! + - `[FETCH]` oder `[XHR]` → API-Call gefunden! + - `[EVENT]` → Drag-Flow verstehen + +### 3. `bridge_script_v2.js` (EXTEND - Experimentell) +**Status:** Versuch DataTransfer zu erweitern +**Problem:** Browser-Sicherheit verhindert Files hinzufügen + +### 4. `bridge_script_hybrid.js` (HYBRID - Experimentell) +**Status:** Versuch parallele Drags +**Problem:** Nur ein Drag zur Zeit möglich + +### 5. `bridge_script_drop_intercept.js` (DROP - Experimentell) +**Status:** Drop-Event abfangen und manipulieren +**Problem:** DataTransfer kann nicht mit Files erstellt werden + +## 🎯 Empfohlener Workflow + +### Phase 1: Debugging (JETZT) + +1. **Debug-Script aktivieren:** + ```python + # main_window.py, _install_bridge_script() + script_path = Path(__file__).parent / "bridge_script_debug.js" + ``` + +2. **Anwendung starten und testen:** + ```powershell + python -m webdrop_bridge.main + ``` + +3. **In Browser (F12 Console):** + - ALT-Drag+Drop durchführen + - Logs kopieren und analysieren + - Nach `[MODAL OPENED]` oder API-Calls suchen + +4. **Herausfinden:** + - ✅ Wie wird Popup ausgelöst? (API-Call, Event, Component-Methode) + - ✅ Welche Asset-ID wird verwendet? + - ✅ Wo im DOM befindet sich die Popup-Logik? + +### Phase 2: Implementation (DANACH) + +Basierend auf Debug-Ergebnissen: + +**Fall A: Popup wird durch API-Call ausgelöst** +```javascript +// In bridge_script.js nach erfolgreichem Drop: +fetch('/api/assets/' + assetId + '/checkout', { + method: 'POST', + headers: {'Content-Type': 'application/json'} +}).then(response => { + console.log('Checkout popup triggered'); +}); +``` + +**Fall B: Popup wird durch Angular-Event ausgelöst** +```javascript +// Trigger Angular CDK Event +var dropList = document.querySelector('[cdkdroplist]'); +if (dropList && window.ng) { + var component = ng.getComponent(dropList); + component.onDrop({assetId: assetId}); +} +``` + +**Fall C: Popup wird durch Component-Methode ausgelöst** +```javascript +// Direkter Methoden-Aufruf +var assetCard = document.getElementById(assetId); +if (assetCard && window.ng) { + var component = ng.getComponent(assetCard); + component.showCheckoutDialog(); +} +``` + +### Phase 3: Testing (FINAL) + +1. Implementation in `bridge_script.js` integrieren +2. Beide Funktionen testen: + - ✅ File wird gedroppt (Z:\ Laufwerk) + - ✅ Popup erscheint (Auschecken-Dialog) + +## 🔧 Script-Wechsel in Code + +```python +# src/webdrop_bridge/ui/main_window.py +# Zeile ~433 in _install_bridge_script() + +# ORIGINAL (funktioniert, aber kein Popup): +script_path = Path(__file__).parent / "bridge_script.js" + +# DEBUG (für Analyse): +script_path = Path(__file__).parent / "bridge_script_debug.js" + +# Oder via Konfiguration: +script_name = self.config.bridge_script or "bridge_script.js" +script_path = Path(__file__).parent / script_name +``` + +## 📝 Debug-Checkliste + +Beim Testen mit Debug-Script, notieren Sie: + +- [ ] Wann erscheint das Popup? (Nach Drop, nach Verzögerung, sofort?) +- [ ] Gibt es API-Calls? (Welche URL, Parameter, Zeitpunkt?) +- [ ] Welche Angular-Events feuern? (CDK Events, Custom Events?) +- [ ] Wo wird Modal/Dialog erstellt? (DOM-Position, Klassen, Component) +- [ ] Welche Asset-Informationen werden benötigt? (ID, Name, URL?) +- [ ] Stack-Trace beim Modal-Öffnen? (console.trace Ausgabe) + +## 🚀 Quick Commands + +```powershell +# App mit Debug-Script starten +python -m webdrop_bridge.main + +# In Browser Console (F12): +webdrop_debug.getEventCounts() # Event-Statistiken +webdrop_debug.resetCounters() # Zähler zurücksetzen +webdrop_debug.getListeners(element) # Event-Listener auflisten +webdrop_debug.getComponent(element) # Angular Component anzeigen + +# Logs filtern: +# Console Filter: "[MODAL]" → Nur Popup-Logs +# Console Filter: "[EVENT]" → Nur Event-Logs +# Console Filter: "[FETCH]|[XHR]" → Nur Network-Logs +``` + +## ⚠️ Bekannte Limitationen + +1. **Angular DevTools benötigt:** Für `ng.getComponent()` Installation nötig +2. **Chrome/Edge:** Einige Features funktionieren nur in Chromium-Browsern +3. **CSP:** Bei strengen Content-Security-Policies können Logs blockiert werden +4. **Performance:** Debug-Script hat deutlichen Performance-Overhead + +## 📚 Weiterführende Dokumentation + +- [ANGULAR_CDK_ANALYSIS.md](ANGULAR_CDK_ANALYSIS.md) - Angular Framework Details +- [DRAG_DROP_PROBLEM_ANALYSIS.md](DRAG_DROP_PROBLEM_ANALYSIS.md) - Problem-Analyse + Lösungen +- [ARCHITECTURE.md](ARCHITECTURE.md) - Gesamtarchitektur diff --git a/src/webdrop_bridge/ui/bridge_script.js b/src/webdrop_bridge/ui/bridge_script.js index 622a0e4..ad8d4ce 100644 --- a/src/webdrop_bridge/ui/bridge_script.js +++ b/src/webdrop_bridge/ui/bridge_script.js @@ -141,26 +141,71 @@ console.log('[WebDrop Bridge] isZDrive:', isZDrive, 'isAzureUrl:', isAzureUrl); if (isZDrive || isAzureUrl) { - console.log('[WebDrop Bridge] >>> CONVERTING URL TO NATIVE DRAG'); + console.log('[WebDrop Bridge] >>> CONVERTING URL TO NATIVE DRAG (DELAYED)'); if (window.bridge && typeof window.bridge.debug_log === 'function') { - window.bridge.debug_log('Convertible URL detected - preventing browser drag'); + window.bridge.debug_log('Convertible URL detected - delaying Qt drag for Angular events'); } - // Prevent the browser's drag operation - e.preventDefault(); - e.stopPropagation(); + // DON'T prevent immediately - let Angular process dragstart for ~200ms + // This allows web app to register the drag and prepare popup - // Start native file drag via Qt - ensureChannel(function() { - if (window.bridge && typeof window.bridge.start_file_drag === 'function') { - console.log('[WebDrop Bridge] Calling start_file_drag:', path.substring(0, 60)); - window.bridge.start_file_drag(path); - currentDragData = null; - } else { - console.error('[WebDrop Bridge] bridge.start_file_drag not available!'); + // Store URL and element for later + window.__lastDraggedUrl = path; + var originalTarget = e.target; + + // After 200ms: Cancel browser drag and start Qt drag + setTimeout(function() { + console.log('[WebDrop Bridge] Starting Qt drag now, browser drag will be cancelled'); + + // Try to cancel browser drag by creating a fake drop on same element + try { + var dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + view: window + }); + originalTarget.dispatchEvent(dropEvent); + } catch(err) { + console.log('[WebDrop Bridge] Could not dispatch drop event:', err); } - }); + + // Hide Angular CDK overlays + var style = document.createElement('style'); + style.id = 'webdrop-bridge-hide-overlay'; + style.textContent = ` + .cdk-drag-animating, + .cdk-drag-preview, + .cdk-drag-placeholder, + [cdkdroplist].cdk-drop-list-dragging, + #root-collection-drop-area, + [id*="drop-area"] { + opacity: 0 !important; + pointer-events: none !important; + display: none !important; + } + `; + document.head.appendChild(style); + + // Start Qt drag + ensureChannel(function() { + if (window.bridge && typeof window.bridge.start_file_drag === 'function') { + console.log('[WebDrop Bridge] Calling start_file_drag:', path.substring(0, 60)); + window.bridge.start_file_drag(path); + currentDragData = null; + + // Cleanup after 5 seconds + setTimeout(function() { + var hideStyle = document.getElementById('webdrop-bridge-hide-overlay'); + if (hideStyle) hideStyle.remove(); + }, 5000); + } else { + console.error('[WebDrop Bridge] bridge.start_file_drag not available!'); + } + }); + }, 200); // 200ms delay + + // Let the browser drag start naturally (no preventDefault yet) return false; } else { @@ -203,6 +248,72 @@ }, 2000); // Wait 2 seconds after DOM ready } + // Global function to trigger checkout after successful file drop + window.trigger_checkout_for_asset = function(azure_url) { + console.log('[WebDrop Bridge] trigger_checkout_for_asset called for:', azure_url); + + // Extract asset ID from Azure URL + // Format: https://devagravitystg.file.core.windows.net/devagravitysync/{assetId}/{filename} + var match = azure_url.match(/\/devagravitysync\/([^\/]+)\//); + if (!match) { + console.error('[WebDrop Bridge] Could not extract asset ID from URL:', azure_url); + return; + } + + var assetId = match[1]; + console.log('[WebDrop Bridge] Extracted asset ID:', assetId); + console.log('[WebDrop Bridge] Calling checkout API directly...'); + + // Direct API call to checkout asset (skip popup, auto-checkout) + var apiUrl = 'https://devagravityprivate.azurewebsites.net/api/assets/' + assetId + '/checkout'; + + fetch(apiUrl, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + // Empty body or add checkout parameters if needed + }) + }).then(function(response) { + if (response.ok) { + console.log('[WebDrop Bridge] ✅ Asset checked out successfully:', assetId); + return response.json(); + } else { + console.warn('[WebDrop Bridge] ⚠️ Checkout API returned status:', response.status); + // Try alternative: Mark as checked out without confirmation + return tryAlternativeCheckout(assetId); + } + }).then(function(data) { + console.log('[WebDrop Bridge] Checkout response:', data); + }).catch(function(err) { + console.error('[WebDrop Bridge] ❌ Checkout API error:', err); + // Fallback: Try alternative checkout method + tryAlternativeCheckout(assetId); + }); + }; + + // Alternative checkout method if direct API fails + function tryAlternativeCheckout(assetId) { + console.log('[WebDrop Bridge] Trying alternative checkout for:', assetId); + + // Option 1: Try GET to fetch asset status, might trigger checkout tracking + var statusUrl = 'https://devagravityprivate.azurewebsites.net/api/assets/' + assetId; + + return fetch(statusUrl, { + method: 'GET', + credentials: 'include' + }).then(function(response) { + if (response.ok) { + console.log('[WebDrop Bridge] Asset status fetched, might have logged usage'); + } + return response.json(); + }).catch(function(err) { + console.error('[WebDrop Bridge] Alternative checkout also failed:', err); + }); + } + // Install after DOM is ready if (document.readyState === 'loading') { console.log('[WebDrop Bridge] Waiting for DOMContentLoaded...'); diff --git a/src/webdrop_bridge/ui/bridge_script_debug.js b/src/webdrop_bridge/ui/bridge_script_debug.js new file mode 100644 index 0000000..3fe16a1 --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_debug.js @@ -0,0 +1,333 @@ +// WebDrop Bridge - DEBUG Version +// Heavy logging to understand web app's drag&drop behavior +// +// Usage: +// 1. Load this script +// 2. Perform ALT-drag+drop in web app +// 3. Check console for detailed logs +// 4. Look for: API calls, events names, component methods + +(function() { + if (window.__webdrop_debug_injected) return; + window.__webdrop_debug_injected = true; + + console.log('%c[WebDrop DEBUG] Script loaded', 'background: #222; color: #bada55; font-size: 14px; font-weight: bold;'); + + // ============================================================================ + // PART 1: Event Monitoring - see ALL drag/drop related events + // ============================================================================ + + var allEvents = ['dragstart', 'drag', 'dragenter', 'dragover', 'dragleave', 'drop', 'dragend']; + var eventCounts = {}; + + allEvents.forEach(function(eventName) { + eventCounts[eventName] = 0; + + document.addEventListener(eventName, function(e) { + eventCounts[eventName]++; + + // Only log dragstart, drop, dragend fully (others are noisy) + if (eventName === 'dragstart' || eventName === 'drop' || eventName === 'dragend') { + console.group('%c[EVENT] ' + eventName.toUpperCase(), 'color: #FF6B6B; font-weight: bold;'); + console.log('Target:', e.target.tagName, e.target.className, e.target.id); + console.log('DataTransfer:', { + types: Array.from(e.dataTransfer.types), + effectAllowed: e.dataTransfer.effectAllowed, + dropEffect: e.dataTransfer.dropEffect, + files: e.dataTransfer.files.length + }); + console.log('Keys:', { + alt: e.altKey, + ctrl: e.ctrlKey, + shift: e.shiftKey + }); + console.log('Position:', {x: e.clientX, y: e.clientY}); + + // Try to read data (only works in drop/dragstart) + if (eventName === 'drop' || eventName === 'dragstart') { + try { + var plainText = e.dataTransfer.getData('text/plain'); + var uriList = e.dataTransfer.getData('text/uri-list'); + console.log('Data:', { + 'text/plain': plainText ? plainText.substring(0, 100) : null, + 'text/uri-list': uriList ? uriList.substring(0, 100) : null + }); + } catch(err) { + console.warn('Could not read DataTransfer data:', err.message); + } + } + + console.groupEnd(); + } + }, true); // Capture phase + }); + + // Log event summary every 5 seconds + setInterval(function() { + var hasEvents = Object.keys(eventCounts).some(function(k) { return eventCounts[k] > 0; }); + if (hasEvents) { + console.log('%c[EVENT SUMMARY]', 'color: #4ECDC4; font-weight: bold;', eventCounts); + } + }, 5000); + + // ============================================================================ + // PART 2: DataTransfer.setData Interception + // ============================================================================ + + try { + var originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + console.log('%c[DataTransfer.setData]', 'color: #FFE66D; font-weight: bold;', format, '=', + typeof data === 'string' ? data.substring(0, 100) : data); + return originalSetData.call(this, format, data); + }; + + console.log('[WebDrop DEBUG] DataTransfer.setData patched ✓'); + } catch(e) { + console.error('[WebDrop DEBUG] Failed to patch DataTransfer:', e); + } + + // ============================================================================ + // PART 3: Network Monitor - detect API calls (with request bodies) + // ============================================================================ + + var originalFetch = window.fetch; + window.fetch = function() { + var url = arguments[0]; + var options = arguments[1] || {}; + + console.log('%c🌐 Fetch called:', 'color: #95E1D3; font-weight: bold;', url); + + // Log headers if present + if (options.headers) { + console.log('%c[FETCH HEADERS]', 'color: #FFB6C1; font-weight: bold;'); + console.log(JSON.stringify(options.headers, null, 2)); + } + + // Log request body if present + if (options.body) { + try { + var bodyPreview = typeof options.body === 'string' ? options.body : JSON.stringify(options.body); + if (bodyPreview.length > 200) { + bodyPreview = bodyPreview.substring(0, 200) + '... (truncated)'; + } + console.log('%c[FETCH BODY]', 'color: #FFE66D; font-weight: bold;', bodyPreview); + } catch(e) { + console.log('%c[FETCH BODY]', 'color: #FFE66D; font-weight: bold;', '[Could not stringify]'); + } + } + + return originalFetch.apply(this, arguments); + }; + + var originalXHROpen = XMLHttpRequest.prototype.open; + var originalXHRSend = XMLHttpRequest.prototype.send; + var originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; + + XMLHttpRequest.prototype.open = function(method, url) { + this._webdrop_method = method; + this._webdrop_url = url; + this._webdrop_headers = {}; + console.log('%c[XHR]', 'color: #95E1D3; font-weight: bold;', method, url); + return originalXHROpen.apply(this, arguments); + }; + + XMLHttpRequest.prototype.setRequestHeader = function(header, value) { + this._webdrop_headers = this._webdrop_headers || {}; + this._webdrop_headers[header] = value; + return originalXHRSetRequestHeader.apply(this, arguments); + }; + + XMLHttpRequest.prototype.send = function(body) { + // Log headers if present + if (this._webdrop_headers && Object.keys(this._webdrop_headers).length > 0) { + if (this._webdrop_url && this._webdrop_url.includes('checkout')) { + console.log('%c[XHR HEADERS - CHECKOUT]', 'background: #FF6B6B; color: white; font-weight: bold; padding: 2px 6px;'); + console.log(JSON.stringify(this._webdrop_headers, null, 2)); + } else { + console.log('%c[XHR HEADERS]', 'color: #FFB6C1; font-weight: bold;'); + console.log(JSON.stringify(this._webdrop_headers, null, 2)); + } + } + + // Log request body if present + if (body) { + try { + var bodyPreview = typeof body === 'string' ? body : JSON.stringify(body); + if (bodyPreview.length > 200) { + bodyPreview = bodyPreview.substring(0, 200) + '... (truncated)'; + } + + // Highlight checkout API calls + if (this._webdrop_url && this._webdrop_url.includes('checkout')) { + console.log('%c[XHR BODY - CHECKOUT]', 'background: #FF6B6B; color: white; font-weight: bold; padding: 2px 6px;', + this._webdrop_method, this._webdrop_url); + console.log('%c[CHECKOUT PAYLOAD]', 'color: #00FF00; font-weight: bold;', bodyPreview); + } else { + console.log('%c[XHR BODY]', 'color: #FFE66D; font-weight: bold;', bodyPreview); + } + } catch(e) { + console.log('%c[XHR BODY]', 'color: #FFE66D; font-weight: bold;', '[Could not stringify]'); + } + } + + return originalXHRSend.apply(this, arguments); + }; + + console.log('[WebDrop DEBUG] Network interceptors installed ✓ (with request body logging)'); + + // ============================================================================ + // PART 4: Angular Event Detection (if Angular is used) + // ============================================================================ + + setTimeout(function() { + // Try to detect Angular + if (window.ng) { + console.log('%c[ANGULAR DETECTED]', 'color: #DD2C00; font-weight: bold;', 'Version:', window.ng.version?.full); + + // Try to find Angular components + var cards = document.querySelectorAll('ay-asset-card'); + console.log('[ANGULAR] Found', cards.length, 'asset cards'); + + if (cards.length > 0 && window.ng.getComponent) { + try { + var component = window.ng.getComponent(cards[0]); + console.log('%c[ANGULAR COMPONENT]', 'color: #DD2C00; font-weight: bold;', component); + console.log('Methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(component))); + } catch(e) { + console.warn('[ANGULAR] Could not get component:', e); + } + } + } else { + console.log('[WebDrop DEBUG] Angular not detected or DevTools required'); + } + + // Try to detect CDK + if (document.querySelector('[cdkdrag]')) { + console.log('%c[ANGULAR CDK] Detected', 'color: #FF6F00; font-weight: bold;'); + + // Monitor CDK specific events (if we can access them) + // Note: CDK events are often internal, we might need to monkey-patch + console.log('[CDK] Drag elements found:', document.querySelectorAll('[cdkdrag]').length); + console.log('[CDK] Drop lists found:', document.querySelectorAll('[cdkdroplist]').length); + } + }, 2000); + + // ============================================================================ + // PART 5: Modal/Dialog Detection + // ============================================================================ + + var modalObserver = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + mutation.addedNodes.forEach(function(node) { + if (node.nodeType === 1) { // Element node + // Check for common modal/dialog patterns + // Safely get className as string (handles SVG elements and undefined) + var className = typeof node.className === 'string' + ? node.className + : (node.className && node.className.baseVal) ? node.className.baseVal : ''; + + var isModal = className && ( + className.includes('modal') || + className.includes('dialog') || + className.includes('popup') || + className.includes('overlay') + ); + + if (isModal) { + console.log('%c[MODAL OPENED]', 'color: #FF6B6B; font-size: 16px; font-weight: bold;'); + console.log('Modal element:', node); + console.log('Classes:', className); + console.log('Content:', node.textContent.substring(0, 200)); + + // Log the stack trace to see what triggered it + console.trace('Modal opened from:'); + } + } + }); + }); + }); + + // Start observer when body is ready + function startModalObserver() { + if (document.body) { + modalObserver.observe(document.body, { + childList: true, + subtree: true + }); + console.log('[WebDrop DEBUG] Modal observer installed ✓'); + } else { + console.warn('[WebDrop DEBUG] document.body not ready, modal observer skipped'); + } + } + + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', startModalObserver); + } else { + startModalObserver(); + } + + // ============================================================================ + // PART 6: Helper Functions for Manual Testing + // ============================================================================ + + window.webdrop_debug = { + // Get all event listeners on an element + getListeners: function(element) { + if (typeof getEventListeners === 'function') { + return getEventListeners(element || document); + } else { + console.warn('getEventListeners not available. Open Chrome DevTools Console.'); + return null; + } + }, + + // Find Angular component for an element + getComponent: function(element) { + if (window.ng && window.ng.getComponent) { + return window.ng.getComponent(element); + } else { + console.warn('Angular DevTools not available. Install Angular DevTools extension.'); + return null; + } + }, + + // Simulate a drop at coordinates + simulateDrop: function(x, y, data) { + var dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + clientX: x, + clientY: y + }); + + // Can't set dataTransfer, but element can be used for testing + var target = document.elementFromPoint(x, y); + target.dispatchEvent(dropEvent); + console.log('Simulated drop at', x, y, 'on', target); + }, + + // Get event counts + getEventCounts: function() { + return eventCounts; + }, + + // Reset counters + resetCounters: function() { + Object.keys(eventCounts).forEach(function(k) { eventCounts[k] = 0; }); + console.log('Counters reset'); + } + }; + + console.log('%c[WebDrop DEBUG] Helper functions available as window.webdrop_debug', 'color: #95E1D3; font-weight: bold;'); + console.log('Try: webdrop_debug.getListeners(), webdrop_debug.getComponent(element)'); + + // ============================================================================ + // READY + // ============================================================================ + + console.log('%c[WebDrop DEBUG] Ready! Perform ALT-drag+drop and watch the logs.', + 'background: #222; color: #00FF00; font-size: 14px; font-weight: bold; padding: 4px;'); +})(); diff --git a/src/webdrop_bridge/ui/bridge_script_drop_intercept.js b/src/webdrop_bridge/ui/bridge_script_drop_intercept.js new file mode 100644 index 0000000..8b49201 --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_drop_intercept.js @@ -0,0 +1,211 @@ +// WebDrop Bridge - Drop Event Interception Strategy +// Strategy: +// 1. Let browser drag proceed normally (web app sets URL in DataTransfer) +// 2. Intercept DROP event in capture phase +// 3. Convert URL to local file path +// 4. Synthesize new DROP event with file data +// 5. Dispatch synthetic event (web app receives it and shows popup) +// 6. File gets dropped correctly + +(function() { + if (window.__webdrop_bridge_drop_intercept) return; + window.__webdrop_bridge_drop_intercept = true; + + console.log('[WebDrop Bridge DROP] Script loaded - Drop interception strategy'); + + var dragState = { + url: null, + localPath: null, + isConvertible: false, + altKeyPressed: false, + dragElement: null + }; + + // Patch DataTransfer.setData to capture URLs during drag + try { + var originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + if (format === 'text/plain' || format === 'text/uri-list') { + console.log('[WebDrop Bridge DROP] Captured data:', format, '=', data.substring(0, 80)); + + if (dragState.altKeyPressed) { + dragState.url = data; + + // Check if convertible + dragState.isConvertible = /^z:/i.test(data) || + /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(data); + + if (dragState.isConvertible) { + console.log('[WebDrop Bridge DROP] >>> CONVERTIBLE URL - will intercept drop'); + + // Request conversion NOW (synchronously if possible) + ensureChannel(function() { + if (window.bridge && typeof window.bridge.convert_url_sync === 'function') { + // Synchronous conversion + dragState.localPath = window.bridge.convert_url_sync(data); + console.log('[WebDrop Bridge DROP] Converted to:', dragState.localPath); + } else if (window.bridge && typeof window.bridge.convert_url_to_path === 'function') { + // Async conversion (fallback) + window.bridge.convert_url_to_path(data); + console.log('[WebDrop Bridge DROP] Async conversion requested'); + } + }); + } + } + } + + // Always call original + return originalSetData.call(this, format, data); + }; + + console.log('[WebDrop Bridge DROP] DataTransfer patched'); + } catch(e) { + console.error('[WebDrop Bridge DROP] Patch failed:', e); + } + + // Callback for async conversion + window.webdrop_set_local_path = function(path) { + dragState.localPath = path; + console.log('[WebDrop Bridge DROP] Local path received:', path); + }; + + function ensureChannel(cb) { + if (window.bridge) { cb(); return; } + + if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { + new QWebChannel(window.qt.webChannelTransport, function(channel) { + window.bridge = channel.objects.bridge; + console.log('[WebDrop Bridge DROP] QWebChannel connected'); + cb(); + }); + } + } + + function createSyntheticDropEvent(originalEvent, localPath) { + console.log('[WebDrop Bridge DROP] Creating synthetic drop event with file:', localPath); + + try { + // Create a new DataTransfer with file + // NOTE: This is complex because DataTransfer can't be created directly + // We'll create a new DragEvent and try to set files + + var newEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + composed: true, + view: window, + detail: originalEvent.detail, + screenX: originalEvent.screenX, + screenY: originalEvent.screenY, + clientX: originalEvent.clientX, + clientY: originalEvent.clientY, + ctrlKey: originalEvent.ctrlKey, + altKey: originalEvent.altKey, + shiftKey: originalEvent.shiftKey, + metaKey: originalEvent.metaKey, + button: originalEvent.button, + buttons: originalEvent.buttons, + relatedTarget: originalEvent.relatedTarget, + // We can't directly set dataTransfer, it's read-only + }); + + // This is a limitation: We can't create a DataTransfer with files from JavaScript + // The only way is to use a real file input or drag a real file + + console.warn('[WebDrop Bridge DROP] Cannot create DataTransfer with files from JS'); + console.log('[WebDrop Bridge DROP] Will use workaround: modify original DataTransfer'); + + return null; // Cannot create synthetic event with files + + } catch(error) { + console.error('[WebDrop Bridge DROP] Synthetic event creation failed:', error); + return null; + } + } + + function installHooks() { + console.log('[WebDrop Bridge DROP] Installing hooks'); + + // Monitor dragstart + document.addEventListener('dragstart', function(e) { + dragState.altKeyPressed = e.altKey; + dragState.url = null; + dragState.localPath = null; + dragState.isConvertible = false; + dragState.dragElement = e.target; + + console.log('[WebDrop Bridge DROP] dragstart, altKey:', e.altKey); + + // Let it proceed + }, true); + + // Intercept DROP event + document.addEventListener('drop', function(e) { + console.log('[WebDrop Bridge DROP] drop event, isConvertible:', dragState.isConvertible); + + if (!dragState.isConvertible || !dragState.localPath) { + console.log('[WebDrop Bridge DROP] Not convertible or no path, letting through'); + return; // Let normal drop proceed + } + + console.log('[WebDrop Bridge DROP] >>> INTERCEPTING DROP for conversion'); + + // This is the problem: We can't modify the DataTransfer at this point + // And we can't create a new one with files from JavaScript + + // WORKAROUND: Tell Qt to handle the drop natively + e.preventDefault(); // Prevent browser handling + e.stopPropagation(); + + // Get drop coordinates + var dropX = e.clientX; + var dropY = e.clientY; + + console.log('[WebDrop Bridge DROP] Drop at:', dropX, dropY); + + // Tell Qt to perform native file drop at these coordinates + ensureChannel(function() { + if (window.bridge && typeof window.bridge.handle_native_drop === 'function') { + window.bridge.handle_native_drop(dragState.localPath, dropX, dropY); + } + }); + + // THEN manually trigger the web app's drop handler + // This is tricky and app-specific + // For Angular CDK, we might need to trigger cdkDropListDropped + + console.warn('[WebDrop Bridge DROP] Web app popup might not appear - investigating...'); + + return false; + + }, true); // Capture phase + + // Clean up + document.addEventListener('dragend', function(e) { + console.log('[WebDrop Bridge DROP] dragend, cleaning up'); + dragState = { + url: null, + localPath: null, + isConvertible: false, + altKeyPressed: false, + dragElement: null + }; + }, false); + + console.log('[WebDrop Bridge DROP] Hooks installed'); + } + + // Initialize + ensureChannel(function() { + installHooks(); + }); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + if (!window.bridge) ensureChannel(installHooks); + }); + } else if (!window.bridge) { + ensureChannel(installHooks); + } +})(); diff --git a/src/webdrop_bridge/ui/bridge_script_hybrid.js b/src/webdrop_bridge/ui/bridge_script_hybrid.js new file mode 100644 index 0000000..9727648 --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_hybrid.js @@ -0,0 +1,109 @@ +// WebDrop Bridge - Hybrid Strategy v3 +// Allow web-app drag to proceed normally (for popups) +// BUT notify Qt to start a PARALLEL native file drag +// Windows supports concurrent drag sources - drop target chooses which to use + +(function() { + if (window.__webdrop_bridge_hybrid_injected) return; + window.__webdrop_bridge_hybrid_injected = true; + + console.log('[WebDrop Bridge HYBRID] Script loaded'); + + var dragState = { + url: null, + inProgress: false, + altKeyPressed: false + }; + + // Patch DataTransfer.setData to capture URLs + try { + var originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + if ((format === 'text/plain' || format === 'text/uri-list') && dragState.inProgress) { + dragState.url = data; + console.log('[WebDrop Bridge HYBRID] Captured URL:', data.substring(0, 80)); + + // Check if convertible + var isConvertible = /^z:/i.test(data) || + /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(data); + + if (isConvertible && dragState.altKeyPressed) { + console.log('[WebDrop Bridge HYBRID] >>> CONVERTIBLE - Triggering Qt native drag'); + + // Notify Qt to start PARALLEL native drag + ensureChannel(function() { + if (window.bridge && typeof window.bridge.start_parallel_drag === 'function') { + console.log('[WebDrop Bridge HYBRID] Calling start_parallel_drag'); + window.bridge.start_parallel_drag(data); + } else if (window.bridge && typeof window.bridge.start_file_drag === 'function') { + // Fallback to old method + console.log('[WebDrop Bridge HYBRID] Using start_file_drag (fallback)'); + window.bridge.start_file_drag(data); + } + }); + } + } + + // ALWAYS call original - web app functionality must work + return originalSetData.call(this, format, data); + }; + + console.log('[WebDrop Bridge HYBRID] DataTransfer patched'); + } catch(e) { + console.error('[WebDrop Bridge HYBRID] Patch failed:', e); + } + + function ensureChannel(cb) { + if (window.bridge) { cb(); return; } + + if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { + new QWebChannel(window.qt.webChannelTransport, function(channel) { + window.bridge = channel.objects.bridge; + console.log('[WebDrop Bridge HYBRID] QWebChannel connected'); + cb(); + }); + } else { + console.error('[WebDrop Bridge HYBRID] QWebChannel not available'); + } + } + + function installHook() { + console.log('[WebDrop Bridge HYBRID] Installing hooks'); + + // Monitor dragstart + document.addEventListener('dragstart', function(e) { + dragState.inProgress = true; + dragState.altKeyPressed = e.altKey; + dragState.url = null; + + console.log('[WebDrop Bridge HYBRID] dragstart, altKey:', e.altKey); + + // NO preventDefault() - let web app proceed normally! + + }, true); // Capture phase + + // Clean up on dragend + document.addEventListener('dragend', function(e) { + console.log('[WebDrop Bridge HYBRID] dragend'); + dragState.inProgress = false; + dragState.url = null; + dragState.altKeyPressed = false; + }, false); + + console.log('[WebDrop Bridge HYBRID] Installed'); + } + + // Initialize + ensureChannel(function() { + installHook(); + }); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + if (!window.bridge) ensureChannel(installHook); + }); + } else if (!window.bridge) { + ensureChannel(installHook); + } +})(); diff --git a/src/webdrop_bridge/ui/bridge_script_intercept.js b/src/webdrop_bridge/ui/bridge_script_intercept.js new file mode 100644 index 0000000..d249a83 --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_intercept.js @@ -0,0 +1,165 @@ +// WebDrop Bridge - Intercept Version +// Prevents browser drag for ALT+drag, hands off to Qt for file drag + +(function() { + if (window.__webdrop_intercept_injected) return; + window.__webdrop_intercept_injected = true; + + // Intercept mode enabled + var INTERCEPT_ENABLED = true; + + console.log('%c[WebDrop Intercept] Script loaded - INTERCEPT_ENABLED=' + INTERCEPT_ENABLED, 'background: #2196F3; color: white; font-weight: bold; padding: 4px 8px;'); + + var currentDragUrl = null; + var angularDragHandlers = []; + var originalAddEventListener = EventTarget.prototype.addEventListener; + var listenerPatchActive = true; + + // Capture Authorization token from XHR requests + window.capturedAuthToken = null; + var originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; + XMLHttpRequest.prototype.setRequestHeader = function(header, value) { + if (header === 'Authorization' && value.startsWith('Bearer ')) { + window.capturedAuthToken = value; + console.log('[Intercept] Captured auth token'); + } + return originalXHRSetRequestHeader.apply(this, arguments); + }; + + // ============================================================================ + // PART 1: Intercept Angular's dragstart listener registration + // ============================================================================ + + EventTarget.prototype.addEventListener = function(type, listener, options) { + if (listenerPatchActive && type === 'dragstart' && listener) { + // Store Angular's dragstart handler instead of registering it + console.log('[Intercept] Storing Angular dragstart listener for', this.tagName || this.constructor.name); + angularDragHandlers.push({ + target: this, + listener: listener, + options: options + }); + return; // Don't actually register it yet + } + // All other events: use original + return originalAddEventListener.call(this, type, listener, options); + }; + + // ============================================================================ + // PART 2: Intercept DataTransfer.setData to capture URL + // ============================================================================ + + var originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + if (format === 'text/plain' || format === 'text/uri-list') { + currentDragUrl = data; + console.log('%c[Intercept] Captured URL:', 'color: #4CAF50; font-weight: bold;', data.substring(0, 80)); + } + return originalSetData.call(this, format, data); + }; + + console.log('[Intercept] DataTransfer.setData patched ✓'); + + // ============================================================================ + // PART 3: Install OUR dragstart handler in capture phase + // ============================================================================ + + setTimeout(function() { + console.log('[Intercept] Installing dragstart handler, have', angularDragHandlers.length, 'Angular handlers'); + + // Stop intercepting addEventListener + listenerPatchActive = false; + + // Register OUR handler in capture phase + originalAddEventListener.call(document, 'dragstart', function(e) { + currentDragUrl = null; // Reset + + console.log('%c[Intercept] dragstart', 'background: #FF9800; color: white; padding: 2px 6px;', 'ALT:', e.altKey); + + // Call Angular's handlers first to let them set the data + var handled = 0; + for (var i = 0; i < angularDragHandlers.length; i++) { + var h = angularDragHandlers[i]; + if (h.target === document || h.target === e.target || + (h.target.contains && h.target.contains(e.target))) {https://devagravitystg.file.core.windows.net/devagravitysync/anPGZszKzgKaSz1SIx2HFgduy/weiss_ORIGINAL.jpg + try { + h.listener.call(e.target, e); + handled++; + } catch(err) { + console.error('[Intercept] Error calling Angular handler:', err); + } + } + } + + console.log('[Intercept] Called', handled, 'Angular handlers, URL:', currentDragUrl ? currentDragUrl.substring(0, 60) : 'none'); + + // NOW check if we should intercept + if (e.altKey && currentDragUrl) { + var isAzure = /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(currentDragUrl); + var isZDrive = /^z:/i.test(currentDragUrl); + + if (isAzure || isZDrive) { + console.log('%c[Intercept] PREVENTING browser drag, using Qt', + 'background: #F44336; color: white; font-weight: bold; padding: 4px 8px;'); + + e.preventDefault(); + e.stopPropagation(); + + ensureChannel(function() { + if (window.bridge && typeof window.bridge.start_file_drag === 'function') { + console.log('%c[Intercept] → Qt: start_file_drag', 'color: #9C27B0; font-weight: bold;'); + window.bridge.start_file_drag(currentDragUrl); + } else { + console.error('[Intercept] bridge.start_file_drag not available!'); + } + }); + + currentDragUrl = null; + return false; + } + } + + console.log('[Intercept] Normal drag, allowing browser'); + }, true); // Capture phase + + console.log('[Intercept] dragstart handler installed ✓'); + }, 1500); // Wait for Angular to register its listeners + + // ============================================================================ + // PART 3: QWebChannel connection + // ============================================================================ + + function ensureChannel(callback) { + if (window.bridge) { + callback(); + return; + } + + if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { + new QWebChannel(window.qt.webChannelTransport, function(channel) { + window.bridge = channel.objects.bridge; + console.log('[WebDrop Intercept] QWebChannel connected ✓'); + callback(); + }); + } else { + console.error('[WebDrop Intercept] QWebChannel not available!'); + } + } + + // Initialize channel on load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + ensureChannel(function() { + console.log('[WebDrop Intercept] Bridge ready ✓'); + }); + }); + } else { + ensureChannel(function() { + console.log('[WebDrop Intercept] Bridge ready ✓'); + }); + } + + console.log('%c[WebDrop Intercept] Ready! ALT-drag will use Qt file drag.', + 'background: #4CAF50; color: white; font-weight: bold; padding: 4px 8px;'); +})(); diff --git a/src/webdrop_bridge/ui/bridge_script_v2.js b/src/webdrop_bridge/ui/bridge_script_v2.js new file mode 100644 index 0000000..33ceccd --- /dev/null +++ b/src/webdrop_bridge/ui/bridge_script_v2.js @@ -0,0 +1,214 @@ +// WebDrop Bridge - Enhanced Script v2 +// Strategy: EXTEND DataTransfer instead of REPLACING the drag +// This allows both file-drop AND web-app functionality (popups) + +(function() { + if (window.__webdrop_bridge_v2_injected) return; + window.__webdrop_bridge_v2_injected = true; + + console.log('[WebDrop Bridge v2] Script loaded - EXTEND strategy'); + + var currentDragUrl = null; + var currentLocalPath = null; + var dragInProgress = false; + + // Patch DataTransfer.setData to capture URLs set by web app + var originalSetData = null; + + try { + if (DataTransfer.prototype.setData) { + originalSetData = DataTransfer.prototype.setData; + + DataTransfer.prototype.setData = function(format, data) { + // Capture text/plain or text/uri-list for our conversion + if ((format === 'text/plain' || format === 'text/uri-list') && dragInProgress) { + currentDragUrl = data; + console.log('[WebDrop Bridge v2] Captured drag URL:', data.substring(0, 80)); + + // Check if this is convertible (Z:\ or Azure) + var isZDrive = /^z:/i.test(data); + var isAzureUrl = /^https?:\/\/.+\.file\.core\.windows\.net\//i.test(data); + + if (isZDrive || isAzureUrl) { + console.log('[WebDrop Bridge v2] >>> CONVERTIBLE URL DETECTED - Will add file data'); + + // Request conversion from Qt backend + if (window.bridge && typeof window.bridge.convert_url_to_path === 'function') { + console.log('[WebDrop Bridge v2] Requesting path conversion...'); + window.bridge.convert_url_to_path(data); + // Note: Conversion happens async, local path will be set via callback + } + } + } + + // ALWAYS call original - don't break web app + return originalSetData.call(this, format, data); + }; + + console.log('[WebDrop Bridge v2] DataTransfer.setData patched'); + } + } catch(e) { + console.error('[WebDrop Bridge v2] Failed to patch DataTransfer:', e); + } + + // Enhanced DataTransfer with file support + // This is called AFTER web app sets its data + function enhanceDataTransfer(e) { + if (!currentLocalPath) { + console.log('[WebDrop Bridge v2] No local path available, cannot enhance'); + return; + } + + console.log('[WebDrop Bridge v2] Enhancing DataTransfer with file:', currentLocalPath); + + try { + var dt = e.dataTransfer; + + // Strategy 1: Add Windows-specific file drop formats + // These are recognized by Windows Explorer and other drop targets + if (originalSetData) { + // Add FileNameW (Unicode file path for Windows) + var fileNameW = currentLocalPath; + + // Try to add custom Windows file drop formats + try { + originalSetData.call(dt, 'application/x-qt-windows-mime;value="FileNameW"', fileNameW); + console.log('[WebDrop Bridge v2] Added FileNameW format'); + } catch (e1) { + console.warn('[WebDrop Bridge v2] FileNameW format failed:', e1); + } + + // Add FileName (ANSI) + try { + originalSetData.call(dt, 'application/x-qt-windows-mime;value="FileName"', fileNameW); + console.log('[WebDrop Bridge v2] Added FileName format'); + } catch (e2) { + console.warn('[WebDrop Bridge v2] FileName format failed:', e2); + } + + // Add FileDrop format + try { + originalSetData.call(dt, 'application/x-qt-windows-mime;value="FileDrop"', fileNameW); + console.log('[WebDrop Bridge v2] Added FileDrop format'); + } catch (e3) { + console.warn('[WebDrop Bridge v2] FileDrop format failed:', e3); + } + } + + // Set effect to allow copy/link + if (dt.effectAllowed === 'uninitialized' || dt.effectAllowed === 'none') { + dt.effectAllowed = 'copyLink'; + console.log('[WebDrop Bridge v2] Set effectAllowed to copyLink'); + } + + } catch (error) { + console.error('[WebDrop Bridge v2] Error enhancing DataTransfer:', error); + } + } + + function ensureChannel(cb) { + if (window.bridge) { cb(); return; } + + function init() { + if (window.QWebChannel && window.qt && window.qt.webChannelTransport) { + new QWebChannel(window.qt.webChannelTransport, function(channel) { + window.bridge = channel.objects.bridge; + console.log('[WebDrop Bridge v2] QWebChannel connected'); + + // Expose callback for Qt to set the converted path + window.setLocalPath = function(path) { + currentLocalPath = path; + console.log('[WebDrop Bridge v2] Local path set from Qt:', path); + }; + + cb(); + }); + } else { + console.error('[WebDrop Bridge v2] QWebChannel not available!'); + } + } + + if (window.QWebChannel) { + init(); + } else { + console.error('[WebDrop Bridge v2] QWebChannel not found!'); + } + } + + function installHook() { + console.log('[WebDrop Bridge v2] Installing drag interceptor (EXTEND mode)'); + + // Use CAPTURE PHASE to intercept early + document.addEventListener('dragstart', function(e) { + try { + dragInProgress = true; + currentDragUrl = null; + currentLocalPath = null; + + console.log('[WebDrop Bridge v2] dragstart on:', e.target.tagName, 'altKey:', e.altKey); + + // Only process ALT-drags (web app's URL drag mode) + if (!e.altKey) { + console.log('[WebDrop Bridge v2] No ALT key, ignoring'); + dragInProgress = false; + return; + } + + console.log('[WebDrop Bridge v2] ALT-drag detected, will monitor for convertible URL'); + + // NOTE: We DON'T call preventDefault() here! + // This allows web app's drag to proceed normally + + // Web app will call setData() which we've patched + // After a short delay, we check if we got a convertible URL + setTimeout(function() { + if (currentDragUrl) { + console.log('[WebDrop Bridge v2] Drag URL set:', currentDragUrl.substring(0, 60)); + // enhanceDataTransfer will be called when we have the path + // For now, we just wait - the drag is already in progress + } + }, 10); + + } catch (error) { + console.error('[WebDrop Bridge v2] Error in dragstart:', error); + dragInProgress = false; + } + }, true); // CAPTURE phase + + // Clean up on dragend + document.addEventListener('dragend', function(e) { + console.log('[WebDrop Bridge v2] dragend, cleaning up state'); + dragInProgress = false; + currentDragUrl = null; + currentLocalPath = null; + }, false); + + // Alternative strategy: Use 'drag' event to continuously update DataTransfer + // This fires many times during the drag + document.addEventListener('drag', function(e) { + if (!dragInProgress || !currentLocalPath) return; + + // Try to enhance on every drag event + enhanceDataTransfer(e); + }, false); + + console.log('[WebDrop Bridge v2] Hooks installed'); + } + + // Initialize + ensureChannel(function() { + console.log('[WebDrop Bridge v2] Channel ready, installing hooks'); + installHook(); + }); + + // Also install on DOM ready as fallback + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + if (!window.bridge) { + ensureChannel(installHook); + } + }); + } else if (!window.bridge) { + ensureChannel(installHook); + } +})(); diff --git a/src/webdrop_bridge/ui/main_window.py b/src/webdrop_bridge/ui/main_window.py index 252e27b..34bb438 100644 --- a/src/webdrop_bridge/ui/main_window.py +++ b/src/webdrop_bridge/ui/main_window.py @@ -1,7 +1,9 @@ """Main application window with web engine integration.""" import asyncio +import json import logging +import re from datetime import datetime from pathlib import Path from typing import Optional @@ -426,7 +428,8 @@ class MainWindow(QMainWindow): logger.warning("Failed to load qwebchannel.js from resources") # Load bridge script from file - script_path = Path(__file__).parent / "bridge_script.js" + # Using intercept script - prevents browser drag, hands off to Qt + script_path = Path(__file__).parent / "bridge_script_intercept.js" try: with open(script_path, 'r', encoding='utf-8') as f: bridge_code = f.read() @@ -492,7 +495,119 @@ class MainWindow(QMainWindow): local_path: Local file path that is being dragged """ logger.info(f"Drag started: {source} -> {local_path}") - # Can be extended with status bar updates or user feedback + + # Ask user if they want to check out the asset + if source.startswith('http'): + self._prompt_checkout(source, local_path) + + def _prompt_checkout(self, azure_url: str, local_path: str) -> None: + """Prompt user to check out the asset. + + Args: + azure_url: Azure Blob Storage URL + local_path: Local file path + """ + from PySide6.QtWidgets import QMessageBox + + # Extract filename for display + filename = Path(local_path).name + + # Show confirmation dialog + reply = QMessageBox.question( + self, + "Checkout Asset", + f"Do you want to check out this asset?\n\n{filename}", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.Yes + ) + + if reply == QMessageBox.StandardButton.Yes: + logger.info(f"User confirmed checkout for {filename}") + self._trigger_checkout_api(azure_url) + else: + logger.info(f"User declined checkout for {filename}") + + def _trigger_checkout_api(self, azure_url: str) -> None: + """Trigger checkout via API call using JavaScript. + + Calls the checkout API from JavaScript so HttpOnly cookies are automatically included. + Example URL: https://devagravitystg.file.core.windows.net/devagravitysync/anPGZszKzgKaSz1SIx2HFgduy/filename + Asset ID: anPGZszKzgKaSz1SIx2HFgduy + + Args: + azure_url: Azure Blob Storage URL containing asset ID + """ + try: + # Extract asset ID from URL (middle segment between domain and filename) + # Format: https://domain/container/ASSET_ID/filename + match = re.search(r'/([^/]+)/[^/]+$', azure_url) + if not match: + logger.warning(f"Could not extract asset ID from URL: {azure_url}") + return + + asset_id = match.group(1) + logger.info(f"Extracted asset ID: {asset_id}") + + # Call API from JavaScript with Authorization header + js_code = f""" + (async function() {{ + try {{ + // Get captured auth token (from intercepted XHR) + const authToken = window.capturedAuthToken; + + if (!authToken) {{ + console.error('No authorization token available'); + return {{ success: false, error: 'No auth token' }}; + }} + + const headers = {{ + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Accept-Language': 'de', + 'Authorization': authToken + }}; + + const response = await fetch( + 'https://devagravityprivate.azurewebsites.net/api/assets/checkout/bulk?checkout=true', + {{ + method: 'PUT', + headers: headers, + body: JSON.stringify({{asset_ids: ['{asset_id}']}}) + }} + ); + + if (response.ok) {{ + console.log('✅ Checkout API successful for asset {asset_id}'); + return {{ success: true, status: response.status }}; + }} else {{ + const text = await response.text(); + console.warn('Checkout API returned status ' + response.status + ': ' + text.substring(0, 200)); + return {{ success: false, status: response.status, error: text }}; + }} + }} catch (error) {{ + console.error('Checkout API call failed:', error); + return {{ success: false, error: error.toString() }}; + }} + }})(); + """ + + def on_result(result): + """Callback when JavaScript completes.""" + if result and isinstance(result, dict): + if result.get('success'): + logger.info(f"✅ Checkout successful for asset {asset_id}") + else: + status = result.get('status', 'unknown') + error = result.get('error', 'unknown error') + logger.warning(f"Checkout API returned status {status}: {error}") + else: + logger.debug(f"Checkout API call completed (result: {result})") + + # Execute JavaScript (async, non-blocking) + self.web_view.page().runJavaScript(js_code, on_result) + + except Exception as e: + logger.exception(f"Error triggering checkout API: {e}") def _on_drag_failed(self, source: str, error: str) -> None: """Handle drag operation failure. @@ -605,33 +720,13 @@ class MainWindow(QMainWindow): def dragEnterEvent(self, event): """Handle drag entering the main window (from WebView or external). - When a drag from the WebView enters the MainWindow area, we can read - the drag data and potentially convert Azure URLs to file drags. + Note: With intercept script, ALT-drags are prevented in JavaScript + and handled via bridge.start_file_drag(). This just handles any + remaining drag events. Args: event: QDragEnterEvent """ - from PySide6.QtCore import QMimeData - - mime_data = event.mimeData() - - # Check if we have text data (URL from web app) - if mime_data.hasText(): - url_text = mime_data.text() - logger.debug(f"Drag entered main window with text: {url_text[:100]}") - - # Store for potential conversion - self._current_drag_url = url_text - - # Check if it's convertible - is_azure = url_text.startswith('https://') and 'file.core.windows.net' in url_text - is_z_drive = url_text.lower().startswith('z:') - - if is_azure or is_z_drive: - logger.info(f"Convertible URL detected in drag: {url_text[:60]}") - event.acceptProposedAction() - return - event.ignore() def dragMoveEvent(self, event): @@ -640,10 +735,7 @@ class MainWindow(QMainWindow): Args: event: QDragMoveEvent """ - if self._current_drag_url: - event.acceptProposedAction() - else: - event.ignore() + event.ignore() def dragLeaveEvent(self, event): """Handle drag leaving the main window. @@ -651,33 +743,15 @@ class MainWindow(QMainWindow): Args: event: QDragLeaveEvent """ - logger.debug("Drag left main window") - # Reset tracking - self._current_drag_url = None + event.ignore() def dropEvent(self, event): """Handle drop on the main window. - This captures drops on the MainWindow area (outside WebView). - If the user drops an Azure URL here, we convert it to a file operation. - Args: event: QDropEvent """ - if self._current_drag_url: - logger.info(f"Drop on main window with URL: {self._current_drag_url[:60]}") - - # Handle via drag interceptor (converts Azure URL to local path) - success = self.drag_interceptor.handle_drag(self._current_drag_url) - - if success: - event.acceptProposedAction() - else: - event.ignore() - - self._current_drag_url = None - else: - event.ignore() + event.ignore() def _on_js_console_message(self, level, message, line_number, source_id): """Redirect JavaScript console messages to Python logger. @@ -863,10 +937,33 @@ class MainWindow(QMainWindow): def closeEvent(self, event) -> None: """Handle window close event. + Properly cleanup WebEnginePage before closing to avoid + "Release of profile requested but WebEnginePage still not deleted" warning. + This ensures session data (cookies, login state) is properly saved. + Args: event: Close event """ - # Can be extended with save operations or cleanup + logger.debug("Closing application - cleaning up web engine resources") + + # Properly delete WebEnginePage before the profile is released + # This ensures cookies and session data are saved correctly + if hasattr(self, 'web_view') and self.web_view: + page = self.web_view.page() + if page: + # Disconnect signals to prevent callbacks during shutdown + try: + page.loadFinished.disconnect() + except RuntimeError: + pass # Already disconnected or never connected + + # Delete the page explicitly + page.deleteLater() + logger.debug("WebEnginePage scheduled for deletion") + + # Clear the page from the view + self.web_view.setPage(None) + event.accept() def check_for_updates_startup(self) -> None: diff --git a/src/webdrop_bridge/ui/restricted_web_view.py b/src/webdrop_bridge/ui/restricted_web_view.py index 489b473..7f20ad2 100644 --- a/src/webdrop_bridge/ui/restricted_web_view.py +++ b/src/webdrop_bridge/ui/restricted_web_view.py @@ -155,7 +155,8 @@ class RestrictedWebEngineView(QWebEngineView): # Create persistent profile with custom storage location # Using "WebDropBridge" as the profile name - profile = QWebEngineProfile("WebDropBridge", self) + # Note: No parent specified so we control the lifecycle + profile = QWebEngineProfile("WebDropBridge") profile.setPersistentStoragePath(str(profile_path)) # Configure persistent cookies (critical for authentication)