diff --git a/src/MediaManager.html b/src/MediaManager.html
index c3d9e55..770b794 100644
--- a/src/MediaManager.html
+++ b/src/MediaManager.html
@@ -484,6 +484,52 @@
+
+
☁️
@@ -986,11 +1032,13 @@
}));
state.setItems(normalized);
- document.getElementById('loading-ui').style.display = 'none';
- document.getElementById('main-ui').style.display = 'block';
- ui.logStatus('done', 'Finished loading.', 'success');
- setTimeout(() => ui.toggleLog(false), 1000); // Auto hide after 1s
+ if (!controller.hasRunMatching) {
+ controller.hasRunMatching = true;
+ controller.checkMatches(normalized);
+ } else {
+ controller.showGallery();
+ }
})
.withFailureHandler(function (err) {
@@ -1114,7 +1162,151 @@
})
.importFromPicker(state.sku, null, item.mimeType, item.filename, url);
});
- }
+ },
+
+ // --- Compatibility / Matching Logic ---
+ matches: [],
+ currentMatchIndex: 0,
+ hasRunMatching: false,
+
+ checkMatches(items) {
+ // Filter candidates
+ var driveOnly = items.filter(function (i) { return i.status === 'drive_only'; });
+ var shopifyOnly = items.filter(function (i) { return i.source === 'shopify_only'; }); // source check is safer for shopify items
+
+ var newMatches = [];
+
+ driveOnly.forEach(function (d) {
+ // Find match by filename
+ // Note: Backend might return "Orphaned Media" if extraction failed, ignore those.
+ if (!d.filename || d.filename === 'Orphaned Media') return;
+
+ var match = shopifyOnly.find(function (s) {
+ return s.filename === d.filename; // Exact match
+ });
+
+ if (match) {
+ newMatches.push({ drive: d, shopify: match });
+ }
+ });
+
+ if (newMatches.length > 0) {
+ this.matches = newMatches;
+ this.currentMatchIndex = 0;
+ ui.logStatus('info', 'Found ' + newMatches.length + ' potential matches. Starting matching wizard...', 'info');
+ this.startMatching();
+ } else {
+ // No matches, show UI
+ this.showGallery();
+ }
+ },
+
+ startMatching() {
+ document.getElementById('loading-ui').style.display = 'none';
+ document.getElementById('main-ui').style.display = 'none';
+ document.getElementById('matching-modal').style.display = 'flex';
+ this.renderMatch();
+ },
+
+ renderMatch() {
+ var match = this.matches[this.currentMatchIndex];
+
+ // Reset Buttons
+ var btnConfirm = document.getElementById('btn-match-confirm');
+ var btnSkip = document.getElementById('btn-match-skip');
+ if (btnConfirm) {
+ btnConfirm.disabled = false;
+ btnConfirm.innerText = "Yes, Link";
+ }
+ if (btnSkip) {
+ btnSkip.disabled = false;
+ btnSkip.innerText = "No, Skip";
+ }
+
+ var dImg = document.getElementById('match-drive-img');
+ var sImg = document.getElementById('match-shopify-img');
+
+ // Reset visual state safely
+ dImg.style.transition = 'none';
+ dImg.style.opacity = '0';
+ sImg.style.transition = 'none';
+ sImg.style.opacity = '0';
+
+ // Clear source to blank pixel to ensure old image is gone
+ var blank = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
+ dImg.src = blank;
+ sImg.src = blank;
+
+ document.getElementById('match-drive-name').innerText = match.drive.filename;
+ document.getElementById('match-shopify-name').innerText = match.shopify.filename;
+
+ document.getElementById('match-index').innerText = this.currentMatchIndex + 1;
+ document.getElementById('match-total').innerText = this.matches.length;
+
+ // Load new images
+ setTimeout(function () {
+ dImg.style.transition = 'opacity 0.3s ease';
+ sImg.style.transition = 'opacity 0.3s ease';
+
+ dImg.onload = function () { dImg.style.opacity = '1'; };
+ sImg.onload = function () { sImg.style.opacity = '1'; };
+
+ dImg.src = match.drive.thumbnail;
+ sImg.src = match.shopify.thumbnail;
+ }, 50);
+ },
+
+ confirmLink() {
+ var match = this.matches[this.currentMatchIndex];
+ var _this = this;
+
+ document.getElementById('btn-match-confirm').disabled = true;
+ document.getElementById('btn-match-confirm').innerText = "Linking...";
+ document.getElementById('btn-match-skip').disabled = true;
+
+ // ui.logStatus('link', 'Linking ' + match.drive.filename + '...', 'info');
+
+ google.script.run
+ .withSuccessHandler(function () {
+ // ui.logStatus('link', 'Linked ' + match.drive.filename, 'success');
+ _this.nextMatch();
+ })
+ .withFailureHandler(function (e) {
+ alert("Failed to link: " + e.message);
+ document.getElementById('btn-match-confirm').disabled = false;
+ document.getElementById('btn-match-confirm').innerText = "Yes, Link";
+ document.getElementById('btn-match-skip').disabled = false;
+ })
+ .linkDriveFileToShopifyMedia(state.sku, match.drive.id, match.shopify.id);
+ },
+
+ skipLink() {
+ document.getElementById('btn-match-skip').innerText = "Skipping...";
+ document.getElementById('btn-match-skip').disabled = true;
+ document.getElementById('btn-match-confirm').disabled = true;
+ setTimeout(() => this.nextMatch(), 200);
+ },
+
+ nextMatch() {
+ this.currentMatchIndex++;
+ if (this.currentMatchIndex < this.matches.length) {
+ this.renderMatch();
+ } else {
+ // Done
+ document.getElementById('matching-modal').style.display = 'none';
+ ui.logStatus('info', 'Matching complete. Reloading...', 'info');
+ document.getElementById('loading-ui').style.display = 'block';
+ // Reload to get fresh state. Since hasRunMatching is true, it shouldn't trigger again.
+ this.loadMedia(true);
+ }
+ },
+
+ showGallery() {
+ document.getElementById('loading-ui').style.display = 'none';
+ document.getElementById('main-ui').style.display = 'block';
+ ui.logStatus('done', 'Finished loading.', 'success');
+ setTimeout(function () { ui.toggleLog(false); }, 1000);
+ }
};
// --- Google Picker API ---
diff --git a/src/global.ts b/src/global.ts
index 49c5b40..b8fe463 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -23,7 +23,7 @@ import { fillProductFromTemplate } from "./fillProductFromTemplate"
import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar"
import { checkRecentSales, reconcileSalesHandler } from "./salesSync"
import { installSalesSyncTrigger } from "./triggers"
-import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess } from "./mediaHandlers"
+import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess, linkDriveFileToShopifyMedia } from "./mediaHandlers"
import { runSystemDiagnostics } from "./verificationSuite"
// prettier-ignore
@@ -64,3 +64,4 @@ import { runSystemDiagnostics } from "./verificationSuite"
;(global as any).createPhotoSession = createPhotoSession
;(global as any).checkPhotoSession = checkPhotoSession
;(global as any).debugFolderAccess = debugFolderAccess
+;(global as any).linkDriveFileToShopifyMedia = linkDriveFileToShopifyMedia
diff --git a/src/mediaHandlers.ts b/src/mediaHandlers.ts
index 62c02cc..1fbebb6 100644
--- a/src/mediaHandlers.ts
+++ b/src/mediaHandlers.ts
@@ -116,6 +116,17 @@ export function getMediaDiagnostics(sku: string) {
}
}
+export function linkDriveFileToShopifyMedia(sku: string, driveId: string, shopifyId: string) {
+ const config = new Config()
+ const driveService = new GASDriveService()
+ const shop = new Shop()
+ const shopifyMediaService = new ShopifyMediaService(shop)
+ const networkService = new GASNetworkService()
+ const mediaService = new MediaService(driveService, shopifyMediaService, networkService, config)
+
+ return mediaService.linkDriveFileToShopifyMedia(sku, driveId, shopifyId)
+}
+
export function saveFileToDrive(sku: string, filename: string, mimeType: string, base64Data: string) {
const config = new Config()
const driveService = new GASDriveService()
diff --git a/src/services/MediaService.ts b/src/services/MediaService.ts
index 50a77ab..5f19c16 100644
--- a/src/services/MediaService.ts
+++ b/src/services/MediaService.ts
@@ -153,12 +153,26 @@ export class MediaService {
contentUrl = m.image.url
}
+ // Extract filename from URL (Shopify URLs usually contain the filename)
+ let filename = "Orphaned Media";
+ try {
+ if (contentUrl) {
+ // Clean query params and get last segment
+ const cleanUrl = contentUrl.split('?')[0];
+ const parts = cleanUrl.split('/');
+ const candidate = parts.pop();
+ if (candidate) filename = candidate;
+ }
+ } catch (e) {
+ console.warn("Failed to extract filename from URL", e);
+ }
+
+
unifiedState.push({
id: m.id, // Use Shopify ID keys for orphans
driveId: null,
shopifyId: m.id,
- filename: "Orphaned Media", // Shopify doesn't always expose filename cleanly in same way
- // Try to get filename if possible or fallback
+ filename: filename,
source: 'shopify_only',
thumbnail: m.preview?.image?.originalSrc || "",
status: 'active',
@@ -172,6 +186,13 @@ export class MediaService {
return unifiedState
}
+ linkDriveFileToShopifyMedia(sku: string, driveId: string, shopifyId: string) {
+ console.log(`MediaService: Linking Drive File ${driveId} to Shopify Media ${shopifyId}`);
+ // Verify ownership? Maybe later. For now, trust the ID.
+ this.driveService.updateFileProperties(driveId, { shopify_media_id: shopifyId });
+ return { success: true };
+ }
+
processMediaChanges(sku: string, finalState: any[], shopifyProductId: string): string[] {
const logs: string[] = []
logs.push(`Starting processing for SKU ${sku}`)