feat(app): better diff/code comments (#14621)
Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com> Co-authored-by: David Hill <iamdavidhill@gmail.com>
This commit is contained in:
co-authored by
adamelmore
David Hill
parent
9a6bfeb782
commit
fc52e4b2d3
@@ -0,0 +1,265 @@
|
||||
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
|
||||
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 (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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user