From 21dac9f09e17f4f1be4ddfad7474400787051a7c Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 31 Aug 2025 00:58:33 +0200 Subject: fix space key to play --- apps/web/src/stores/keybindings-store.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'apps/web/src/stores') diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts index 7fa0da5e..febcaa30 100644 --- a/apps/web/src/stores/keybindings-store.ts +++ b/apps/web/src/stores/keybindings-store.ts @@ -206,6 +206,9 @@ function getPressedKey(ev: KeyboardEvent): string | null { const key = (ev.key ?? "").toLowerCase(); const code = ev.code ?? ""; + if (code === "Space" || key === " " || key === "spacebar" || key === "space") + return "space"; + // Check arrow keys if (key.startsWith("arrow")) { return key.slice(5); @@ -213,7 +216,6 @@ function getPressedKey(ev: KeyboardEvent): string | null { // Check for special keys if (key === "tab") return "tab"; - if (key === " " || key === "space") return "space"; if (key === "home") return "home"; if (key === "end") return "end"; if (key === "delete") return "delete"; -- cgit v1.3.1 From d747fb458d2fbebd2ce007132b43cd973bd5513f Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 31 Aug 2025 13:40:41 +0200 Subject: feat: make timeline renderer 15x faster --- apps/web/src/lib/timeline-renderer.ts | 51 +++++----- apps/web/src/lib/video-cache.ts | 186 ++++++++++++++++++++++++++++++++++ apps/web/src/stores/media-store.ts | 5 + 3 files changed, 218 insertions(+), 24 deletions(-) create mode 100644 apps/web/src/lib/video-cache.ts (limited to 'apps/web/src/stores') diff --git a/apps/web/src/lib/timeline-renderer.ts b/apps/web/src/lib/timeline-renderer.ts index 6241a7d9..08785369 100644 --- a/apps/web/src/lib/timeline-renderer.ts +++ b/apps/web/src/lib/timeline-renderer.ts @@ -1,6 +1,6 @@ -import { Input, ALL_FORMATS, BlobSource, VideoSampleSink } from "mediabunny"; import type { TimelineTrack } from "@/types/timeline"; import type { MediaFile } from "@/types/media"; +import { videoCache } from "./video-cache"; export interface RenderContext { ctx: CanvasRenderingContext2D; @@ -65,31 +65,34 @@ export async function renderTimelineFrame({ for (const { element, mediaItem } of active) { if (element.type === "media" && mediaItem) { if (mediaItem.type === "video") { - const input = new Input({ - source: new BlobSource(mediaItem.file), - formats: ALL_FORMATS, - }); - const track = await input.getPrimaryVideoTrack(); - if (!track) continue; - const decodable = await track.canDecode(); - if (!decodable) continue; - const sink = new VideoSampleSink(track); + try { + const localTime = time - element.startTime + element.trimStart; - const localTime = time - element.startTime + element.trimStart; - const sample = await sink.getSample(localTime); - if (!sample) continue; + const frame = await videoCache.getFrameAt( + mediaItem.id, + mediaItem.file, + localTime + ); + if (!frame) continue; - const mediaW = Math.max(1, mediaItem.width || canvasWidth); - const mediaH = Math.max(1, mediaItem.height || canvasHeight); - const containScale = Math.min( - canvasWidth / mediaW, - canvasHeight / mediaH - ); - const drawW = mediaW * containScale; - const drawH = mediaH * containScale; - const drawX = (canvasWidth - drawW) / 2; - const drawY = (canvasHeight - drawH) / 2; - sample.draw(ctx, drawX, drawY, drawW, drawH); + const mediaW = Math.max(1, mediaItem.width || canvasWidth); + const mediaH = Math.max(1, mediaItem.height || canvasHeight); + const containScale = Math.min( + canvasWidth / mediaW, + canvasHeight / mediaH + ); + const drawW = mediaW * containScale; + const drawH = mediaH * containScale; + const drawX = (canvasWidth - drawW) / 2; + const drawY = (canvasHeight - drawH) / 2; + + ctx.drawImage(frame.canvas, drawX, drawY, drawW, drawH); + } catch (error) { + console.warn( + `Failed to render video frame for ${mediaItem.name}:`, + error + ); + } } if (mediaItem.type === "image") { const img = new Image(); diff --git a/apps/web/src/lib/video-cache.ts b/apps/web/src/lib/video-cache.ts new file mode 100644 index 00000000..aeb62d1c --- /dev/null +++ b/apps/web/src/lib/video-cache.ts @@ -0,0 +1,186 @@ +import { + Input, + ALL_FORMATS, + BlobSource, + CanvasSink, + WrappedCanvas, +} from "mediabunny"; + +interface VideoSinkData { + sink: CanvasSink; + iterator: AsyncGenerator | null; + currentFrame: WrappedCanvas | null; + lastTime: number; +} +export class VideoCache { + private sinks = new Map(); + private initPromises = new Map>(); + + async getFrameAt( + mediaId: string, + file: File, + time: number + ): Promise { + await this.ensureSink(mediaId, file); + + const sinkData = this.sinks.get(mediaId); + if (!sinkData) return null; + + if ( + sinkData.currentFrame && + this.isFrameValid(sinkData.currentFrame, time) + ) { + return sinkData.currentFrame; + } + + if ( + sinkData.iterator && + sinkData.currentFrame && + time >= sinkData.lastTime && + time < sinkData.lastTime + 2.0 + ) { + const frame = await this.iterateToTime(sinkData, time); + if (frame) return frame; + } + + return await this.seekToTime(sinkData, time); + } + + private isFrameValid(frame: WrappedCanvas, time: number): boolean { + return time >= frame.timestamp && time < frame.timestamp + frame.duration; + } + private async iterateToTime( + sinkData: VideoSinkData, + targetTime: number + ): Promise { + if (!sinkData.iterator) return null; + + try { + while (true) { + const { value: frame, done } = await sinkData.iterator.next(); + + if (done || !frame) break; + + sinkData.currentFrame = frame; + sinkData.lastTime = frame.timestamp; + + if (this.isFrameValid(frame, targetTime)) { + return frame; + } + + if (frame.timestamp > targetTime + 1.0) break; + } + } catch (error) { + console.warn("Iterator failed, will restart:", error); + sinkData.iterator = null; + } + + return null; + } + private async seekToTime( + sinkData: VideoSinkData, + time: number + ): Promise { + try { + if (sinkData.iterator) { + await sinkData.iterator.return(); + sinkData.iterator = null; + } + + sinkData.iterator = sinkData.sink.canvases(time); + sinkData.lastTime = time; + + const { value: frame } = await sinkData.iterator.next(); + + if (frame) { + sinkData.currentFrame = frame; + return frame; + } + } catch (error) { + console.warn("Failed to seek video:", error); + } + + return null; + } + private async ensureSink(mediaId: string, file: File): Promise { + if (this.sinks.has(mediaId)) return; + + if (this.initPromises.has(mediaId)) { + await this.initPromises.get(mediaId); + return; + } + + const initPromise = this.initializeSink(mediaId, file); + this.initPromises.set(mediaId, initPromise); + + try { + await initPromise; + } finally { + this.initPromises.delete(mediaId); + } + } + private async initializeSink(mediaId: string, file: File): Promise { + try { + const input = new Input({ + source: new BlobSource(file), + formats: ALL_FORMATS, + }); + + const videoTrack = await input.getPrimaryVideoTrack(); + if (!videoTrack) { + throw new Error("No video track found"); + } + + const canDecode = await videoTrack.canDecode(); + if (!canDecode) { + throw new Error("Video codec not supported for decoding"); + } + + const sink = new CanvasSink(videoTrack, { + poolSize: 3, + fit: "contain", + }); + + this.sinks.set(mediaId, { + sink, + iterator: null, + currentFrame: null, + lastTime: -1, + }); + } catch (error) { + console.error(`Failed to initialize video sink for ${mediaId}:`, error); + throw error; + } + } + + clearVideo(mediaId: string): void { + const sinkData = this.sinks.get(mediaId); + if (sinkData) { + if (sinkData.iterator) { + sinkData.iterator.return(); + } + + this.sinks.delete(mediaId); + } + + this.initPromises.delete(mediaId); + } + + clearAll(): void { + for (const [mediaId] of this.sinks) { + this.clearVideo(mediaId); + } + } + + getStats() { + return { + totalSinks: this.sinks.size, + activeSinks: Array.from(this.sinks.values()).filter((s) => s.iterator) + .length, + cachedFrames: Array.from(this.sinks.values()).filter( + (s) => s.currentFrame + ).length, + }; + } +} +export const videoCache = new VideoCache(); diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts index 2b7cb62e..c9e29fff 100644 --- a/apps/web/src/stores/media-store.ts +++ b/apps/web/src/stores/media-store.ts @@ -3,6 +3,7 @@ import { storageService } from "@/lib/storage/storage-service"; import { useTimelineStore } from "./timeline-store"; import { generateUUID } from "@/lib/utils"; import { MediaType, MediaFile } from "@/types/media"; +import { videoCache } from "@/lib/video-cache"; interface MediaStore { mediaFiles: MediaFile[]; @@ -165,6 +166,8 @@ export const useMediaStore = create((set, get) => ({ const state = get(); const item = state.mediaFiles.find((media) => media.id === id); + videoCache.clearVideo(id); + // Cleanup object URLs to prevent memory leaks if (item?.url) { URL.revokeObjectURL(item.url); @@ -289,6 +292,8 @@ export const useMediaStore = create((set, get) => ({ clearAllMedia: () => { const state = get(); + videoCache.clearAll(); + // Cleanup all object URLs state.mediaFiles.forEach((item) => { if (item.url) { -- cgit v1.3.1 From 29fdd8568dbb9c67ee19492781f3f03eb2c3bd24 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 31 Aug 2025 14:10:59 +0200 Subject: refactor: improve types, fix background blur, some other stuff --- .../editor/media-panel/views/settings.tsx | 5 +- apps/web/src/components/editor/preview-panel.tsx | 12 +- apps/web/src/lib/export.ts | 4 +- apps/web/src/lib/timeline-renderer.ts | 145 ++++++++++++++++++--- apps/web/src/stores/project-store.ts | 10 +- apps/web/src/types/project.ts | 4 +- 6 files changed, 148 insertions(+), 32 deletions(-) (limited to 'apps/web/src/stores') diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx index df1ec54f..6cdd1b65 100644 --- a/apps/web/src/components/editor/media-panel/views/settings.tsx +++ b/apps/web/src/components/editor/media-panel/views/settings.tsx @@ -16,6 +16,7 @@ import { } from "../../properties-panel/property-item"; import { FPS_PRESETS } from "@/constants/timeline-constants"; import { useProjectStore } from "@/stores/project-store"; +import type { BlurIntensity } from "@/types/project"; import { useEditorStore } from "@/stores/editor-store"; import { useAspectRatio } from "@/hooks/use-aspect-ratio"; import Image from "next/image"; @@ -215,7 +216,7 @@ BackgroundPreviews.displayName = "BackgroundPreviews"; function BackgroundView() { const { activeProject, updateBackgroundType } = useProjectStore(); - const blurLevels = useMemo( + const blurLevels = useMemo>( () => [ { label: "Light", value: 4 }, { label: "Medium", value: 8 }, @@ -225,7 +226,7 @@ function BackgroundView() { ); const handleBlurSelect = useCallback( - async (blurIntensity: number) => { + async (blurIntensity: BlurIntensity) => { await updateBackgroundType("blur", { blurIntensity }); }, [updateBackgroundType] diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index c53cdafb..b7cc9578 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -521,9 +521,11 @@ export function PreviewPanel() { canvasHeight: displayHeight, tracks, mediaFiles, + backgroundType: activeProject?.backgroundType, + blurIntensity: activeProject?.blurIntensity, backgroundColor: activeProject?.backgroundType === "blur" - ? "transparent" + ? undefined : activeProject?.backgroundColor || "#000000", projectCanvasSize: canvasSize, }); @@ -553,9 +555,11 @@ export function PreviewPanel() { canvasHeight: displayHeight, tracks, mediaFiles, + backgroundType: activeProject?.backgroundType, + blurIntensity: activeProject?.blurIntensity, backgroundColor: activeProject?.backgroundType === "blur" - ? "transparent" + ? undefined : activeProject?.backgroundColor || "#000000", projectCanvasSize: canvasSize, }); @@ -624,9 +628,11 @@ export function PreviewPanel() { canvasHeight: displayHeight, tracks, mediaFiles, + backgroundType: activeProject?.backgroundType, + blurIntensity: activeProject?.blurIntensity, backgroundColor: activeProject?.backgroundType === "blur" - ? "transparent" + ? undefined : activeProject?.backgroundColor || "#000000", projectCanvasSize: canvasSize, }); diff --git a/apps/web/src/lib/export.ts b/apps/web/src/lib/export.ts index 8b1ab83e..52c58736 100644 --- a/apps/web/src/lib/export.ts +++ b/apps/web/src/lib/export.ts @@ -252,9 +252,11 @@ export async function exportProject( canvasHeight: canvas.height, tracks, mediaFiles, + backgroundType: activeProject.backgroundType, + blurIntensity: activeProject.blurIntensity, backgroundColor: activeProject.backgroundType === "blur" - ? "transparent" + ? undefined : activeProject.backgroundColor || "#000000", projectCanvasSize: canvasSize, }); diff --git a/apps/web/src/lib/timeline-renderer.ts b/apps/web/src/lib/timeline-renderer.ts index 08785369..ddc361b5 100644 --- a/apps/web/src/lib/timeline-renderer.ts +++ b/apps/web/src/lib/timeline-renderer.ts @@ -1,5 +1,6 @@ import type { TimelineTrack } from "@/types/timeline"; import type { MediaFile } from "@/types/media"; +import type { BlurIntensity } from "@/types/project"; import { videoCache } from "./video-cache"; export interface RenderContext { @@ -10,9 +11,29 @@ export interface RenderContext { tracks: TimelineTrack[]; mediaFiles: MediaFile[]; backgroundColor?: string; + backgroundType?: "color" | "blur"; + blurIntensity?: BlurIntensity; projectCanvasSize?: { width: number; height: number }; } +const imageElementCache = new Map(); + +async function getImageElement( + mediaItem: MediaFile +): Promise { + const cacheKey = mediaItem.id; + const cached = imageElementCache.get(cacheKey); + if (cached) return cached; + const img = new Image(); + await new Promise((resolve, reject) => { + img.onload = () => resolve(); + img.onerror = () => reject(new Error("Image load failed")); + img.src = mediaItem.url || URL.createObjectURL(mediaItem.file); + }); + imageElementCache.set(cacheKey, img); + return img; +} + export async function renderTimelineFrame({ ctx, time, @@ -21,6 +42,8 @@ export async function renderTimelineFrame({ tracks, mediaFiles, backgroundColor, + backgroundType, + blurIntensity, projectCanvasSize, }: RenderContext): Promise { // Background @@ -44,7 +67,7 @@ export async function renderTimelineFrame({ for (let t = tracks.length - 1; t >= 0; t -= 1) { const track = tracks[t]; for (const element of track.elements) { - if ((element as any).hidden) continue; + if (element.hidden) continue; const elementStart = element.startTime; const elementEnd = element.startTime + @@ -62,6 +85,72 @@ export async function renderTimelineFrame({ } } + // If background is set to blur, draw the active media as a blurred cover layer first + if (backgroundType === "blur") { + const blurPx = Math.max(0, blurIntensity ?? 8); + // Find a suitable media element (video/image) among active elements + const bgCandidate = active.find(({ element, mediaItem }) => { + return ( + element.type === "media" && + mediaItem !== null && + (mediaItem.type === "video" || mediaItem.type === "image") + ); + }); + if (bgCandidate && bgCandidate.mediaItem) { + const { element, mediaItem } = bgCandidate; + try { + if (mediaItem.type === "video") { + const localTime = time - element.startTime + element.trimStart; + const frame = await videoCache.getFrameAt( + mediaItem.id, + mediaItem.file, + Math.max(0, localTime) + ); + if (frame) { + const mediaW = Math.max(1, mediaItem.width || canvasWidth); + const mediaH = Math.max(1, mediaItem.height || canvasHeight); + const coverScale = Math.max( + canvasWidth / mediaW, + canvasHeight / mediaH + ); + const drawW = mediaW * coverScale; + const drawH = mediaH * coverScale; + const drawX = (canvasWidth - drawW) / 2; + const drawY = (canvasHeight - drawH) / 2; + ctx.save(); + ctx.filter = `blur(${blurPx}px)`; + ctx.drawImage(frame.canvas, drawX, drawY, drawW, drawH); + ctx.restore(); + } + } else if (mediaItem.type === "image") { + const img = await getImageElement(mediaItem); + const mediaW = Math.max( + 1, + mediaItem.width || img.naturalWidth || canvasWidth + ); + const mediaH = Math.max( + 1, + mediaItem.height || img.naturalHeight || canvasHeight + ); + const coverScale = Math.max( + canvasWidth / mediaW, + canvasHeight / mediaH + ); + const drawW = mediaW * coverScale; + const drawH = mediaH * coverScale; + const drawX = (canvasWidth - drawW) / 2; + const drawY = (canvasHeight - drawH) / 2; + ctx.save(); + ctx.filter = `blur(${blurPx}px)`; + ctx.drawImage(img, drawX, drawY, drawW, drawH); + ctx.restore(); + } + } catch { + // Ignore background blur failures; foreground will still render + } + } + } + for (const { element, mediaItem } of active) { if (element.type === "media" && mediaItem) { if (mediaItem.type === "video") { @@ -121,33 +210,47 @@ export async function renderTimelineFrame({ } } if (element.type === "text") { - const posX = canvasWidth / 2 + (element as any).x * scaleX; - const posY = canvasHeight / 2 + (element as any).y * scaleY; + const text = element; + const posX = canvasWidth / 2 + text.x * scaleX; + const posY = canvasHeight / 2 + text.y * scaleY; ctx.save(); ctx.translate(posX, posY); - ctx.rotate(((element as any).rotation * Math.PI) / 180); - ctx.globalAlpha = Math.max(0, Math.min(1, (element as any).opacity)); - const px = (element as any).fontSize * scaleX; - const weight = (element as any).fontWeight === "bold" ? "bold " : ""; - const style = (element as any).fontStyle === "italic" ? "italic " : ""; - ctx.font = `${style}${weight}${px}px ${(element as any).fontFamily}`; - ctx.fillStyle = (element as any).color; - ctx.textAlign = (element as any).textAlign; + ctx.rotate((text.rotation * Math.PI) / 180); + ctx.globalAlpha = Math.max(0, Math.min(1, text.opacity)); + const px = text.fontSize * scaleX; + const weight = text.fontWeight === "bold" ? "bold " : ""; + const style = text.fontStyle === "italic" ? "italic " : ""; + ctx.font = `${style}${weight}${px}px ${text.fontFamily}`; + ctx.fillStyle = text.color; + ctx.textAlign = text.textAlign as CanvasTextAlign; ctx.textBaseline = "middle"; - const metrics = ctx.measureText((element as any).content); - const ascent = - (metrics as unknown as { actualBoundingBoxAscent?: number }) - .actualBoundingBoxAscent ?? px * 0.8; - const descent = - (metrics as unknown as { actualBoundingBoxDescent?: number }) - .actualBoundingBoxDescent ?? px * 0.2; + const metrics = ctx.measureText(text.content); + const hasBoxMetrics = + "actualBoundingBoxAscent" in metrics && + "actualBoundingBoxDescent" in metrics; + const ascent = hasBoxMetrics + ? ( + metrics as TextMetrics & { + actualBoundingBoxAscent: number; + actualBoundingBoxDescent: number; + } + ).actualBoundingBoxAscent + : px * 0.8; + const descent = hasBoxMetrics + ? ( + metrics as TextMetrics & { + actualBoundingBoxAscent: number; + actualBoundingBoxDescent: number; + } + ).actualBoundingBoxDescent + : px * 0.2; const textW = metrics.width; const textH = ascent + descent; const padX = 8 * scaleX; const padY = 4 * scaleX; - if ((element as any).backgroundColor) { + if (text.backgroundColor) { ctx.save(); - ctx.fillStyle = (element as any).backgroundColor; + ctx.fillStyle = text.backgroundColor; let bgLeft = -textW / 2; if (ctx.textAlign === "left") bgLeft = 0; if (ctx.textAlign === "right") bgLeft = -textW; @@ -159,7 +262,7 @@ export async function renderTimelineFrame({ ); ctx.restore(); } - ctx.fillText((element as any).content, 0, 0); + ctx.fillText(text.content, 0, 0); ctx.restore(); } } diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index b924ea22..ed5eda73 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -1,4 +1,4 @@ -import { TProject } from "@/types/project"; +import { TProject, BlurIntensity } from "@/types/project"; import { create } from "zustand"; import { storageService } from "@/lib/storage/storage-service"; import { toast } from "sonner"; @@ -44,7 +44,7 @@ interface ProjectStore { updateProjectBackground: (backgroundColor: string) => Promise; updateBackgroundType: ( type: "color" | "blur", - options?: { backgroundColor?: string; blurIntensity?: number } + options?: { backgroundColor?: string; blurIntensity?: BlurIntensity } ) => Promise; updateProjectFps: (fps: number) => Promise; updateCanvasSize: (size: CanvasSize, mode: CanvasMode) => Promise; @@ -399,7 +399,7 @@ export const useProjectStore = create((set, get) => ({ updateBackgroundType: async ( type: "color" | "blur", - options?: { backgroundColor?: string; blurIntensity?: number } + options?: { backgroundColor?: string; blurIntensity?: BlurIntensity } ) => { const { activeProject } = get(); if (!activeProject) return; @@ -410,7 +410,9 @@ export const useProjectStore = create((set, get) => ({ ...(options?.backgroundColor && { backgroundColor: options.backgroundColor, }), - ...(options?.blurIntensity && { blurIntensity: options.blurIntensity }), + ...(options?.blurIntensity !== undefined && { + blurIntensity: options.blurIntensity, + }), updatedAt: new Date(), }; diff --git a/apps/web/src/types/project.ts b/apps/web/src/types/project.ts index bf510942..66de858a 100644 --- a/apps/web/src/types/project.ts +++ b/apps/web/src/types/project.ts @@ -1,5 +1,7 @@ import { CanvasSize } from "./editor"; +export type BlurIntensity = 4 | 8 | 18; + export interface TProject { id: string; name: string; @@ -9,7 +11,7 @@ export interface TProject { mediaItems?: string[]; backgroundColor?: string; backgroundType?: "color" | "blur"; - blurIntensity?: number; // in pixels (4, 8, 18) + blurIntensity?: BlurIntensity; fps?: number; bookmarks?: number[]; canvasSize: CanvasSize; -- cgit v1.3.1 From 350b55081cde3a4332c982ea5c53b7d359029f6b Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 1 Sep 2025 16:17:06 +0200 Subject: refactor(core): switch to scene-based architecture --- apps/web/src/app/layout.tsx | 5 +- apps/web/src/components/editor/scenes-view.tsx | 64 ++++++ .../editor/timeline/timeline-toolbar.tsx | 13 +- apps/web/src/components/footer.tsx | 17 -- .../providers/migrators/scenes-migrator.tsx | 176 +++++++++++++++++ apps/web/src/components/ui/sheet.tsx | 12 +- apps/web/src/lib/fetch-github-stars.ts | 29 --- apps/web/src/lib/storage/storage-service.ts | 159 +++++++++++---- apps/web/src/lib/storage/types.ts | 20 +- apps/web/src/stores/media-store.ts | 8 +- apps/web/src/stores/project-store.ts | 145 +++++++++----- apps/web/src/stores/scene-store.ts | 219 +++++++++++++++++++++ apps/web/src/stores/sounds-store.ts | 4 +- apps/web/src/stores/timeline-store.ts | 76 ++++++- apps/web/src/types/project.ts | 10 + 15 files changed, 793 insertions(+), 164 deletions(-) create mode 100644 apps/web/src/components/editor/scenes-view.tsx create mode 100644 apps/web/src/components/providers/migrators/scenes-migrator.tsx delete mode 100644 apps/web/src/lib/fetch-github-stars.ts create mode 100644 apps/web/src/stores/scene-store.ts (limited to 'apps/web/src/stores') diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index ee0bdbd2..7e32ea22 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,6 +5,7 @@ import "./globals.css"; import { Toaster } from "../components/ui/sonner"; import { TooltipProvider } from "../components/ui/tooltip"; import { StorageProvider } from "../components/storage-provider"; +import { ScenesMigrator } from "../components/providers/migrators/scenes-migrator"; import { baseMetaData } from "./metadata"; import { defaultFont } from "../lib/font-config"; import { BotIdClient } from "botid/client"; @@ -36,7 +37,9 @@ export default function RootLayout({ - {children} + + {children} +