diff options
Diffstat (limited to 'apps/web/src/project')
| -rw-r--r-- | apps/web/src/project/components/delete-project-dialog.tsx | 84 | ||||
| -rw-r--r-- | apps/web/src/project/components/migration-dialog.tsx | 44 | ||||
| -rw-r--r-- | apps/web/src/project/components/project-info-dialog.tsx | 81 | ||||
| -rw-r--r-- | apps/web/src/project/components/rename-project-dialog.tsx | 72 | ||||
| -rw-r--r-- | apps/web/src/project/types.ts | 54 |
5 files changed, 335 insertions, 0 deletions
diff --git a/apps/web/src/project/components/delete-project-dialog.tsx b/apps/web/src/project/components/delete-project-dialog.tsx new file mode 100644 index 00000000..0086384e --- /dev/null +++ b/apps/web/src/project/components/delete-project-dialog.tsx @@ -0,0 +1,84 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogBody, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; + +export function DeleteProjectDialog({ + isOpen, + onOpenChange, + onConfirm, + projectNames, +}: { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: () => void; + projectNames: string[]; +}) { + const count = projectNames.length; + const isSingle = count === 1; + const singleName = isSingle ? projectNames[0] : null; + + return ( + <Dialog open={isOpen} onOpenChange={onOpenChange}> + <DialogContent + onOpenAutoFocus={(event) => { + event.preventDefault(); + event.stopPropagation(); + }} + > + <DialogHeader> + <DialogTitle> + {singleName ? ( + <> + {"Delete '"} + <span className="inline-block max-w-[300px] truncate align-bottom"> + {singleName} + </span> + {"'?"} + </> + ) : ( + `Delete ${count} projects?` + )} + </DialogTitle> + </DialogHeader> + <DialogBody> + <Alert variant="destructive"> + <AlertTitle>Warning</AlertTitle> + <AlertDescription> + This will permanently delete{" "} + {singleName ? `"${singleName}"` : `${count} projects`} and all + associated files. + </AlertDescription> + </Alert> + <div className="flex flex-col gap-3"> + <Label className="text-xs font-semibold text-slate-500"> + Type "DELETE" to confirm + </Label> + <Input + type="text" + placeholder="DELETE" + size="lg" + variant="destructive" + /> + </div> + </DialogBody> + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + Cancel + </Button> + <Button variant="destructive" onClick={onConfirm}> + Delete project + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/src/project/components/migration-dialog.tsx b/apps/web/src/project/components/migration-dialog.tsx new file mode 100644 index 00000000..4aac26d5 --- /dev/null +++ b/apps/web/src/project/components/migration-dialog.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useEditor } from "@/editor/use-editor"; +import { Loader2 } from "lucide-react"; + +export function MigrationDialog() { + const editor = useEditor(); + const migrationState = editor.project.getMigrationState(); + + if (!migrationState.isMigrating) return null; + + const title = migrationState.projectName + ? "Updating project" + : "Updating projects"; + const description = migrationState.projectName + ? `Upgrading "${migrationState.projectName}" from v${migrationState.fromVersion} to v${migrationState.toVersion}` + : `Upgrading projects from v${migrationState.fromVersion} to v${migrationState.toVersion}`; + + return ( + <Dialog open={true}> + <DialogContent + className="sm:max-w-md" + onPointerDownOutside={(event) => event.preventDefault()} + onEscapeKeyDown={(event) => event.preventDefault()} + > + <DialogHeader> + <DialogTitle>{title}</DialogTitle> + <DialogDescription>{description}</DialogDescription> + </DialogHeader> + + <div className="flex items-center justify-center py-4"> + <Loader2 className="text-muted-foreground size-8 animate-spin" /> + </div> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/src/project/components/project-info-dialog.tsx b/apps/web/src/project/components/project-info-dialog.tsx new file mode 100644 index 00000000..f0ea364e --- /dev/null +++ b/apps/web/src/project/components/project-info-dialog.tsx @@ -0,0 +1,81 @@ +import { + Dialog, + DialogBody, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import type { TProjectMetadata } from "@/project/types"; +import { formatDate } from "@/utils/date"; +import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm"; +import { Button } from "@/components/ui/button"; + +function InfoRow({ + label, + value, +}: { + label: string; + value: string | React.ReactNode; +}) { + return ( + <div className="flex justify-between items-center py-0 last:pb-0"> + <span className="text-muted-foreground text-sm">{label}</span> + <span className="text-sm font-medium">{value}</span> + </div> + ); +} + +export function ProjectInfoDialog({ + isOpen, + onOpenChange, + project, +}: { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + project: TProjectMetadata; +}) { + const durationSeconds = mediaTimeToSeconds({ time: project.duration }); + const durationFormatted = + project.duration > 0 + ? (formatTimecode({ time: project.duration, format: durationSeconds >= 3600 ? "HH:MM:SS" : "MM:SS" }) ?? "") + : "0:00"; + + return ( + <Dialog open={isOpen} onOpenChange={onOpenChange}> + <DialogContent onOpenAutoFocus={(event) => event.preventDefault()}> + <DialogHeader> + <DialogTitle className="truncate max-w-[350px]"> + {project.name} + </DialogTitle> + </DialogHeader> + + <DialogBody className="flex flex-col"> + <InfoRow label="Duration" value={durationFormatted} /> + <InfoRow + label="Created" + value={formatDate({ date: project.createdAt })} + /> + <InfoRow + label="Modified" + value={formatDate({ date: project.updatedAt })} + /> + <InfoRow + label="Project ID" + value={ + <code className="text-xs bg-muted px-1.5 py-0.5 rounded"> + {project.id.slice(0, 8)} + </code> + } + /> + </DialogBody> + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + Close + </Button> + <Button onClick={() => onOpenChange(false)}>Done</Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/src/project/components/rename-project-dialog.tsx b/apps/web/src/project/components/rename-project-dialog.tsx new file mode 100644 index 00000000..73024d71 --- /dev/null +++ b/apps/web/src/project/components/rename-project-dialog.tsx @@ -0,0 +1,72 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogBody, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { useState } from "react"; +import { Label } from "@/components/ui/label"; + +export function RenameProjectDialog({ + isOpen, + onOpenChange, + onConfirm, + projectName, +}: { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: (newName: string) => void; + projectName: string; +}) { + const [name, setName] = useState(projectName); + + const handleOpenChange = (open: boolean) => { + if (open) { + setName(projectName); + } + onOpenChange(open); + }; + + return ( + <Dialog open={isOpen} onOpenChange={handleOpenChange}> + <DialogContent> + <DialogHeader> + <DialogTitle>Rename project</DialogTitle> + </DialogHeader> + + <DialogBody className="gap-3"> + <Label>New name</Label> + <Input + value={name} + onChange={(e) => setName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + onConfirm(name); + } + }} + placeholder="Enter a new name" + /> + </DialogBody> + + <DialogFooter> + <Button + variant="outline" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + onOpenChange(false); + }} + > + Cancel + </Button> + <Button onClick={() => onConfirm(name)}>Rename</Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/src/project/types.ts b/apps/web/src/project/types.ts new file mode 100644 index 00000000..42e844f6 --- /dev/null +++ b/apps/web/src/project/types.ts @@ -0,0 +1,54 @@ +import type { FrameRate } from "opencut-wasm"; +import type { TScene } from "@/timeline/types"; + +export type TBackground = + | { + type: "color"; + color: string; + } + | { + type: "blur"; + blurIntensity: number; + }; + +export interface TCanvasSize { + width: number; + height: number; +} + +export interface TProjectMetadata { + id: string; + name: string; + thumbnail?: string; + duration: number; + createdAt: Date; + updatedAt: Date; +} + +export interface TProjectSettings { + fps: FrameRate; + canvasSize: TCanvasSize; + canvasSizeMode?: "preset" | "custom"; + lastCustomCanvasSize?: TCanvasSize | null; + originalCanvasSize?: TCanvasSize | null; + background: TBackground; +} + +export interface TTimelineViewState { + zoomLevel: number; + scrollLeft: number; + playheadTime: number; +} + +export interface TProject { + metadata: TProjectMetadata; + scenes: TScene[]; + currentSceneId: string; + settings: TProjectSettings; + version: number; + timelineViewState?: TTimelineViewState; +} + +export type TProjectSortKey = "createdAt" | "updatedAt" | "name" | "duration"; +export type TSortOrder = "asc" | "desc"; +export type TProjectSortOption = `${TProjectSortKey}-${TSortOrder}`; |
