diff --git a/src/MediaManager.html b/src/MediaManager.html index 087644b..2fc4996 100644 --- a/src/MediaManager.html +++ b/src/MediaManager.html @@ -439,14 +439,44 @@ width: 6px; } - .log-content::-webkit-scrollbar-track { - background: transparent; - } - .log-content::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } + + /* Transfer Session UI */ + .transfer-session { + margin-top: 12px; + padding: 16px; + background: #f8fafc; + border: 1px solid var(--border); + border-radius: 8px; + } + + .progress-track { + width: 100%; + height: 8px; + background: #e2e8f0; + border-radius: 4px; + overflow: hidden; + margin-top: 8px; + } + + .progress-fill { + height: 100%; + background: var(--primary); + width: 0%; + transition: width 0.3s ease; + } + + .transfer-footer { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid #f1f5f9; + } @@ -530,21 +560,36 @@ - + +
@@ -820,42 +865,81 @@ this.saveBtn.innerText = enable ? "Save Changes" : "No Changes"; }; - UI.prototype.showPhotoSession = function (url) { - var uiEl = document.getElementById('photos-session-ui'); - var link = document.getElementById('photos-session-link'); - var status = document.getElementById('photos-session-status'); + UI.prototype.showTransferSession = function (mode, serviceName) { + var el = document.getElementById('transfer-session-ui'); + var desc = document.getElementById('transfer-desc'); + var statusText = document.getElementById('transfer-status-text'); + var bar = document.getElementById('transfer-progress-bar'); + var btnReopen = document.getElementById('btn-transfer-reopen'); + var btnCancel = document.getElementById('btn-transfer-cancel'); - uiEl.style.display = 'block'; - link.href = url; - // We also open it automatically in a popup - const width = 1200; - const height = 800; - const left = (screen.width - width) / 2; - const top = (screen.height - height) / 2; + el.style.display = 'block'; + bar.style.width = '0%'; + statusText.innerText = 'Initializing...'; + document.getElementById('transfer-count').innerText = ''; - // Attempt popup - const popup = window.open(url, 'googlePhotos', `width=${width},height=${height},top=${top},left=${left}`); + // Reset Buttons + btnCancel.disabled = false; + btnReopen.disabled = true; // Default - if (popup) { - link.innerText = "Re-open Popup ↗"; - link.onclick = function (e) { - e.preventDefault(); - window.open(url, 'googlePhotos', `width=${width},height=${height},top=${top},left=${left}`); + if (mode === 'waiting') { + desc.innerText = "Select items from " + (serviceName || "the service") + " in the popup window. Click 'Done' when finished."; + statusText.innerText = "Waiting for selection..."; + } else { + desc.innerText = "Importing media from " + (serviceName || "source") + "..."; } + }; + + UI.prototype.updateTransferProgress = function (current, total, statusMsg) { + var bar = document.getElementById('transfer-progress-bar'); + var statusText = document.getElementById('transfer-status-text'); + var countText = document.getElementById('transfer-count'); + + // Disable Reopen once transfer starts + const btnReopen = document.getElementById('btn-transfer-reopen'); + if (btnReopen) btnReopen.disabled = true; + + if (total > 0) { + var pct = Math.round((current / total) * 100); + bar.style.width = pct + '%'; + countText.innerText = current + ' / ' + total; } else { - link.innerText = "Open Google Photos ↗"; - link.onclick = null; // Default href behavior + // Indeterminate + bar.style.width = '100%'; + countText.innerText = ''; } - status.innerText = "Waiting for selection in popup..."; + if (statusMsg) statusText.innerText = statusMsg; + + // If done, disable cancel + if (current === total && total > 0) { + const btnCancel = document.getElementById('btn-transfer-cancel'); + if (btnCancel) btnCancel.disabled = true; + } }; - UI.prototype.closePhotoSession = function () { - document.getElementById('photos-session-ui').style.display = 'none'; + UI.prototype.hideTransferSession = function () { + document.getElementById('transfer-session-ui').style.display = 'none'; }; - UI.prototype.updatePhotoStatus = function (msg) { - document.getElementById('photos-session-status').innerText = msg; + UI.prototype.setupReopenButton = function (url) { + var btn = document.getElementById('btn-transfer-reopen'); + if (!url) { + btn.disabled = true; + return; + } + + const width = 1200; + const height = 800; + const left = (screen.width - width) / 2; + const top = (screen.height - height) / 2; + const params = `width=${width},height=${height},top=${top},left=${left}`; + + btn.onclick = function (e) { + e.preventDefault(); + window.open(url, 'googlePhotos', params); + }; + btn.disabled = false; }; UI.prototype.render = function (items) { @@ -1268,11 +1352,6 @@ // Start Polling this.startLogPolling(jobId); - // Expand Log Card - if (!ui.logCard.classList.contains('expanded')) { - ui.toggleLogExpand(); - } - // Filter out deleted items const activeItems = state.items.filter(i => !i._deleted); @@ -1336,21 +1415,42 @@ handleFiles(fileList) { if (!fileList || fileList.length === 0) return; + this.setPickerState(true); - Array.from(fileList).forEach(file => { + this.initTransferState(); + ui.showTransferSession('transferring', 'Your Computer'); + + const total = fileList.length; + let done = 0; + + Array.from(fileList).forEach((file, i) => { + if (this.transferState.shouldCancel) return; + const reader = new FileReader(); reader.onload = (e) => { + if (this.transferState.shouldCancel) return; + const data = e.target.result.split(',')[1]; // Base64 + ui.updateTransferProgress(done, total, `Uploading ${file.name}...`); + google.script.run .withSuccessHandler(() => { - this.loadMedia(); - this.setPickerState(false); + if (this.transferState.shouldCancel) return; + done++; + ui.updateTransferProgress(done, total); + + if (done === total) { + ui.updateTransferProgress(total, total, "Done!"); + this.finishTransfer(); + } }) .withFailureHandler(err => { console.error(err); - alert("Upload failed: " + err.message); - this.setPickerState(false); + // Allow continuing even if one fails + done++; + ui.updateTransferProgress(done, total, "Error on " + file.name); + if (done === total) this.finishTransfer(); }) .saveFileToDrive(state.sku, file.name, file.type, data); }; @@ -1375,6 +1475,8 @@ openPicker() { if (!pickerApiLoaded) return alert("API Loading..."); this.setPickerState(true); + // We don't show the full session UI for Drive picker config loading, + // just disable buttons. The picker is its own UI. google.script.run .withSuccessHandler(c => createPicker(c)) .withFailureHandler(e => { @@ -1385,53 +1487,119 @@ }, importFromPicker(fileId, mime, name, url) { + // Drive Picker Result + this.initTransferState(); + ui.showTransferSession('transferring', 'Google Drive'); + + // Single Item, so 0/1 -> 1/1 + ui.updateTransferProgress(0, 1, "Importing " + name + "..."); + google.script.run .withSuccessHandler(() => { - this.loadMedia(); - this.setPickerState(false); + if (this.transferState.shouldCancel) return; + ui.updateTransferProgress(1, 1, "Done!"); + this.finishTransfer(); }) .withFailureHandler(e => { alert("Import failed: " + e.message); - this.setPickerState(false); + this.finishTransfer(); // Cleanup }) .importFromPicker(state.sku, fileId, mime, name, url); }, + // --- Transfer State & Logic --- + transferState: { + isTransferring: false, + shouldCancel: false, + pollingId: null + }, + + initTransferState() { + this.transferState = { isTransferring: true, shouldCancel: false, pollingId: null }; + }, + + cancelTransfer() { + this.transferState.shouldCancel = true; + this.transferState.isTransferring = false; + + if (this.transferState.pollingId) { + clearTimeout(this.transferState.pollingId); // Note: using setTimeout in pollPhotoSession + this.transferState.pollingId = null; + } + + ui.hideTransferSession(); + this.setPickerState(false); + ui.logStatus('info', 'Transfer cancelled by user.', 'info'); + }, + + finishTransfer() { + if (this.transferState.shouldCancel) return; // Already cleaned up + this.transferState.isTransferring = false; + + this.loadMedia(); + this.setPickerState(false); + setTimeout(() => ui.hideTransferSession(), 2000); + }, + + startPhotoSession() { this.setPickerState(true); - ui.updatePhotoStatus("Starting session..."); + this.initTransferState(); + ui.showTransferSession('waiting', 'Google Photos'); + google.script.run .withSuccessHandler(session => { - ui.showPhotoSession(session.pickerUri); + if (this.transferState.shouldCancel) return; // Cancelled during init + + // Setup popup logic + const width = 1200, height = 800; + const left = (screen.width - width) / 2; + const top = (screen.height - height) / 2; + const params = `width=${width},height=${height},top=${top},left=${left}`; + + // Auto open + window.open(session.pickerUri, 'googlePhotos', params); + + // Setup reopen link + ui.setupReopenButton(session.pickerUri); + this.pollPhotoSession(session.id); }) .withFailureHandler(e => { - ui.updatePhotoStatus("Failed: " + e.message); + alert("Failed to start session: " + e.message); this.setPickerState(false); + ui.hideTransferSession(); }) .createPhotoSession(); }, pollPhotoSession(sessionId) { - let processing = false; + if (this.transferState.shouldCancel) return; + const check = () => { - if (processing) return; + if (this.transferState.shouldCancel) return; + google.script.run .withSuccessHandler(res => { + if (this.transferState.shouldCancel) return; + if (res.status === 'complete') { - processing = true; - ui.updatePhotoStatus("Importing photos..."); + // Transition to transferring + ui.updateTransferProgress(0, 0, "Importing photos..."); this.processPhotoItems(res.mediaItems); } else if (res.status === 'error') { - ui.updatePhotoStatus("Error: " + res.message); - this.setPickerState(false); + alert("Photo Picker Error: " + res.message); + this.cancelTransfer(); } else { - setTimeout(check, 2000); + // Keep polling + this.transferState.pollingId = setTimeout(check, 2000); } }) .withFailureHandler(e => { - ui.updatePhotoStatus("Error polling: " + e.message); - this.setPickerState(false); + console.error(e); + // Retry? Or fail. Let's fail after error. + alert("Polling Error: " + e.message); + this.cancelTransfer(); }) .checkPhotoSession(sessionId); }; @@ -1440,41 +1608,37 @@ processPhotoItems(items) { let done = 0; + const total = items.length; + + ui.updateTransferProgress(0, total, `Found ${total} items. Starting import...`); + + // Process sequentially or parallel? Parallel is fine for GAS calls usually, + // but let's count them accurately. + items.forEach(item => { - console.log("[MediaManager] Processing Item:", JSON.stringify(item)); + if (this.transferState.shouldCancel) return; - // The API returns nested 'mediaFile' object for actual file details const mediaFile = item.mediaFile || item; - const url = mediaFile.baseUrl || item.baseUrl; const filename = mediaFile.filename || item.filename; let mimeType = mediaFile.mimeType || item.mimeType; - console.log(`[MediaManager] Extracted: URL=${url ? 'Yes' : 'No'}, Mime=${mimeType}, Name=${filename}`); - - // Force video mimeType if metadata indicates video (Critical for backend =dv param) if (item.mediaMetadata && item.mediaMetadata.video) { - console.log("[MediaManager] Metadata indicates VIDEO. Forcing video/mp4."); mimeType = 'video/mp4'; } google.script.run .withSuccessHandler(() => { + if (this.transferState.shouldCancel) return; done++; - if (done === items.length) { - ui.updatePhotoStatus("Done!"); - controller.loadMedia(); - this.setPickerState(false); - setTimeout(() => ui.closePhotoSession(), 2000); - } + ui.updateTransferProgress(done, total); + if (done === total) this.finishTransfer(); }) .withFailureHandler(e => { - console.error("Import failed", e); - // If last one + console.error(e); done++; - if (done === items.length) { - this.setPickerState(false); - } + ui.updateTransferProgress(done, total, "Error importing item"); + if (done === total) this.finishTransfer(); }) .importFromPicker(state.sku, null, mimeType, filename, url); });