| 1 | import type { UIImage, UIVideo } from "@/lib/types"; |
| 2 | |
| 3 | export interface WireMediaRef { |
| 4 | url: string; |
| 5 | name?: string; |
| 6 | } |
| 7 | |
| 8 | const IMAGE_EXT_RE = /\.(avif|bmp|gif|jpe?g|png|webp)(?:$|[?#])/i; |
| 9 | const VIDEO_EXT_RE = /\.(m4v|mov|mp4|og[gv]|webm)(?:$|[?#])/i; |
| 10 | |
| 11 | function mediaLeaf(url: string): string | undefined { |
| 12 | if (!url) return undefined; |
| 13 | try { |
| 14 | const parsed = new URL(url, "http://localhost"); |
| 15 | const parts = parsed.pathname.split("/"); |
| 16 | return decodeURIComponent(parts[parts.length - 1] || "") || undefined; |
| 17 | } catch { |
| 18 | const parts = url.split("/"); |
| 19 | return parts[parts.length - 1] || undefined; |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | export function inferMediaKind( |
| 24 | url?: string, |
| 25 | name?: string, |
| 26 | ): "image" | "video" | null { |
| 27 | const candidates = [name ?? "", url ?? ""]; |
| 28 | if (candidates.some((candidate) => IMAGE_EXT_RE.test(candidate))) return "image"; |
| 29 | if (candidates.some((candidate) => VIDEO_EXT_RE.test(candidate))) return "video"; |
| 30 | return null; |
| 31 | } |
| 32 | |
| 33 | function mediaName(ref: WireMediaRef): string | undefined { |
| 34 | return ref.name || mediaLeaf(ref.url); |
| 35 | } |
| 36 | |
| 37 | export function splitMediaByKind( |
| 38 | refs?: WireMediaRef[] | null, |
| 39 | ): { |
| 40 | images?: UIImage[]; |
| 41 | videos?: UIVideo[]; |
| 42 | } { |
| 43 | const images: UIImage[] = []; |
| 44 | const videos: UIVideo[] = []; |
| 45 | for (const ref of refs ?? []) { |
| 46 | if (!ref?.url) continue; |
| 47 | const name = mediaName(ref); |
| 48 | const kind = inferMediaKind(ref.url, name); |
| 49 | if (kind === "image") { |
| 50 | images.push({ url: ref.url, name }); |
| 51 | } else if (kind === "video") { |
| 52 | videos.push({ url: ref.url, name }); |
| 53 | } |
| 54 | } |
| 55 | return { |
| 56 | ...(images.length > 0 ? { images } : {}), |
| 57 | ...(videos.length > 0 ? { videos } : {}), |
| 58 | }; |
| 59 | } |
| 60 | |
| 61 | const COMPOSE_PLAYED_PREFIX = "echo:compose-played:"; |
| 62 | |
| 63 | /** True if this tab already auto-played (or user-played) the compose video. */ |
| 64 | export function wasComposeVideoPlayed(url: string): boolean { |
| 65 | if (!url) return false; |
| 66 | try { |
| 67 | return sessionStorage.getItem(COMPOSE_PLAYED_PREFIX + url) === "1"; |
| 68 | } catch { |
| 69 | return false; |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | export function markComposeVideoPlayed(url: string): void { |
| 74 | if (!url) return; |
| 75 | try { |
| 76 | sessionStorage.setItem(COMPOSE_PLAYED_PREFIX + url, "1"); |
| 77 | } catch { |
| 78 | // private mode / quota |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | export function wireMediaRefs( |
| 83 | mediaUrls?: WireMediaRef[] | null, |
| 84 | fallbackUrls?: string[] | null, |
| 85 | ): WireMediaRef[] { |
| 86 | if (Array.isArray(mediaUrls) && mediaUrls.length > 0) { |
| 87 | return mediaUrls.filter((item): item is WireMediaRef => !!item?.url); |
| 88 | } |
| 89 | return (fallbackUrls ?? []) |
| 90 | .filter((url): url is string => typeof url === "string" && url.length > 0) |
| 91 | .map((url) => ({ url, name: mediaLeaf(url) })); |
| 92 | } |
| 93 |