From f6831cdc8f69055280807377ebeda14dd2568a32 Mon Sep 17 00:00:00 2001 From: Ben Miller Date: Mon, 29 Dec 2025 09:12:37 -0700 Subject: [PATCH] feat(media): implement video processing polling and fallback This commit adds robust handling for Google Drive videos that are still processing (lacking thumbnails). Changes include: 1. Backend (MediaService.ts): Implement try/catch around thumbnail generation. If it fails, return a placeholder and flag the item as 'isProcessing'. 2. Frontend (MediaManager.html): - Add polling logic to check for updates on processing items every 15s. - Add UI support for processing state: slate background, centered animated hourglass emoji. - Implement sand animation (toggling hourglass state) and rotation animation (180deg flip on poll event). - Fix badges and positioning issues. --- src/MediaManager.html | 1159 +++++++++++++++++++--------------- src/services/MediaService.ts | 25 +- 2 files changed, 670 insertions(+), 514 deletions(-) diff --git a/src/MediaManager.html b/src/MediaManager.html index 6c534a9..96d7905 100644 --- a/src/MediaManager.html +++ b/src/MediaManager.html @@ -153,6 +153,7 @@ } .media-overlay .icon-btn { + padding: 4px; pointer-events: auto; } @@ -160,6 +161,32 @@ opacity: 1; } + /* Processing State */ + .media-item.processing-card { + background-color: #334155 !important; + /* Slate-700 */ + display: flex; + align-items: center; + justify-content: center; + } + + .media-item.processing-card .media-content { + display: none !important; + } + + .processing-icon { + font-size: 40px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + z-index: 5; + transition: transform 0.6s ease-in-out; + } + + /* .flipping removed, handled by JS inline style */ + .type-badge { position: absolute; top: 6px; @@ -364,7 +391,7 @@ ... - + @@ -464,7 +491,8 @@ @@ -542,64 +570,64 @@ /** * State Management & Error Handling */ - window.onerror = function (msg, url, line) { - alert("Script Error: " + msg + "\nLine: " + line); - }; + window.onerror = function (msg, url, line) { + alert("Script Error: " + msg + "\nLine: " + line); + }; - // --- ES5 Refactor: MediaState --- - function MediaState() { - this.sku = null; + // --- ES5 Refactor: MediaState --- + function MediaState() { + this.sku = null; this.token = null; this.items = []; this.initialState = []; } - MediaState.prototype.setSku = function (info) { - this.sku = info ? info.sku : null; - this.title = info ? info.title : ""; - this.items = []; - this.initialState = []; - ui.updateSku(this.sku, this.title); + MediaState.prototype.setSku = function (info) { + this.sku = info ? info.sku : null; + this.title = info ? info.title : ""; + this.items = []; + this.initialState = []; + ui.updateSku(this.sku, this.title); }; - MediaState.prototype.setItems = function (items) { - this.items = items || []; + MediaState.prototype.setItems = function (items) { + this.items = items || []; this.initialState = JSON.parse(JSON.stringify(this.items)); - ui.render(this.items); - this.checkDirty(); + ui.render(this.items); + this.checkDirty(); }; - MediaState.prototype.addItem = function (item) { - this.items.push(item); - ui.render(this.items); - this.checkDirty(); + MediaState.prototype.addItem = function (item) { + this.items.push(item); + ui.render(this.items); + this.checkDirty(); }; - MediaState.prototype.deleteItem = function (index) { - var item = this.items[index]; + MediaState.prototype.deleteItem = function (index) { + var item = this.items[index]; if (item.source === 'new') { - this.items.splice(index, 1); - } else { - item._deleted = !item._deleted; - } - ui.render(this.items); - this.checkDirty(); + this.items.splice(index, 1); + } else { + item._deleted = !item._deleted; + } + ui.render(this.items); + this.checkDirty(); }; - MediaState.prototype.reorderItems = function (newIndices) { - // Handled by Sortable - }; - - MediaState.prototype.checkDirty = function () { - var plan = this.calculateDiff(); - var isDirty = plan.hasChanges; - ui.toggleSave(isDirty); - return plan; + MediaState.prototype.reorderItems = function (newIndices) { + // Handled by Sortable }; - MediaState.prototype.calculateDiff = function () { - var currentIds = new Set(this.items.map(function (i) { return i.id; })); - var initialIds = new Set(this.initialState.map(function (i) { return i.id; })); + MediaState.prototype.checkDirty = function () { + var plan = this.calculateDiff(); + var isDirty = plan.hasChanges; + ui.toggleSave(isDirty); + return plan; + }; + + MediaState.prototype.calculateDiff = function () { + var currentIds = new Set(this.items.map(function (i) { return i.id; })); + var initialIds = new Set(this.initialState.map(function (i) { return i.id; })); var actions = []; @@ -638,108 +666,108 @@ // If lengths differ despite logic, assume change or weird state } - if (orderChanged) { - actions.push({ type: 'reorder', name: 'Reorder Gallery' }); - } + if (orderChanged) { + actions.push({ type: 'reorder', name: 'Reorder Gallery' }); + } var uniqueActions = actions.filter(function (v, i, a) { return a.findIndex(function (t) { return t.type === v.type && t.name === v.name; }) === i; }); - return { - hasChanges: uniqueActions.length > 0, - actions: uniqueActions - }; + return { + hasChanges: uniqueActions.length > 0, + actions: uniqueActions + }; }; - MediaState.prototype.hasNewItems = function () { - return this.items.some(function (i) { - return !i._deleted && (i.status === 'drive_only' || i.source === 'new'); - }); - }; + MediaState.prototype.hasNewItems = function () { + return this.items.some(function (i) { + return !i._deleted && (i.status === 'drive_only' || i.source === 'new'); + }); + }; - var state = new MediaState(); - window.state = state; + var state = new MediaState(); + window.state = state; - // --- ES5 Refactor: UI --- - function UI() { - this.grid = document.getElementById('media-grid'); - this.saveBtn = document.getElementById('save-btn'); - this.toggleLogBtn = document.getElementById('toggle-log-btn'); - this.logContainer = document.getElementById('status-log-container'); - this.linksContainer = document.getElementById('quick-links'); - this.sortable = null; - this.driveUrl = null; - this.shopifyUrl = null; - } + // --- ES5 Refactor: UI --- + function UI() { + this.grid = document.getElementById('media-grid'); + this.saveBtn = document.getElementById('save-btn'); + this.toggleLogBtn = document.getElementById('toggle-log-btn'); + this.logContainer = document.getElementById('status-log-container'); + this.linksContainer = document.getElementById('quick-links'); + this.sortable = null; + this.driveUrl = null; + this.shopifyUrl = null; + } - UI.prototype.setDriveLink = function (url) { this.driveUrl = url; this.renderLinks(); }; - UI.prototype.setShopifyLink = function (url) { this.shopifyUrl = url; this.renderLinks(); }; + UI.prototype.setDriveLink = function (url) { this.driveUrl = url; this.renderLinks(); }; + UI.prototype.setShopifyLink = function (url) { this.shopifyUrl = url; this.renderLinks(); }; - UI.prototype.renderLinks = function () { - this.linksContainer.innerHTML = ''; + UI.prototype.renderLinks = function () { + this.linksContainer.innerHTML = ''; if (this.driveUrl) this.linksContainer.innerHTML += 'Drive ↗'; if (this.shopifyUrl) this.linksContainer.innerHTML += 'Shopify ↗'; }; - UI.prototype.toggleLog = function (forceState) { - var isVisible = typeof forceState === 'boolean' ? !forceState : this.logContainer.style.display !== 'none'; - this.logContainer.style.display = isVisible ? 'none' : 'block'; - this.toggleLogBtn.innerText = isVisible ? "View Log" : "Hide Log"; + UI.prototype.toggleLog = function (forceState) { + var isVisible = typeof forceState === 'boolean' ? !forceState : this.logContainer.style.display !== 'none'; + this.logContainer.style.display = isVisible ? 'none' : 'block'; + this.toggleLogBtn.innerText = isVisible ? "View Log" : "Hide Log"; }; - UI.prototype.updateSku = function (sku, title) { - document.getElementById('current-sku').innerText = sku || '...'; - document.getElementById('current-title').innerText = title || ''; - document.getElementById('loading-ui').style.display = 'none'; - document.getElementById('main-ui').style.display = 'block'; + UI.prototype.updateSku = function (sku, title) { + document.getElementById('current-sku').innerText = sku || '...'; + document.getElementById('current-title').innerText = title || ''; + document.getElementById('loading-ui').style.display = 'none'; + document.getElementById('main-ui').style.display = 'block'; }; - UI.prototype.toggleSave = function (enable) { - this.saveBtn.disabled = !enable; - this.saveBtn.innerText = enable ? "Save Changes" : "No Changes"; + UI.prototype.toggleSave = function (enable) { + this.saveBtn.disabled = !enable; + 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.showPhotoSession = function (url) { + var uiEl = document.getElementById('photos-session-ui'); + var link = document.getElementById('photos-session-link'); + var status = document.getElementById('photos-session-status'); - 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; + 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; - // Attempt popup - const popup = window.open(url, 'googlePhotos', `width=${width},height=${height},top=${top},left=${left}`); + // Attempt popup + const popup = window.open(url, 'googlePhotos', `width=${width},height=${height},top=${top},left=${left}`); - 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}`); - } - } else { - link.innerText = "Open Google Photos ↗"; - link.onclick = null; // Default href behavior - } + 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}`); + } + } else { + link.innerText = "Open Google Photos ↗"; + link.onclick = null; // Default href behavior + } - status.innerText = "Waiting for selection in popup..."; - }; + status.innerText = "Waiting for selection in popup..."; + }; - UI.prototype.closePhotoSession = function () { - document.getElementById('photos-session-ui').style.display = 'none'; - }; + UI.prototype.closePhotoSession = function () { + document.getElementById('photos-session-ui').style.display = 'none'; + }; - UI.prototype.updatePhotoStatus = function (msg) { - document.getElementById('photos-session-status').innerText = msg; - }; + UI.prototype.updatePhotoStatus = function (msg) { + document.getElementById('photos-session-status').innerText = msg; + }; - UI.prototype.render = function (items) { - this.grid.innerHTML = ''; + UI.prototype.render = function (items) { + this.grid.innerHTML = ''; var _this = this; // Capture 'this' for callbacks var activeCount = items.filter(function (i) { return !i._deleted; }).length; document.getElementById('item-count').innerText = '(' + activeCount + ')'; @@ -771,28 +799,33 @@ }); }; - UI.prototype.setLoadingState = function (isLoading) { - if (isLoading) { - this.grid.innerHTML = '
' + - '
' + - '
Connecting to systems...
'; - } + UI.prototype.setLoadingState = function (isLoading) { + if (isLoading) { + this.grid.innerHTML = '
' + + '
' + + '
Connecting to systems...
'; + } }; - UI.prototype.logStatus = function (step, message, type) { - if (!type) type = 'info'; - var container = this.logContainer; - var icon = type === 'success' ? '✅' : type === 'error' ? '❌' : '⏳'; - var el = document.createElement('div'); - el.innerHTML = '' + icon + ' ' + message; - if (type === 'error') el.style.color = 'var(--error)'; - container.appendChild(el); + UI.prototype.logStatus = function (step, message, type) { + if (!type) type = 'info'; + var container = this.logContainer; + var icon = type === 'success' ? '✅' : type === 'error' ? '❌' : '⏳'; + var el = document.createElement('div'); + el.innerHTML = '' + icon + ' ' + message; + if (type === 'error') el.style.color = 'var(--error)'; + container.appendChild(el); }; - UI.prototype.createCard = function (item, index) { - var div = document.createElement('div'); - div.className = 'media-item ' + (item._deleted ? 'deleted-item' : ''); - div.dataset.id = item.id; + UI.prototype.createCard = function (item, index) { + var div = document.createElement('div'); + div.className = 'media-item ' + (item._deleted ? 'deleted-item' : ''); + div.dataset.id = item.id; + + // Processing Class + if (item.isProcessing) { + div.className += ' processing-card'; + } div.onmouseenter = function () { var v = div.querySelector('video'); @@ -815,10 +848,16 @@ var isVideo = (item.mimeType && item.mimeType.startsWith('video/')) || (item.filename && item.filename.match(/\.(mp4|mov|webm)$/i)); if (isVideo) console.log("[MediaManager] Video Detected: " + item.filename); - var videoBadgeIcon = isVideo ? '
🎞️
' : ''; + var videoBadgeIcon = isVideo ? '
🎞️
' : ''; + // Processing Badge REMOVED (Handled by center icon now) - // content URL logic (Only relevant for Shopify where we have a direct public link) - var contentUrl = item.contentUrl || ""; + var centerIcon = ''; + if (item.isProcessing) { + centerIcon = '
'; + } + + // content URL logic (Only relevant for Shopify where we have a direct public link) + var contentUrl = item.contentUrl || ""; var actionBtn = item._deleted ? '' @@ -827,25 +866,26 @@ div.innerHTML = badge + videoBadgeIcon + + centerIcon + '
' + '' + actionBtn + '
'; - // Create Media Element - // RULE: Only create