// useThumbs — authenticated thumbnail loader for the grid view. // // The backend emits `thumb_url` as a ROOT-RELATIVE path ("/api/files/thumb/{id}"). // A plain `` only works for the native same-origin SPA: an embedded // webcomponent resolves it against the HOST page's origin (work.example.com → 404) // and, even with the URL fixed, `` cannot carry the bearer header a // proxied host (fishapp PWA) requires. So thumbs are fetched through the same // auth machinery as every API call (headers + credentials), cached as object // URLs, and handed to the grid; a failed fetch falls back to the file icon. import { ref } from 'vue'; import type { FileNode } from '../types/FileNode'; interface ThumbApiSlice { authHeaders: (extra?: Record) => Promise>; credentialsMode: () => RequestCredentials; } /** Cap the object-URL cache; beyond it oldest entries are revoked. Thumbs are * small (~KBs) so this is generous while still bounding a long session. */ const MAX_CACHED = 500; export function useThumbs(apiBase: string | undefined, api: ThumbApiSlice) { const urls = ref>({}); const order: string[] = []; const failed = new Set(); const pending = new Set(); function resolveUrl(raw: string): string { if (/^https?:\/\//i.test(raw)) return raw; const base = (apiBase || '').replace(/\/+$/, ''); return base && raw.startsWith('/') ? base + raw : raw; } /** Reactive: returns the loaded object URL for the node's thumb, kicking off * the fetch on first sight. null = not (yet) available → show the icon. */ function src(n: FileNode): string | null { const raw = n.thumb_url; if (!raw) return null; const got = urls.value[raw]; if (got) return got; if (!failed.has(raw) && !pending.has(raw)) void load(raw); return null; } async function load(raw: string): Promise { pending.add(raw); try { const res = await fetch(resolveUrl(raw), { headers: await api.authHeaders({ Accept: '*/*' }), credentials: api.credentialsMode(), }); if (!res.ok) throw new Error(String(res.status)); const blob = await res.blob(); if (order.length >= MAX_CACHED) { const evict = order.shift(); if (evict && urls.value[evict]) { URL.revokeObjectURL(urls.value[evict]); const next = { ...urls.value }; delete next[evict]; urls.value = next; } } order.push(raw); urls.value = { ...urls.value, [raw]: URL.createObjectURL(blob) }; } catch { // 404 (thumb evicted server-side) or auth hiccup — icon fallback, and we // don't retry within this session to avoid hammering. failed.add(raw); } finally { pending.delete(raw); } } return { src }; }