2026-07-31 16:04:15 +08:00
|
|
|
import * as React from "react"
|
|
|
|
|
import { useTranslate } from "@workspace/i18n"
|
|
|
|
|
import { Button } from "@workspace/ui/components/button"
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from "@workspace/ui/components/dialog"
|
|
|
|
|
|
|
|
|
|
import { useMedia } from "./context"
|
|
|
|
|
import { MediaLibrary } from "./media-library"
|
|
|
|
|
import { mediaMessages } from "./messages"
|
|
|
|
|
import type { MediaAsset, MediaKind } from "./types"
|
|
|
|
|
import { defaultMediaReference } from "./utils"
|
|
|
|
|
|
|
|
|
|
const EMPTY_SELECTION: readonly MediaAsset[] = []
|
|
|
|
|
|
|
|
|
|
export interface MediaPickerDialogProps {
|
|
|
|
|
allowedKinds?: readonly MediaKind[]
|
|
|
|
|
initialSelection?: readonly MediaAsset[]
|
|
|
|
|
multiple?: boolean
|
|
|
|
|
open: boolean
|
|
|
|
|
onConfirm: (assets: readonly MediaAsset[]) => void
|
|
|
|
|
onOpenChange: (open: boolean) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function MediaPickerDialog({
|
|
|
|
|
allowedKinds,
|
|
|
|
|
initialSelection = EMPTY_SELECTION,
|
|
|
|
|
multiple = false,
|
|
|
|
|
open,
|
|
|
|
|
onConfirm,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
}: MediaPickerDialogProps) {
|
|
|
|
|
const { adapter } = useMedia()
|
|
|
|
|
const t = useTranslate()
|
|
|
|
|
const [selected, setSelected] =
|
|
|
|
|
React.useState<readonly MediaAsset[]>(initialSelection)
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (open) setSelected(initialSelection)
|
|
|
|
|
}, [initialSelection, open])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<DialogContent
|
|
|
|
|
className="flex h-[min(88dvh,54rem)] max-h-[calc(100dvh-2rem)] flex-col gap-3 p-4 sm:max-w-[min(94vw,86rem)]"
|
|
|
|
|
showCloseButton={false}
|
|
|
|
|
>
|
|
|
|
|
<DialogHeader className="sr-only">
|
|
|
|
|
<DialogTitle>{t(mediaMessages.selectMedia)}</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
{t(mediaMessages.selectMediaDescription)}
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<MediaLibrary
|
|
|
|
|
allowedKinds={allowedKinds}
|
|
|
|
|
selectionMode={multiple ? "multiple" : "single"}
|
|
|
|
|
selectedAssets={selected}
|
|
|
|
|
onSelectionChange={setSelected}
|
|
|
|
|
className="min-h-0 flex-1"
|
|
|
|
|
footer={
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
{t(mediaMessages.selectedCount, { count: selected.length })}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => onOpenChange(false)}
|
|
|
|
|
>
|
|
|
|
|
{t(mediaMessages.cancel)}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={selected.length === 0}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onConfirm(
|
|
|
|
|
selected.map((asset) => {
|
|
|
|
|
const url =
|
|
|
|
|
adapter.resolveReference?.(asset) ??
|
|
|
|
|
defaultMediaReference(asset)
|
2026-09-20 15:53:02 +08:00
|
|
|
return url === asset.url
|
|
|
|
|
? asset
|
|
|
|
|
: Object.assign({}, asset, { url })
|
2026-07-31 16:04:15 +08:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
onOpenChange(false)
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t(mediaMessages.useSelected)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)
|
|
|
|
|
}
|