diff options
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/components/editor/preview-panel.tsx | 22 | ||||
| -rw-r--r-- | apps/web/src/components/editor/scenes-view.tsx | 262 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx | 14 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline/timeline-toolbar.tsx | 6 | ||||
| -rw-r--r-- | apps/web/src/components/providers/migrators/scenes-migrator.tsx | 12 | ||||
| -rw-r--r-- | apps/web/src/components/ui/button.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/components/ui/dialog.tsx | 4 | ||||
| -rw-r--r-- | apps/web/src/components/ui/split-button.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/hooks/use-frame-cache.ts | 60 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/storage-service.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/stores/project-store.ts | 8 | ||||
| -rw-r--r-- | apps/web/src/stores/scene-store.ts | 113 | ||||
| -rw-r--r-- | apps/web/src/types/project.ts | 1 |
13 files changed, 364 insertions, 144 deletions
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 8b1abd81..36f6a2ba 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -14,6 +14,7 @@ import { cn } from "@/lib/utils"; import { formatTimeCode } from "@/lib/time"; import { EditableTimecode } from "@/components/ui/editable-timecode"; import { useFrameCache } from "@/hooks/use-frame-cache"; +import { useSceneStore } from "@/stores/scene-store"; import { DEFAULT_CANVAS_SIZE, DEFAULT_FPS, @@ -43,6 +44,7 @@ export function PreviewPanel() { const { currentTime, toggle, setCurrentTime } = usePlaybackStore(); const { isPlaying, volume, muted } = usePlaybackStore(); const { activeProject } = useProjectStore(); + const { currentScene } = useSceneStore(); const previewRef = useRef<HTMLDivElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null); const { getCachedFrame, cacheFrame, invalidateCache, preRenderNearbyFrames } = @@ -219,11 +221,10 @@ export function PreviewPanel() { }; }, [dragState, previewDimensions, canvasSize, updateTextElement]); - // Invalidate cache when timeline changes + // Clear the frame cache when background settings change since they affect rendering useEffect(() => { invalidateCache(); }, [ - tracks, mediaFiles, activeProject?.backgroundColor, activeProject?.backgroundType, @@ -494,7 +495,8 @@ export function PreviewPanel() { currentTime, tracks, mediaFiles, - activeProject + activeProject, + currentScene?.id ); if (cachedFrame) { mainCtx.putImageData(cachedFrame, 0, 0); @@ -532,7 +534,9 @@ export function PreviewPanel() { }); return tempCtx.getImageData(0, 0, displayWidth, displayHeight); - } + }, + currentScene?.id, + 3 ); } else { // Small lookahead while playing @@ -567,6 +571,7 @@ export function PreviewPanel() { return tempCtx.getImageData(0, 0, displayWidth, displayHeight); }, + currentScene?.id, 1 ); } @@ -644,7 +649,14 @@ export function PreviewPanel() { displayWidth, displayHeight ); - cacheFrame(currentTime, imageData, tracks, mediaFiles, activeProject); + cacheFrame( + currentTime, + imageData, + tracks, + mediaFiles, + activeProject, + currentScene?.id + ); // Blit offscreen to visible canvas mainCtx.clearRect(0, 0, displayWidth, displayHeight); diff --git a/apps/web/src/components/editor/scenes-view.tsx b/apps/web/src/components/editor/scenes-view.tsx index f1df32e9..b5432b53 100644 --- a/apps/web/src/components/editor/scenes-view.tsx +++ b/apps/web/src/components/editor/scenes-view.tsx @@ -1,64 +1,198 @@ -"use client";
-
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetHeader,
- SheetTitle,
- SheetTrigger,
-} from "@/components/ui/sheet";
-import { Button } from "@/components/ui/button";
-import { useSceneStore } from "@/stores/scene-store";
-import { Check } from "lucide-react";
-
-export function ScenesView({ children }: { children: React.ReactNode }) {
- const { scenes, currentScene, switchToScene } = useSceneStore();
-
- const handleSceneSwitch = async (sceneId: string) => {
- try {
- await switchToScene({ sceneId });
- } catch (error) {
- console.error("Failed to switch scene:", error);
- }
- };
-
- return (
- <Sheet>
- <SheetTrigger asChild>{children}</SheetTrigger>
- <SheetContent>
- <SheetHeader>
- <SheetTitle>Scenes</SheetTitle>
- <SheetDescription>
- Switch between scenes in your project
- </SheetDescription>
- </SheetHeader>
- <div className="py-4">
- {scenes.length === 0 ? (
- <div className="text-sm text-muted-foreground">
- No scenes available
- </div>
- ) : (
- <div className="space-y-2">
- {scenes.map((scene) => (
- <Button
- key={scene.id}
- variant={
- currentScene?.id === scene.id ? "default" : "outline"
- }
- className="w-full justify-between"
- onClick={() => handleSceneSwitch(scene.id)}
- >
- <span>{scene.name}</span>
- {currentScene?.id === scene.id && (
- <Check className="h-4 w-4" />
- )}
- </Button>
- ))}
- </div>
- )}
- </div>
- </SheetContent>
- </Sheet>
- );
-}
+"use client"; + +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { Button } from "@/components/ui/button"; +import { useSceneStore } from "@/stores/scene-store"; +import { Check, ListCheck, Trash2 } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, + DialogTrigger, +} from "@/components/ui/dialog"; + +export function ScenesView({ children }: { children: React.ReactNode }) { + const { scenes, currentScene, switchToScene, deleteScene } = useSceneStore(); + const [isSelectMode, setIsSelectMode] = useState(false); + const [selectedScenes, setSelectedScenes] = useState<Set<string>>(new Set()); + + const handleSceneSwitch = async (sceneId: string) => { + if (isSelectMode) { + toggleSceneSelection(sceneId); + return; + } + + try { + await switchToScene({ sceneId }); + } catch (error) { + console.error("Failed to switch scene:", error); + } + }; + + const toggleSceneSelection = (sceneId: string) => { + setSelectedScenes((prev) => { + const newSet = new Set(prev); + if (newSet.has(sceneId)) { + newSet.delete(sceneId); + } else { + newSet.add(sceneId); + } + return newSet; + }); + }; + + const handleSelectMode = () => { + setIsSelectMode(!isSelectMode); + setSelectedScenes(new Set()); + }; + + const handleDeleteSelected = async () => { + for (const sceneId of selectedScenes) { + const scene = scenes.find((s) => s.id === sceneId); + if (scene && !scene.isMain) { + try { + await deleteScene({ sceneId }); + } catch (error) { + console.error("Failed to delete scene:", error); + } + } + } + setSelectedScenes(new Set()); + setIsSelectMode(false); + }; + + return ( + <Sheet> + <SheetTrigger asChild>{children}</SheetTrigger> + <SheetContent> + <SheetHeader> + <SheetTitle> + {isSelectMode ? `Select scenes (${selectedScenes.size})` : "Scenes"} + </SheetTitle> + <SheetDescription> + {isSelectMode + ? "Select scenes to delete" + : "Switch between scenes in your project"} + </SheetDescription> + </SheetHeader> + <div className="py-4 flex flex-col gap-4"> + <div className="flex items-center gap-2"> + <Button + className="rounded-md" + variant={isSelectMode ? "default" : "outline"} + size="sm" + onClick={handleSelectMode} + > + <ListCheck /> + {isSelectMode ? "Cancel" : "Select"} + </Button> + {isSelectMode && ( + <DeleteDialog + count={selectedScenes.size} + onDelete={handleDeleteSelected} + disabled={Array.from(selectedScenes).some( + (id) => scenes.find((s) => s.id === id)?.isMain + )} + > + <Button className="rounded-md" variant="destructive" size="sm"> + <Trash2 /> + Delete ({selectedScenes.size}) + </Button> + </DeleteDialog> + )} + </div> + {scenes.length === 0 ? ( + <div className="text-sm text-muted-foreground"> + No scenes available + </div> + ) : ( + <div className="space-y-2"> + {scenes.map((scene) => ( + <Button + key={scene.id} + variant="outline" + className={cn( + "w-full justify-between font-normal", + currentScene?.id === scene.id && + !isSelectMode && + "border-primary !text-primary", + isSelectMode && + selectedScenes.has(scene.id) && + "bg-accent border-foreground/30" + )} + onClick={() => handleSceneSwitch(scene.id)} + > + <span>{scene.name}</span> + <div className="flex items-center gap-2"> + {((isSelectMode && selectedScenes.has(scene.id)) || + (!isSelectMode && currentScene?.id === scene.id)) && ( + <Check className="h-4 w-4" /> + )} + </div> + </Button> + ))} + </div> + )} + </div> + </SheetContent> + </Sheet> + ); +} + +function DeleteDialog({ + count, + onDelete, + disabled, + children, +}: { + count: number; + onDelete: () => void; + disabled?: boolean; + children: React.ReactNode; +}) { + const [open, setOpen] = useState(false); + + const handleDelete = () => { + onDelete(); + setOpen(false); + }; + + return ( + <Dialog open={open} onOpenChange={setOpen}> + <DialogTrigger asChild>{children}</DialogTrigger> + <DialogContent> + <DialogHeader> + <DialogTitle>Delete Scenes</DialogTitle> + <DialogDescription> + Are you sure you want to delete {count} scene + {count === 1 ? "" : "s"}? This action cannot be undone. + </DialogDescription> + </DialogHeader> + <DialogFooter> + <Button variant="outline" onClick={() => setOpen(false)}> + Cancel + </Button> + <Button + variant="destructive" + onClick={handleDelete} + disabled={disabled} + > + Delete + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx b/apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx index ef667d67..7def2985 100644 --- a/apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx +++ b/apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx @@ -5,6 +5,7 @@ import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { TimelineTrack } from "@/types/timeline"; import { MediaFile } from "@/types/media"; import { TProject } from "@/types/project"; +import { useSceneStore } from "@/stores/scene-store"; interface CacheSegment { startTime: number; @@ -22,7 +23,8 @@ interface TimelineCacheIndicatorProps { time: number, tracks: TimelineTrack[], mediaFiles: MediaFile[], - activeProject: TProject | null + activeProject: TProject | null, + sceneId?: string ) => "cached" | "not-cached"; } @@ -34,6 +36,8 @@ export function TimelineCacheIndicator({ activeProject, getRenderStatus, }: TimelineCacheIndicatorProps) { + const { currentScene } = useSceneStore(); + // Calculate cache segments by sampling the timeline const calculateCacheSegments = (): CacheSegment[] => { const segments: CacheSegment[] = []; @@ -49,7 +53,13 @@ export function TimelineCacheIndicator({ for (let i = 0; i <= totalSamples; i++) { const time = i / sampleRate; const cached = - getRenderStatus(time, tracks, mediaFiles, activeProject) === "cached"; + getRenderStatus( + time, + tracks, + mediaFiles, + activeProject, + currentScene?.id + ) === "cached"; if (!currentSegment) { // Start first segment diff --git a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx index 7eeb84e4..79b5d5b2 100644 --- a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx +++ b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx @@ -67,7 +67,7 @@ export function TimelineToolbar({ } = useTimelineStore(); const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore(); const { toggleBookmark, isBookmarked, activeProject } = useProjectStore(); - const { currentScene } = useSceneStore(); + const { scenes, currentScene } = useSceneStore(); const handleSplitSelected = () => { if (selectedElements.length === 0) return; @@ -359,11 +359,11 @@ export function TimelineToolbar({ </TooltipProvider> </div> <div> - <SplitButton> + <SplitButton className="border border-foreground/10"> <SplitButtonLeft>{currentScene?.name || "No Scene"}</SplitButtonLeft> <SplitButtonSeparator /> <ScenesView> - <SplitButtonRight onClick={() => {}}> + <SplitButtonRight disabled={scenes.length === 1} onClick={() => {}}> <LayersIcon /> </SplitButtonRight> </ScenesView> diff --git a/apps/web/src/components/providers/migrators/scenes-migrator.tsx b/apps/web/src/components/providers/migrators/scenes-migrator.tsx index 521e24af..b984a7f4 100644 --- a/apps/web/src/components/providers/migrators/scenes-migrator.tsx +++ b/apps/web/src/components/providers/migrators/scenes-migrator.tsx @@ -92,23 +92,13 @@ export function ScenesMigrator({ children }: { children: React.ReactNode }) { id: generateUUID(), name: "Main Scene", isMain: true, - isBackground: false, - createdAt: new Date(), - updatedAt: new Date(), - }; - - const backgroundScene: Scene = { - id: generateUUID(), - name: "Background", - isMain: false, - isBackground: true, createdAt: new Date(), updatedAt: new Date(), }; const migratedProject: TProject = { ...project, - scenes: [mainScene, backgroundScene], + scenes: [mainScene], currentSceneId: mainScene.id, updatedAt: new Date(), }; diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx index 3d17c042..56a10f44 100644 --- a/apps/web/src/components/ui/button.tsx +++ b/apps/web/src/components/ui/button.tsx @@ -16,7 +16,7 @@ const buttonVariants = cva( "primary-gradient": "bg-gradient-to-r from-cyan-400 to-blue-500 text-white hover:opacity-85 transition-opacity", destructive: - "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", + "bg-destructive/0 border border-destructive/25 text-destructive shadow-xs hover:bg-destructive hover:text-destructive-foreground", outline: "border border-input bg-transparent shadow-xs hover:opacity-75 transition-opacity hover:text-accent-foreground", secondary: diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx index ad7e825b..42d0f80d 100644 --- a/apps/web/src/components/ui/dialog.tsx +++ b/apps/web/src/components/ui/dialog.tsx @@ -22,7 +22,7 @@ const DialogOverlay = React.forwardRef< <DialogPrimitive.Overlay ref={ref} className={cn( - "fixed inset-0 z-150 bg-black/20 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + "fixed inset-0 z-250 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", className )} {...props} @@ -39,7 +39,7 @@ const DialogContent = React.forwardRef< <DialogPrimitive.Content ref={ref} className={cn( - "fixed left-[50%] top-[50%] p-6 z-150 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-popover shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-lg", + "fixed left-[50%] top-[50%] p-6 z-250 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-popover shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-lg", className )} onCloseAutoFocus={(e) => { diff --git a/apps/web/src/components/ui/split-button.tsx b/apps/web/src/components/ui/split-button.tsx index 1ef29500..ca3b518c 100644 --- a/apps/web/src/components/ui/split-button.tsx +++ b/apps/web/src/components/ui/split-button.tsx @@ -39,7 +39,7 @@ const SplitButtonSide = forwardRef< ref={ref}
variant="text"
className={cn(
- "h-full rounded-none bg-panel-accent !opacity-100 border-0 gap-0 font-normal transition-colors",
+ "h-full rounded-none bg-panel-accent !opacity-100 border-0 gap-0 font-normal transition-colors disabled:text-muted-foreground",
onClick
? "hover:bg-foreground/10 hover:opacity-100 cursor-pointer"
: "cursor-default select-text",
diff --git a/apps/web/src/hooks/use-frame-cache.ts b/apps/web/src/hooks/use-frame-cache.ts index 2abfb912..cce187f1 100644 --- a/apps/web/src/hooks/use-frame-cache.ts +++ b/apps/web/src/hooks/use-frame-cache.ts @@ -37,7 +37,8 @@ export function useFrameCache(options: FrameCacheOptions = {}) { time: number, tracks: TimelineTrack[], mediaFiles: MediaFile[], - activeProject: TProject | null + activeProject: TProject | null, + sceneId?: string ): string => { // Get elements that are active at this time const activeElements: Array<{ @@ -117,11 +118,13 @@ export function useFrameCache(options: FrameCacheOptions = {}) { canvasSize: activeProject?.canvasSize, }; - return JSON.stringify({ + const hash = { activeElements, projectState, - time: Math.floor(time * cacheResolution) / cacheResolution, // Quantize time - }); + sceneId, + time: Math.floor(time * cacheResolution) / cacheResolution, + }; + return JSON.stringify(hash); }, [cacheResolution] ); @@ -132,7 +135,8 @@ export function useFrameCache(options: FrameCacheOptions = {}) { time: number, tracks: TimelineTrack[], mediaFiles: MediaFile[], - activeProject: TProject | null + activeProject: TProject | null, + sceneId?: string ): boolean => { const frameKey = Math.floor(time * cacheResolution); const cached = frameCacheRef.current.get(frameKey); @@ -143,7 +147,8 @@ export function useFrameCache(options: FrameCacheOptions = {}) { time, tracks, mediaFiles, - activeProject + activeProject, + sceneId ); return cached.timelineHash === currentHash; }, @@ -156,21 +161,33 @@ export function useFrameCache(options: FrameCacheOptions = {}) { time: number, tracks: TimelineTrack[], mediaFiles: MediaFile[], - activeProject: TProject | null + activeProject: TProject | null, + sceneId?: string ): ImageData | null => { const frameKey = Math.floor(time * cacheResolution); const cached = frameCacheRef.current.get(frameKey); - if (!cached) return null; + if (!cached) { + return null; + } const currentHash = getTimelineHash( time, tracks, mediaFiles, - activeProject + activeProject, + sceneId ); + console.log(cached.timelineHash === currentHash); if (cached.timelineHash !== currentHash) { // Cache is stale, remove it + console.log( + "Cache miss - hash mismatch:", + JSON.stringify({ + cachedHash: cached.timelineHash.slice(0, 100), + currentHash: currentHash.slice(0, 100), + }) + ); frameCacheRef.current.delete(frameKey); return null; } @@ -187,14 +204,16 @@ export function useFrameCache(options: FrameCacheOptions = {}) { imageData: ImageData, tracks: TimelineTrack[], mediaFiles: MediaFile[], - activeProject: TProject | null + activeProject: TProject | null, + sceneId?: string ): void => { const frameKey = Math.floor(time * cacheResolution); const timelineHash = getTimelineHash( time, tracks, mediaFiles, - activeProject + activeProject, + sceneId ); // Enforce cache size limit (LRU eviction) @@ -230,9 +249,10 @@ export function useFrameCache(options: FrameCacheOptions = {}) { time: number, tracks: TimelineTrack[], mediaFiles: MediaFile[], - activeProject: TProject | null + activeProject: TProject | null, + sceneId?: string ): "cached" | "not-cached" => { - return isFrameCached(time, tracks, mediaFiles, activeProject) + return isFrameCached(time, tracks, mediaFiles, activeProject, sceneId) ? "cached" : "not-cached"; }, @@ -247,6 +267,7 @@ export function useFrameCache(options: FrameCacheOptions = {}) { mediaFiles: MediaFile[], activeProject: TProject | null, renderFunction: (time: number) => Promise<ImageData>, + sceneId?: string, range: number = 3 // seconds ) => { const framesToPreRender: number[] = []; @@ -260,7 +281,7 @@ export function useFrameCache(options: FrameCacheOptions = {}) { const time = currentTime + offset; if (time < 0) continue; - if (!isFrameCached(time, tracks, mediaFiles, activeProject)) { + if (!isFrameCached(time, tracks, mediaFiles, activeProject, sceneId)) { framesToPreRender.push(time); } } @@ -276,7 +297,7 @@ export function useFrameCache(options: FrameCacheOptions = {}) { for (let k = 0; k < cacheResolution; k++) { const t = s + k / cacheResolution; if (t < 0) continue; - if (!isFrameCached(t, tracks, mediaFiles, activeProject)) { + if (!isFrameCached(t, tracks, mediaFiles, activeProject, sceneId)) { expandedTimes.push(t); } } @@ -298,7 +319,14 @@ export function useFrameCache(options: FrameCacheOptions = {}) { requestIdleCallback(async () => { try { const imageData = await renderFunction(time); - cacheFrame(time, imageData, tracks, mediaFiles, activeProject); + cacheFrame( + time, + imageData, + tracks, + mediaFiles, + activeProject, + sceneId + ); } catch (error) { console.warn(`Pre-render failed for time ${time}:`, error); } diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts index 112d09b6..2688f6e8 100644 --- a/apps/web/src/lib/storage/storage-service.ts +++ b/apps/web/src/lib/storage/storage-service.ts @@ -78,7 +78,6 @@ class StorageService { id: scene.id, name: scene.name, isMain: scene.isMain, - isBackground: scene.isBackground, createdAt: scene.createdAt.toISOString(), updatedAt: scene.updatedAt.toISOString(), })); @@ -114,7 +113,6 @@ class StorageService { id: scene.id, name: scene.name, isMain: scene.isMain, - isBackground: scene.isBackground || false, // Default for legacy scenes createdAt: new Date(scene.createdAt), updatedAt: new Date(scene.updatedAt), })) || []; diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 0c67be9f..c58f7309 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -4,7 +4,7 @@ import { storageService } from "@/lib/storage/storage-service"; import { toast } from "sonner"; import { useMediaStore } from "./media-store"; import { useTimelineStore } from "./timeline-store"; -import { createBackgroundScene, useSceneStore } from "./scene-store"; +import { useSceneStore } from "./scene-store"; import { generateUUID } from "@/lib/utils"; import { CanvasSize, CanvasMode } from "@/types/editor"; @@ -14,9 +14,8 @@ export const DEFAULT_FPS = 30; export function createMainScene(): Scene { return { id: generateUUID(), - name: "Main scene", + name: "Main Scene", isMain: true, - isBackground: false, createdAt: new Date(), updatedAt: new Date(), }; @@ -24,7 +23,6 @@ export function createMainScene(): Scene { const createDefaultProject = (name: string): TProject => { const mainScene = createMainScene(); - const backgroundScene = createBackgroundScene(); return { id: generateUUID(), @@ -32,7 +30,7 @@ const createDefaultProject = (name: string): TProject => { thumbnail: "", createdAt: new Date(), updatedAt: new Date(), - scenes: [mainScene, backgroundScene], + scenes: [mainScene], currentSceneId: mainScene.id, backgroundColor: "#000000", backgroundType: "color", diff --git a/apps/web/src/stores/scene-store.ts b/apps/web/src/stores/scene-store.ts index 1b789427..a9f65a49 100644 --- a/apps/web/src/stores/scene-store.ts +++ b/apps/web/src/stores/scene-store.ts @@ -9,29 +9,17 @@ export function getMainScene({ scenes }: { scenes: Scene[] }): Scene | null { return scenes.find((scene) => scene.isMain) || null; } -export function getBackgroundScene({ - scenes, -}: { - scenes: Scene[]; -}): Scene | null { - return scenes.find((scene) => scene.isBackground) || null; -} - -export function createBackgroundScene(): Scene { - return { - id: generateUUID(), - name: "Background", - isMain: false, - isBackground: true, - createdAt: new Date(), - updatedAt: new Date(), - }; -} - -function ensureBackgroundScene({ scenes }: { scenes: Scene[] }): Scene[] { - const hasBackground = scenes.some((scene) => scene.isBackground); - if (!hasBackground) { - return [...scenes, createBackgroundScene()]; +function ensureMainScene(scenes: Scene[]): Scene[] { + const hasMain = scenes.some((scene) => scene.isMain); + if (!hasMain) { + const mainScene: Scene = { + id: generateUUID(), + name: "Main scene", + isMain: true, + createdAt: new Date(), + updatedAt: new Date(), + }; + return [mainScene, ...scenes]; } return scenes; } @@ -49,6 +37,7 @@ interface SceneStore { name: string; isMain: boolean; }) => Promise<string>; + deleteScene: ({ sceneId }: { sceneId: string }) => Promise<void>; renameScene: ({ sceneId, name, @@ -115,6 +104,62 @@ export const useSceneStore = create<SceneStore>((set, get) => ({ } }, + deleteScene: async ({ sceneId }: { sceneId: string }) => { + const { scenes, currentScene } = get(); + const sceneToDelete = scenes.find((s) => s.id === sceneId); + + if (!sceneToDelete) { + throw new Error("Scene not found"); + } + + if (sceneToDelete.isMain) { + throw new Error("Cannot delete main scene"); + } + + const updatedScenes = scenes.filter((s) => s.id !== sceneId); + + // Determine new current scene if we're deleting the current one + let newCurrentScene = currentScene; + if (currentScene?.id === sceneId) { + newCurrentScene = getMainScene({ scenes: updatedScenes }); + } + + // Update project + const projectStore = useProjectStore.getState(); + const { activeProject } = projectStore; + + if (!activeProject) { + throw new Error("No active project"); + } + + const updatedProject = { + ...activeProject, + scenes: updatedScenes, + updatedAt: new Date(), + }; + + try { + await storageService.saveProject({ project: updatedProject }); + useProjectStore.setState({ activeProject: updatedProject }); + set({ + scenes: updatedScenes, + currentScene: newCurrentScene, + }); + + // If we switched scenes, load the new scene's timeline + if (newCurrentScene && newCurrentScene.id !== currentScene?.id) { + const timelineStore = useTimelineStore.getState(); + await timelineStore.loadProjectTimeline({ + projectId: activeProject.id, + sceneId: newCurrentScene.id, + }); + } + } catch (error) { + console.error("Failed to delete scene:", error); + throw error; + } + }, + renameScene: async ({ sceneId, name }: { sceneId: string; name: string }) => { const { scenes } = get(); const updatedScenes = scenes.map((scene) => @@ -226,7 +271,7 @@ export const useSceneStore = create<SceneStore>((set, get) => ({ scenes: Scene[]; currentSceneId?: string; }) => { - const ensuredScenes = ensureBackgroundScene({ scenes }); + const ensuredScenes = ensureMainScene(scenes); const currentScene = currentSceneId ? ensuredScenes.find((s) => s.id === currentSceneId) : null; @@ -241,19 +286,25 @@ export const useSceneStore = create<SceneStore>((set, get) => ({ if (ensuredScenes.length > scenes.length) { const projectStore = useProjectStore.getState(); const { activeProject } = projectStore; - + if (activeProject) { const updatedProject = { ...activeProject, scenes: ensuredScenes, updatedAt: new Date(), }; - - storageService.saveProject({ project: updatedProject }).then(() => { - useProjectStore.setState({ activeProject: updatedProject }); - }).catch(error => { - console.error("Failed to save project with background scene:", error); - }); + + storageService + .saveProject({ project: updatedProject }) + .then(() => { + useProjectStore.setState({ activeProject: updatedProject }); + }) + .catch((error) => { + console.error( + "Failed to save project with background scene:", + error + ); + }); } } }, diff --git a/apps/web/src/types/project.ts b/apps/web/src/types/project.ts index 7fa53c69..f182653d 100644 --- a/apps/web/src/types/project.ts +++ b/apps/web/src/types/project.ts @@ -6,7 +6,6 @@ export interface Scene { id: string; name: string; isMain: boolean; - isBackground: boolean; createdAt: Date; updatedAt: Date; } |
