import { ref, computed, type ComputedRef } from 'vue' export interface BannerFallbackOptions { /** Primary image URL candidates, tried in order */ primaryUrls: () => (string | undefined | null)[] /** Async fetch to try when all primary URLs fail */ apiFetch: () => Promise<{ posterUrl: string | null; backdropUrl: string | null }> /** Title for gradient generation */ title: () => string /** Optional seed override for gradient hue */ gradientSeed?: () => string } export interface BannerFallbackReturn { bannerSrc: ComputedRef fallbackGradient: ComputedRef onBannerError: () => void } export function useBannerFallback(options: BannerFallbackOptions): BannerFallbackReturn { const primaryIndex = ref(0) const stage = ref<'primary' | 'api' | 'done'>('primary') const apiUrl = ref(null) let apiFetching = false const bannerSrc = computed(() => { if (stage.value === 'done') return null if (stage.value === 'primary') { const urls = options.primaryUrls() // Find first non-null URL starting from primaryIndex for (let i = primaryIndex.value; i < urls.length; i++) { if (urls[i]) return urls[i]! } // No primary URLs available — skip to API immediately return null } if (stage.value === 'api' && apiUrl.value) return apiUrl.value return null }) const fallbackGradient = computed(() => { const seed = options.gradientSeed ? options.gradientSeed() : options.title() const hue = [...seed].reduce((acc, c) => acc + c.charCodeAt(0), 0) % 360 return `linear-gradient(135deg, hsl(${hue}, 25%, 12%) 0%, hsl(${(hue + 40) % 360}, 20%, 8%) 100%)` }) async function onBannerError() { if (stage.value === 'primary') { const urls = options.primaryUrls() // Advance to next primary URL let nextIdx = primaryIndex.value + 1 while (nextIdx < urls.length && !urls[nextIdx]) nextIdx++ if (nextIdx < urls.length) { primaryIndex.value = nextIdx return } // All primaries exhausted — try API if (!apiFetching) { apiFetching = true try { const result = await options.apiFetch() const url = result.backdropUrl ?? result.posterUrl if (url) { apiUrl.value = url stage.value = 'api' return } } catch { /* ignore */ } } stage.value = 'done' return } if (stage.value === 'api') { stage.value = 'done' } } // If no primary URLs at all, trigger API fetch on first render const urls = options.primaryUrls() const hasAnyPrimary = urls.some(u => !!u) if (!hasAnyPrimary && !apiFetching) { apiFetching = true options.apiFetch().then(result => { const url = result.backdropUrl ?? result.posterUrl if (url) { apiUrl.value = url stage.value = 'api' } else { stage.value = 'done' } }).catch(() => { stage.value = 'done' }) } return { bannerSrc, fallbackGradient, onBannerError } }