193 lines
6.4 KiB
TypeScript
193 lines
6.4 KiB
TypeScript
import { Trans, useLingui } from "@lingui/react/macro";
|
|
import type { MediaProject } from "@rakazo/contracts";
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogTitle,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
Tabs,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from "@rakazo/ui-web";
|
|
import { ChevronDown, ChevronLeft, X } from "lucide-react";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { MediaStudioContext, useMediaCapabilities } from "../../lib/media-studio";
|
|
import { rpc } from "../../lib/rpc";
|
|
import { BrandKitView } from "./BrandKitView";
|
|
import { CollectionView } from "./CollectionView";
|
|
import { EditorView } from "./EditorView";
|
|
import { LibraryView } from "./LibraryView";
|
|
import type { MediaSection, MediaView } from "./nav";
|
|
import { pushView, sectionOf } from "./nav";
|
|
import { PeopleView } from "./PeopleView";
|
|
import { ProjectView } from "./ProjectView";
|
|
import { ResultsView } from "./ResultsView";
|
|
import { ReviewView } from "./ReviewView";
|
|
|
|
function ViewSwitch({
|
|
view,
|
|
navigate,
|
|
onClose,
|
|
}: {
|
|
view: MediaView;
|
|
navigate: (view: MediaView) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const props = { navigate, onClose };
|
|
switch (view.name) {
|
|
case "library":
|
|
return <LibraryView {...props} mode={view.mode} />;
|
|
case "collection":
|
|
return <CollectionView {...props} collectionId={view.collectionId} />;
|
|
case "people":
|
|
return <PeopleView {...props} />;
|
|
case "results":
|
|
return <ResultsView {...props} />;
|
|
case "project":
|
|
return <ProjectView {...props} projectId={view.projectId} />;
|
|
case "editor":
|
|
return <EditorView {...props} projectId={view.projectId} planId={view.planId} />;
|
|
case "review":
|
|
return <ReviewView {...props} projectId={view.projectId} renderId={view.renderId} />;
|
|
case "brandKit":
|
|
return <BrandKitView {...props} brandKitId={view.brandKitId} />;
|
|
}
|
|
}
|
|
|
|
function ProjectsMenu({
|
|
navigate,
|
|
refreshKey,
|
|
}: {
|
|
navigate: (view: MediaView) => void;
|
|
refreshKey: unknown;
|
|
}) {
|
|
const [projects, setProjects] = useState<MediaProject[]>([]);
|
|
useEffect(() => {
|
|
void refreshKey;
|
|
let active = true;
|
|
rpc.media.projects
|
|
.list({ cursor: null, limit: 20 })
|
|
.then((page) => {
|
|
if (active) setProjects(page.items.filter((project) => project.status !== "archived"));
|
|
})
|
|
.catch(() => undefined);
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [refreshKey]);
|
|
if (projects.length === 0) return null;
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={<Button variant="ghost" size="sm" data-testid="media-projects-menu" />}
|
|
>
|
|
<Trans>Projects</Trans>
|
|
<ChevronDown data-icon="inline-end" aria-hidden="true" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="max-h-80 w-64 overflow-y-auto">
|
|
{projects.map((project) => (
|
|
<DropdownMenuItem
|
|
key={project.id}
|
|
onClick={() => navigate({ name: "project", projectId: project.id })}
|
|
>
|
|
<span className="truncate" dir="auto">
|
|
{project.title}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|
|
|
|
/** Media Studio surface: a full-window layer with its own back stack. */
|
|
export function MediaStudio({
|
|
onClose,
|
|
canLinkFolder = false,
|
|
}: {
|
|
onClose: () => void;
|
|
/** Deployment owner only: folder sources live on the host. */
|
|
canLinkFolder?: boolean;
|
|
}) {
|
|
const { t } = useLingui();
|
|
const capabilities = useMediaCapabilities();
|
|
const [stack, setStack] = useState<MediaView[]>([{ name: "library" }]);
|
|
const view = stack.at(-1) ?? { name: "library" };
|
|
const section = sectionOf(view);
|
|
const navigate = useCallback(
|
|
(next: MediaView) => setStack((current) => pushView(current, next)),
|
|
[],
|
|
);
|
|
const back = useCallback(
|
|
() => setStack((current) => (current.length > 1 ? current.slice(0, -1) : current)),
|
|
[],
|
|
);
|
|
const env = useMemo(() => ({ capabilities, canLinkFolder }), [capabilities, canLinkFolder]);
|
|
const notConfigured = capabilities !== null && !capabilities.configured;
|
|
|
|
return (
|
|
<Dialog
|
|
open
|
|
onOpenChange={(open) => {
|
|
if (!open) onClose();
|
|
}}
|
|
>
|
|
<DialogContent
|
|
data-testid="media-studio"
|
|
data-view={view.name}
|
|
showCloseButton={false}
|
|
className="inset-0 top-0 left-0 flex h-full w-full max-w-none translate-x-0 translate-y-0 flex-col gap-0 rounded-none bg-background p-0 text-foreground ring-0 sm:max-w-none"
|
|
>
|
|
<header className="flex items-center gap-2 border-b border-sidebar-border px-3 py-2.5 md:px-[18px]">
|
|
{stack.length > 1 ? (
|
|
<Button variant="ghost" size="icon-sm" aria-label={t`Back`} onClick={back}>
|
|
<ChevronLeft />
|
|
</Button>
|
|
) : null}
|
|
<DialogTitle className="text-ui-body font-medium text-foreground">
|
|
<Trans>Media</Trans>
|
|
</DialogTitle>
|
|
<div className="flex min-w-0 flex-1 justify-center">
|
|
<Tabs
|
|
value={section ?? ""}
|
|
onValueChange={(value) => navigate({ name: value as MediaSection })}
|
|
>
|
|
<TabsList variant="line" aria-label={t`Media Studio`}>
|
|
<TabsTrigger value="library">
|
|
<Trans>Library</Trans>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="results">
|
|
<Trans>Results</Trans>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="brandKit">
|
|
<Trans>Brand kit</Trans>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
</div>
|
|
<ProjectsMenu navigate={navigate} refreshKey={view} />
|
|
<DialogClose aria-label={t`Close`} render={<Button variant="ghost" size="icon-sm" />}>
|
|
<X />
|
|
</DialogClose>
|
|
</header>
|
|
<MediaStudioContext.Provider value={env}>
|
|
<main className="flex min-h-0 flex-1 flex-col">
|
|
{notConfigured ? (
|
|
<div className="grid flex-1 place-items-center px-8 text-center text-ui-body text-muted-foreground">
|
|
<Trans>Media Studio is not set up on this server.</Trans>
|
|
</div>
|
|
) : (
|
|
<ViewSwitch view={view} navigate={navigate} onClose={onClose} />
|
|
)}
|
|
</main>
|
|
</MediaStudioContext.Provider>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|