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:
@ -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...";
|
||||||
|
|||||||
Reference in New Issue
Block a user