Files
simple-react-app-kit/packages/media/src/media-detail.tsx
T
2026-09-20 16:44:15 +08:00

192 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from "react"
import {
ExternalLinkIcon,
FilmIcon,
HeartIcon,
ImageIcon,
Trash2Icon,
} from "@workspace/ui/components/icon"
import { useTranslate } from "@workspace/i18n"
import { Button } from "@workspace/ui/components/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@workspace/ui/components/dialog"
import { Separator } from "@workspace/ui/components/separator"
import { cn } from "@workspace/ui/lib/utils"
import { mediaMessages } from "./messages"
import type { MediaAsset } from "./types"
import { formatMediaFileSize } from "./utils"
export interface MediaDetailDialogProps {
asset?: MediaAsset
deletePending?: boolean
favoritePending?: boolean
open: boolean
resolveUrl: (asset: MediaAsset) => string
onDelete: (asset: MediaAsset) => void
onFavorite: (asset: MediaAsset) => void
onOpenChange: (open: boolean) => void
}
export function MediaDetailDialog({
asset,
deletePending,
favoritePending,
open,
resolveUrl,
onDelete,
onFavorite,
onOpenChange,
}: MediaDetailDialogProps) {
const t = useTranslate()
const [zoom, setZoom] = React.useState(1)
const imageSource = asset?.kind === "image" ? resolveUrl(asset) : undefined
React.useEffect(() => setZoom(1), [imageSource, open])
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{t(mediaMessages.details)}</DialogTitle>
<DialogDescription className="truncate">
{asset?.name}
</DialogDescription>
</DialogHeader>
{asset && (
<>
<div className="grid max-h-[55dvh] min-h-56 place-items-center overflow-hidden rounded-lg bg-muted/50">
{asset.kind === "image" ? (
<img
src={imageSource}
alt={asset.name}
draggable={false}
className="max-h-[55dvh] max-w-full touch-none object-contain select-none"
style={{ transform: `scale(${zoom})` }}
onWheel={(event) => {
event.preventDefault()
setZoom((current) =>
Math.min(4, Math.max(1, current - event.deltaY / 400))
)
}}
onDoubleClick={() => setZoom(1)}
/>
) : (
<video
src={resolveUrl(asset)}
controls
preload="metadata"
className="max-h-[55dvh] max-w-full bg-black object-contain"
/>
)}
</div>
<div className="flex min-w-0 items-center gap-2">
{asset.kind === "image" ? (
<ImageIcon className="size-4 shrink-0" />
) : (
<FilmIcon className="size-4 shrink-0" />
)}
<strong className="truncate">{asset.name}</strong>
</div>
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-[repeat(3,minmax(0,1fr))_auto]">
<MediaMetadata
label={t(mediaMessages.type)}
value={asset.mimeType}
/>
<MediaMetadata
label={t(mediaMessages.size)}
value={formatMediaFileSize(asset.sizeBytes)}
/>
<MediaMetadata
label={t(mediaMessages.dimensions)}
value={
asset.width && asset.height
? `${asset.width} × ${asset.height}`
: t(mediaMessages.noDimensions)
}
/>
<MediaMetadata
label={t(mediaMessages.fileName)}
value={asset.name}
/>
<MediaMetadata
label={t(mediaMessages.uploadedAt)}
value={new Date(asset.createdAt).toLocaleString()}
/>
</dl>
<Separator />
<DialogFooter className="flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<Button
type="button"
variant="outline"
disabled={favoritePending}
onClick={() => onFavorite(asset)}
>
<HeartIcon
className={asset.favorite ? "fill-current" : undefined}
/>
{asset.favorite
? t(mediaMessages.unfavorite)
: t(mediaMessages.favoriteAction)}
</Button>
<div className="flex flex-col gap-2 sm:flex-row">
<Button
nativeButton={false}
variant="outline"
render={
<a
href={resolveUrl(asset)}
target="_blank"
rel="noreferrer"
/>
}
>
<ExternalLinkIcon />
{t(mediaMessages.viewOriginal)}
</Button>
<Button
type="button"
variant="destructive"
disabled={deletePending}
onClick={() => onDelete(asset)}
>
<Trash2Icon />
{t(mediaMessages.delete)}
</Button>
</div>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
)
}
function MediaMetadata({
label,
value,
}: {
label: string
value: React.ReactNode
}) {
return (
<div className="min-w-0">
<dt className="text-xs text-muted-foreground">{label}</dt>
<dd className={cn("mt-1 font-medium wrap-break-word tabular-nums")}>
{value}
</dd>
</div>
)
}