Buckets:
| import type { FileContent } from "@opencode-ai/sdk/v2" | |
| import { createEffect, createMemo, createResource, Match, on, Show, Switch, type JSX } from "solid-js" | |
| import { useI18n } from "../context/i18n" | |
| import { | |
| dataUrlFromMediaValue, | |
| hasMediaValue, | |
| isBinaryContent, | |
| mediaKindFromPath, | |
| normalizeMimeType, | |
| svgTextFromValue, | |
| } from "../pierre/media" | |
| export type FileMediaOptions = { | |
| mode?: "auto" | "off" | |
| path?: string | |
| current?: unknown | |
| before?: unknown | |
| after?: unknown | |
| deleted?: boolean | |
| readFile?: (path: string) => Promise<FileContent | undefined> | |
| onLoad?: () => void | |
| onError?: (ctx: { kind: "image" | "audio" | "svg" }) => void | |
| } | |
| function mediaValue(cfg: FileMediaOptions, mode: "image" | "audio") { | |
| if (cfg.current !== undefined) return cfg.current | |
| if (mode === "image") return cfg.after ?? cfg.before | |
| return cfg.after ?? cfg.before | |
| } | |
| export function FileMedia(props: { media?: FileMediaOptions; fallback: () => JSX.Element }) { | |
| const i18n = useI18n() | |
| const cfg = () => props.media | |
| const kind = createMemo(() => { | |
| const media = cfg() | |
| if (!media || media.mode === "off") return | |
| return mediaKindFromPath(media.path) | |
| }) | |
| const isBinary = createMemo(() => { | |
| const media = cfg() | |
| if (!media || media.mode === "off") return false | |
| if (kind()) return false | |
| return isBinaryContent(media.current as any) | |
| }) | |
| const onLoad = () => props.media?.onLoad?.() | |
| const deleted = createMemo(() => { | |
| const media = cfg() | |
| const k = kind() | |
| if (!media || !k) return false | |
| if (media.deleted) return true | |
| if (k === "svg") return false | |
| if (media.current !== undefined) return false | |
| return !hasMediaValue(media.after as any) && hasMediaValue(media.before as any) | |
| }) | |
| const direct = createMemo(() => { | |
| const media = cfg() | |
| const k = kind() | |
| if (!media || (k !== "image" && k !== "audio")) return | |
| return dataUrlFromMediaValue(mediaValue(media, k), k) | |
| }) | |
| const request = createMemo(() => { | |
| const media = cfg() | |
| const k = kind() | |
| if (!media || (k !== "image" && k !== "audio")) return | |
| if (media.current !== undefined) return | |
| if (deleted()) return | |
| if (direct()) return | |
| if (!media.path || !media.readFile) return | |
| return { | |
| key: `${k}:${media.path}`, | |
| kind: k, | |
| path: media.path, | |
| readFile: media.readFile, | |
| onError: media.onError, | |
| } | |
| }) | |
| const [loaded] = createResource(request, async (input) => { | |
| return input.readFile(input.path).then( | |
| (result) => { | |
| const src = dataUrlFromMediaValue(result as any, input.kind) | |
| if (!src) { | |
| input.onError?.({ kind: input.kind }) | |
| return { key: input.key, error: true as const } | |
| } | |
| return { | |
| key: input.key, | |
| src, | |
| mime: input.kind === "audio" ? normalizeMimeType(result?.mimeType) : undefined, | |
| } | |
| }, | |
| () => { | |
| input.onError?.({ kind: input.kind }) | |
| return { key: input.key, error: true as const } | |
| }, | |
| ) | |
| }) | |
| const remote = createMemo(() => { | |
| const input = request() | |
| const value = loaded() | |
| if (!input || !value || value.key !== input.key) return | |
| return value | |
| }) | |
| const src = createMemo(() => { | |
| const value = remote() | |
| return direct() ?? (value && "src" in value ? value.src : undefined) | |
| }) | |
| const status = createMemo(() => { | |
| if (direct()) return "ready" as const | |
| if (!request()) return "idle" as const | |
| if (loaded.loading) return "loading" as const | |
| if (remote()?.error) return "error" as const | |
| if (src()) return "ready" as const | |
| return "idle" as const | |
| }) | |
| const audioMime = createMemo(() => { | |
| const value = remote() | |
| return value && "mime" in value ? value.mime : undefined | |
| }) | |
| const svgSource = createMemo(() => { | |
| const media = cfg() | |
| if (!media || kind() !== "svg") return | |
| return svgTextFromValue(media.current as any) | |
| }) | |
| const svgSrc = createMemo(() => { | |
| const media = cfg() | |
| if (!media || kind() !== "svg") return | |
| return dataUrlFromMediaValue(media.current as any, "svg") | |
| }) | |
| const svgInvalid = createMemo(() => { | |
| const media = cfg() | |
| if (!media || kind() !== "svg") return | |
| if (svgSource() !== undefined) return | |
| if (!hasMediaValue(media.current as any)) return | |
| return [media.path, media.current] as const | |
| }) | |
| createEffect( | |
| on( | |
| svgInvalid, | |
| (value) => { | |
| if (!value) return | |
| cfg()?.onError?.({ kind: "svg" }) | |
| }, | |
| { defer: true }, | |
| ), | |
| ) | |
| const kindLabel = (value: "image" | "audio") => | |
| i18n.t(value === "image" ? "ui.fileMedia.kind.image" : "ui.fileMedia.kind.audio") | |
| return ( | |
| <Switch> | |
| <Match when={kind() === "image" || kind() === "audio"}> | |
| <Show | |
| when={src()} | |
| fallback={(() => { | |
| const media = cfg() | |
| const k = kind() | |
| if (!media || (k !== "image" && k !== "audio")) return props.fallback() | |
| const label = kindLabel(k) | |
| if (deleted()) { | |
| return ( | |
| <div class="flex min-h-40 items-center justify-center px-6 py-4 text-center text-text-weak"> | |
| {i18n.t("ui.fileMedia.state.removed", { kind: label })} | |
| </div> | |
| ) | |
| } | |
| if (status() === "loading") { | |
| return ( | |
| <div class="flex min-h-40 items-center justify-center px-6 py-4 text-center text-text-weak"> | |
| {i18n.t("ui.fileMedia.state.loading", { kind: label })} | |
| </div> | |
| ) | |
| } | |
| if (status() === "error") { | |
| return ( | |
| <div class="flex min-h-40 items-center justify-center px-6 py-4 text-center text-text-weak"> | |
| {i18n.t("ui.fileMedia.state.error", { kind: label })} | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div class="flex min-h-40 items-center justify-center px-6 py-4 text-center text-text-weak"> | |
| {i18n.t("ui.fileMedia.state.unavailable", { kind: label })} | |
| </div> | |
| ) | |
| })()} | |
| > | |
| {(value) => { | |
| const k = kind() | |
| if (k !== "image" && k !== "audio") return props.fallback() | |
| if (k === "image") { | |
| return ( | |
| <div class="flex justify-center bg-background-stronger px-6 py-4"> | |
| <img | |
| src={value()} | |
| alt={cfg()?.path} | |
| class="max-h-[60vh] max-w-full rounded border border-border-weak-base bg-background-base object-contain" | |
| onLoad={onLoad} | |
| /> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div class="flex justify-center bg-background-stronger px-6 py-4"> | |
| <audio class="w-full max-w-xl" controls preload="metadata" onLoadedMetadata={onLoad}> | |
| <source src={value()} type={audioMime()} /> | |
| </audio> | |
| </div> | |
| ) | |
| }} | |
| </Show> | |
| </Match> | |
| <Match when={kind() === "svg"}> | |
| {(() => { | |
| if (svgSource() === undefined && svgSrc() == null) return props.fallback() | |
| return ( | |
| <div class="flex flex-col gap-4 px-6 py-4"> | |
| <Show when={svgSource() !== undefined}>{props.fallback()}</Show> | |
| <Show when={svgSrc()}> | |
| {(value) => ( | |
| <div class="flex justify-center"> | |
| <img | |
| src={value()} | |
| alt={cfg()?.path} | |
| class="max-h-[60vh] max-w-full rounded border border-border-weak-base bg-background-base object-contain" | |
| onLoad={onLoad} | |
| /> | |
| </div> | |
| )} | |
| </Show> | |
| </div> | |
| ) | |
| })()} | |
| </Match> | |
| <Match when={isBinary()}> | |
| <div class="flex min-h-56 flex-col items-center justify-center gap-2 px-6 py-10 text-center"> | |
| <div class="text-14-semibold text-text-strong"> | |
| {cfg()?.path?.split("/").pop() ?? i18n.t("ui.fileMedia.binary.title")} | |
| </div> | |
| <div class="text-14-regular text-text-weak"> | |
| {(() => { | |
| const path = cfg()?.path | |
| if (!path) return i18n.t("ui.fileMedia.binary.description.default") | |
| return i18n.t("ui.fileMedia.binary.description.path", { path }) | |
| })()} | |
| </div> | |
| </div> | |
| </Match> | |
| <Match when={true}>{props.fallback()}</Match> | |
| </Switch> | |
| ) | |
| } | |
Xet Storage Details
- Size:
- 8.65 kB
- Xet hash:
- 5c9c312a5ccf69dae291c0b7c6a510ed7267d8f97c0b60395e00af488b6e4a39
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.