Refine Photo Picker Session UI and Logic
- Unified transfer session UI layout with instructions at the top and footer controls. - Implemented side-by-side 'Re-open Popup' and 'Cancel' buttons with proper state management (disabled/enabled). - Added dynamic service context to instructions (e.g., 'Importing from Google Photos'). - Refactored UI class to handle new DOM structure and button logic. - Updated controller to support new UI interactions and improved cancellation flow.
This commit is contained in:
@ -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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -530,21 +560,36 @@
|
||||
</button>
|
||||
</div>
|
||||
<input type="file" id="file-input" multiple style="display:none" onchange="controller.handleFiles(this.files)">
|
||||
</div>
|
||||
<!-- Unified Transfer Session UI -->
|
||||
<div id="transfer-session-ui" class="transfer-session" style="display:none;">
|
||||
<!-- Instructions (Top) -->
|
||||
<div id="transfer-desc" style="font-size:13px; color:var(--text); margin-bottom:12px; line-height:1.4;">
|
||||
<!-- Dynamic Helper Text -->
|
||||
</div>
|
||||
|
||||
<!-- Photos Session UI -->
|
||||
<div id="photos-session-ui"
|
||||
style="display:none; margin-top:12px; padding:12px; background:#f0f9ff; border-radius:8px; border:1px solid #bae6fd;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
|
||||
<span style="font-weight:600; font-size:12px; color:#0369a1;">Photo Picker Session</span>
|
||||
<button onclick="ui.closePhotoSession()"
|
||||
style="background:none; border:none; color:#0369a1; cursor:pointer; font-size:16px;">×</button>
|
||||
</div>
|
||||
<a id="photos-session-link" href="#" target="_blank" class="btn"
|
||||
style="background:#0ea5e9; text-decoration:none; margin-bottom:8px;">
|
||||
Open Google Photos ↗
|
||||
</a>
|
||||
<div id="photos-session-status" style="font-size:11px; color:#64748b; text-align:center;">Initializing...</div>
|
||||
<!-- Progress Section (Middle) -->
|
||||
<div id="transfer-progress-container">
|
||||
<div
|
||||
style="display:flex; justify-content:space-between; font-size:11px; color:var(--text-secondary); margin-bottom: 4px;">
|
||||
<span id="transfer-status-text">Processing...</span>
|
||||
<span id="transfer-count"></span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div id="transfer-progress-bar" class="progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Buttons (Bottom) -->
|
||||
<div class="transfer-footer">
|
||||
<button id="btn-transfer-reopen" class="btn btn-secondary" style="font-size:12px;" disabled>
|
||||
Re-open Popup ↗
|
||||
</button>
|
||||
<button id="btn-transfer-cancel" onclick="controller.cancelTransfer()" class="btn btn-secondary"
|
||||
style="font-size:12px;">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Permanent Log Card -->
|
||||
@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user