feat(media): implement integrated media manager with sidebar and picker

- Implement DriveService and ShopifyMediaService for backend operations
- Create MediaSidebar.html with premium UI and auto-polling
- Integrate Google Picker API for robust file selection
- Orchestrate sync logic via MediaService (Drive -> Staged Upload -> Shopify)
- Add secure config handling for API keys and tokens
- Update ppsscript.json with required OAuth scopes
- Update MEMORY.md and README.md with architecture details
This commit is contained in:
Ben Miller
2025-12-25 15:10:17 -07:00
parent 2417359595
commit 95094b1674
21 changed files with 973 additions and 16 deletions

122
src/mediaHandlers.ts Normal file
View File

@ -0,0 +1,122 @@
import { GASSpreadsheetService } from "./services/GASSpreadsheetService"
import { GASDriveService } from "./services/GASDriveService"
import { ShopifyMediaService } from "./services/ShopifyMediaService"
import { GASNetworkService } from "./services/GASNetworkService"
import { MediaService } from "./services/MediaService"
import { Shop } from "./shopifyApi"
import { Config } from "./config"
import { Product } from "./Product"
export function showMediaSidebar() {
const html = HtmlService.createHtmlOutputFromFile("MediaSidebar")
.setTitle("Media Manager")
.setWidth(350);
SpreadsheetApp.getUi().showSidebar(html);
}
export function getSelectedSku(): string | null {
const ss = new GASSpreadsheetService()
const sheet = SpreadsheetApp.getActiveSheet()
if (sheet.getName() !== "product_inventory") return null
const row = sheet.getActiveRange().getRow()
if (row <= 1) return null // Header
const sku = ss.getCellValueByColumnName("product_inventory", row, "sku")
return sku ? String(sku) : null
}
export function getPickerConfig() {
const config = new Config()
return {
apiKey: config.googlePickerApiKey,
token: ScriptApp.getOAuthToken(),
email: Session.getEffectiveUser().getEmail(),
parentId: config.productPhotosFolderId // Root folder to start picker in? Optionally could be SKU folder
}
}
export function getMediaForSku(sku: string): any[] {
const config = new Config()
const driveService = new GASDriveService()
try {
const folder = driveService.getOrCreateFolder(sku, config.productPhotosFolderId)
const files = driveService.getFiles(folder.getId())
return files.map(f => {
let thumb = ""
try {
const bytes = f.getThumbnail().getBytes()
thumb = "data:image/png;base64," + Utilities.base64Encode(bytes)
} catch (e) {
console.log(`Failed to get thumbnail for ${f.getName()}`)
// Fallback or empty
}
return {
id: f.getId(),
name: f.getName(),
thumbnailLink: thumb
}
})
} catch (e) {
console.error(e)
return []
}
}
export function saveFileToDrive(sku: string, filename: string, mimeType: string, base64Data: string) {
const config = new Config()
const driveService = new GASDriveService()
const folder = driveService.getOrCreateFolder(sku, config.productPhotosFolderId)
const blob = Utilities.newBlob(Utilities.base64Decode(base64Data), mimeType, filename)
driveService.saveFile(blob, folder.getId())
// Auto-sync after upload?
// syncMediaForSku(sku) // Optional: auto-sync
}
// Picker Callback specific handler if needed, or we just rely on frontend passing back file ID
// Implementing a "copy from Picker" handler
export function importFromPicker(sku: string, fileId: string, mimeType: string) {
const config = new Config()
const driveService = new GASDriveService()
// Check if file is already in our folder structure?
// If user picks from "Photos", it's a separate Blob. We might need to copy it to our SKU folder.
// Use DriveApp to get the file (if we have permissions) and make a copy.
console.log(`Importing ${fileId} for ${sku}`)
const file = DriveApp.getFileById(fileId) // Assuming we have scope
const folder = driveService.getOrCreateFolder(sku, config.productPhotosFolderId)
file.makeCopy(file.getName(), folder)
}
export function syncMediaForSku(sku: 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)
// Need Shopify Product ID
// We can get it from the Product class or Sheet
const product = new Product(sku)
if (!product.shopify_id) {
product.MatchToShopifyProduct(shop)
}
if (!product.shopify_id) {
throw new Error("Product not found on Shopify. Please sync product first.")
}
mediaService.syncMediaForSku(sku, product.shopify_id)
// Update thumbnail in sheet
// TODO: Implement thumbnail update in sheet if desired
}