Improve Media Manager loading state with parallel fetching and overlay

- Implemented simultaneous execution of getMediaDiagnostics and getMediaForSku in MediaManager.html to speed up initial load and refresh.
- Added a translucent grid-loading-overlay that appears over existing tiles during refresh, preventing interaction while maintaining context.
- Differentiated loading messages: 'Connecting to systems...' for initial load vs 'Refreshing media...' for updates.
- Fixed a syntax error in the save handler.
This commit is contained in:
Ben Miller
2025-12-31 09:05:38 -07:00
parent 8487df3ea0
commit e0e5b76c8e

View File

@ -477,6 +477,25 @@
padding-top: 12px; padding-top: 12px;
border-top: 1px solid #f1f5f9; border-top: 1px solid #f1f5f9;
} }
/* Grid Overlay */
.grid-loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(1px);
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-weight: 500;
border-radius: 8px;
}
</style> </style>
</head> </head>
@ -560,36 +579,36 @@
</button> </button>
</div> </div>
<input type="file" id="file-input" multiple style="display:none" onchange="controller.handleFiles(this.files)"> <input type="file" id="file-input" multiple style="display:none" onchange="controller.handleFiles(this.files)">
<!-- Unified Transfer Session UI --> <!-- Unified Transfer Session UI -->
<div id="transfer-session-ui" class="transfer-session" style="display:none;"> <div id="transfer-session-ui" class="transfer-session" style="display:none;">
<!-- Instructions (Top) --> <!-- Instructions (Top) -->
<div id="transfer-desc" style="font-size:13px; color:var(--text); margin-bottom:12px; line-height:1.4;"> <div id="transfer-desc" style="font-size:13px; color:var(--text); margin-bottom:12px; line-height:1.4;">
<!-- Dynamic Helper Text --> <!-- Dynamic Helper Text -->
</div> </div>
<!-- Progress Section (Middle) --> <!-- Progress Section (Middle) -->
<div id="transfer-progress-container"> <div id="transfer-progress-container">
<div <div
style="display:flex; justify-content:space-between; font-size:11px; color:var(--text-secondary); margin-bottom: 4px;"> 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-status-text">Processing...</span>
<span id="transfer-count"></span> <span id="transfer-count"></span>
</div> </div>
<div class="progress-track"> <div class="progress-track">
<div id="transfer-progress-bar" class="progress-fill"></div> <div id="transfer-progress-bar" class="progress-fill"></div>
</div> </div>
</div> </div>
<!-- Footer Buttons (Bottom) --> <!-- Footer Buttons (Bottom) -->
<div class="transfer-footer"> <div class="transfer-footer">
<button id="btn-transfer-reopen" class="btn btn-secondary" style="font-size:12px;" disabled> <button id="btn-transfer-reopen" class="btn btn-secondary" style="font-size:12px;" disabled>
Re-open Popup ↗ Re-open Popup ↗
</button> </button>
<button id="btn-transfer-cancel" onclick="controller.cancelTransfer()" class="btn btn-secondary" <button id="btn-transfer-cancel" onclick="controller.cancelTransfer()" class="btn btn-secondary"
style="font-size:12px;"> style="font-size:12px;">
Cancel Cancel
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Permanent Log Card --> <!-- Permanent Log Card -->
@ -865,81 +884,81 @@
this.saveBtn.innerText = enable ? "Save Changes" : "No Changes"; this.saveBtn.innerText = enable ? "Save Changes" : "No Changes";
}; };
UI.prototype.showTransferSession = function (mode, serviceName) { UI.prototype.showTransferSession = function (mode, serviceName) {
var el = document.getElementById('transfer-session-ui'); var el = document.getElementById('transfer-session-ui');
var desc = document.getElementById('transfer-desc'); var desc = document.getElementById('transfer-desc');
var statusText = document.getElementById('transfer-status-text'); var statusText = document.getElementById('transfer-status-text');
var bar = document.getElementById('transfer-progress-bar'); var bar = document.getElementById('transfer-progress-bar');
var btnReopen = document.getElementById('btn-transfer-reopen'); var btnReopen = document.getElementById('btn-transfer-reopen');
var btnCancel = document.getElementById('btn-transfer-cancel'); var btnCancel = document.getElementById('btn-transfer-cancel');
el.style.display = 'block'; el.style.display = 'block';
bar.style.width = '0%'; bar.style.width = '0%';
statusText.innerText = 'Initializing...'; statusText.innerText = 'Initializing...';
document.getElementById('transfer-count').innerText = ''; document.getElementById('transfer-count').innerText = '';
// Reset Buttons // Reset Buttons
btnCancel.disabled = false; btnCancel.disabled = false;
btnReopen.disabled = true; // Default btnReopen.disabled = true; // Default
if (mode === 'waiting') { if (mode === 'waiting') {
desc.innerText = "Select items from " + (serviceName || "the service") + " in the popup window. Click 'Done' when finished."; desc.innerText = "Select items from " + (serviceName || "the service") + " in the popup window. Click 'Done' when finished.";
statusText.innerText = "Waiting for selection..."; 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 { } else {
// Indeterminate desc.innerText = "Importing media from " + (serviceName || "source") + "...";
bar.style.width = '100%'; }
countText.innerText = ''; };
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 {
// Indeterminate
bar.style.width = '100%';
countText.innerText = '';
} }
if (statusMsg) statusText.innerText = statusMsg; if (statusMsg) statusText.innerText = statusMsg;
// If done, disable cancel // If done, disable cancel
if (current === total && total > 0) { if (current === total && total > 0) {
const btnCancel = document.getElementById('btn-transfer-cancel'); const btnCancel = document.getElementById('btn-transfer-cancel');
if (btnCancel) btnCancel.disabled = true; if (btnCancel) btnCancel.disabled = true;
} }
}; };
UI.prototype.hideTransferSession = function () { UI.prototype.hideTransferSession = function () {
document.getElementById('transfer-session-ui').style.display = 'none'; document.getElementById('transfer-session-ui').style.display = 'none';
}; };
UI.prototype.setupReopenButton = function (url) { UI.prototype.setupReopenButton = function (url) {
var btn = document.getElementById('btn-transfer-reopen'); var btn = document.getElementById('btn-transfer-reopen');
if (!url) { if (!url) {
btn.disabled = true; btn.disabled = true;
return; return;
} }
const width = 1200; const width = 1200;
const height = 800; const height = 800;
const left = (screen.width - width) / 2; const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2; const top = (screen.height - height) / 2;
const params = `width=${width},height=${height},top=${top},left=${left}`; const params = `width=${width},height=${height},top=${top},left=${left}`;
btn.onclick = function (e) { btn.onclick = function (e) {
e.preventDefault(); e.preventDefault();
window.open(url, 'googlePhotos', params); window.open(url, 'googlePhotos', params);
}; };
btn.disabled = false; btn.disabled = false;
}; };
UI.prototype.render = function (items) { UI.prototype.render = function (items) {
@ -983,10 +1002,31 @@
}; };
UI.prototype.setLoadingState = function (isLoading) { UI.prototype.setLoadingState = function (isLoading) {
var overlay = document.getElementById('grid-loading-overlay');
if (isLoading) { if (isLoading) {
this.grid.innerHTML = '<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--text-secondary);">' + // Check if we have items
'<div class="spinner" style="margin-bottom: 12px;"></div>' + var hasItems = this.grid.children.length > 0 && !this.grid.querySelector('.empty-state');
'<div>Connecting to systems...</div></div>';
if (hasItems) {
// Create overlay if not exists
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'grid-loading-overlay';
overlay.className = 'grid-loading-overlay';
overlay.innerHTML = '<div class="spinner" style="margin-bottom: 12px;"></div><div>Refreshing media...</div>';
this.grid.style.position = 'relative'; // Ensure positioning context
this.grid.appendChild(overlay);
}
} else {
// Standard empty state loading
this.grid.innerHTML = '<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--text-secondary);">' +
'<div class="spinner" style="margin-bottom: 12px;"></div>' +
'<div>Loading media...</div></div>';
}
} else {
// Clear overlay
if (overlay) overlay.remove();
} }
}; };
@ -1281,9 +1321,9 @@
ui.logStatus('init', 'Initializing access...', 'info'); ui.logStatus('init', 'Initializing access...', 'info');
// 1. Diagnostics (Parallel)
google.script.run google.script.run
.withSuccessHandler((diagnostics) => { // Use arrow .withSuccessHandler((diagnostics) => {
// Check Resumption // Check Resumption
if (diagnostics.activeJobId) { if (diagnostics.activeJobId) {
ui.logStatus('resume', 'Resuming active background job...', 'info'); ui.logStatus('resume', 'Resuming active background job...', 'info');
@ -1304,7 +1344,6 @@
// Capture Token // Capture Token
if (diagnostics.token) state.token = diagnostics.token; if (diagnostics.token) state.token = diagnostics.token;
// Shopify Status // Shopify Status
if (diagnostics.shopify.status === 'ok') { if (diagnostics.shopify.status === 'ok') {
ui.logStatus('shopify', `Shopify Product: ok (${diagnostics.shopify.mediaCount} media) (ID: ${diagnostics.shopify.id}) <a href="${diagnostics.shopify.adminUrl}" target="_blank" style="margin-left:8px;">Open Admin ↗</a>`, 'success'); ui.logStatus('shopify', `Shopify Product: ok (${diagnostics.shopify.mediaCount} media) (ID: ${diagnostics.shopify.id}) <a href="${diagnostics.shopify.adminUrl}" target="_blank" style="margin-left:8px;">Open Admin ↗</a>`, 'success');
@ -1314,43 +1353,44 @@
} else { } else {
ui.logStatus('shopify', `Shopify Check Failed: ${diagnostics.shopify.error}`, 'error'); ui.logStatus('shopify', `Shopify Check Failed: ${diagnostics.shopify.error}`, 'error');
} }
ui.logStatus('fetch', 'Fetching full media state (this may take a moment)...', 'info');
// 2. Load Full Media
google.script.run
.withSuccessHandler(function (items) {
// Normalize items
const normalized = items.map(i => ({
...i,
id: i.id || Math.random().toString(36).substr(2, 9),
status: i.source || 'drive_only', // Fix: Use source as status
source: i.source,
_deleted: false // Init soft delete flag
}));
state.setItems(normalized);
if (!controller.hasRunMatching) {
controller.hasRunMatching = true;
controller.checkMatches(normalized);
} else {
controller.showGallery();
}
})
.withFailureHandler(function (err) {
ui.logStatus('fatal', `Failed to load media: ${err.message}`, 'error');
})
.getMediaForSku(sku);
}) })
.withFailureHandler(function (err) { .withFailureHandler(function (err) {
ui.logStatus('fatal', `Diagnostics failed: ${err.message}`, 'error'); ui.logStatus('fatal', `Diagnostics failed: ${err.message}`, 'error');
}) })
.getMediaDiagnostics(sku, ""); .getMediaDiagnostics(sku, "");
// 2. Load Full Media (Parallel)
ui.logStatus('fetch', 'Fetching full media state...', 'info');
google.script.run
.withSuccessHandler(function (items) {
// Normalize items
const normalized = items.map(function (i) {
return {
...i,
id: i.id || Math.random().toString(36).substr(2, 9),
status: i.source || 'drive_only',
source: i.source,
_deleted: false
};
});
state.setItems(normalized);
if (!controller.hasRunMatching) {
controller.hasRunMatching = true;
controller.checkMatches(normalized);
} else {
controller.showGallery();
}
})
.withFailureHandler(function (err) {
ui.logStatus('fatal', `Failed to load media: ${err.message}`, 'error');
ui.setLoadingState(false);
})
.getMediaForSku(sku);
}, },
saveChanges() { saveChanges() {
ui.toggleSave(false); ui.toggleSave(false);
ui.saveBtn.innerText = "Saving..."; ui.saveBtn.innerText = "Saving...";