返回 JoyAI-Echo
media.ts
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
93 lines TYPESCRIPT