From 3e89d2998588f8e35c524e523ba2ee853f0f6475 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 20 Apr 2026 11:31:17 +0200 Subject: refactor: restructure files to their domains, new preview overlay system, and dep graph --- .../components/storage-persistence-dialog.tsx | 41 +++++++++++++++ .../storage/migrations/__tests__/v1-to-v2.test.ts | 6 +-- .../storage/migrations/transformers/v1-to-v2.ts | 6 +-- .../storage/migrations/transformers/v14-to-v15.ts | 2 +- .../storage/migrations/transformers/v17-to-v18.ts | 4 +- .../storage/migrations/transformers/v20-to-v21.ts | 2 +- .../storage/migrations/transformers/v22-to-v23.ts | 2 +- .../storage/migrations/transformers/v26-to-v27.ts | 2 +- apps/web/src/services/storage/service.ts | 10 ++-- apps/web/src/services/storage/types.ts | 6 +-- apps/web/src/services/storage/use-local-storage.ts | 58 ++++++++++++++++++++++ .../services/storage/use-storage-persistence.ts | 45 +++++++++++++++++ 12 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/services/storage/components/storage-persistence-dialog.tsx create mode 100644 apps/web/src/services/storage/use-local-storage.ts create mode 100644 apps/web/src/services/storage/use-storage-persistence.ts (limited to 'apps/web/src/services/storage') diff --git a/apps/web/src/services/storage/components/storage-persistence-dialog.tsx b/apps/web/src/services/storage/components/storage-persistence-dialog.tsx new file mode 100644 index 00000000..4544ef1d --- /dev/null +++ b/apps/web/src/services/storage/components/storage-persistence-dialog.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogBody, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useStoragePersistence } from "@/services/storage/use-storage-persistence"; + +export function StoragePersistenceDialog() { + const { showDialog, onConfirm, onDismiss } = useStoragePersistence(); + + return ( + !open && onDismiss()}> + + + Don't lose your projects + + +

+ Your browser can automatically delete your projects when storage + runs low. +

+

+ Allow OpenCut to protect them? +

+
+ + + + +
+
+ ); +} diff --git a/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts b/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts index 9cea9a2e..6d0c37fb 100644 --- a/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts +++ b/apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; -import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur"; -import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color"; -import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes"; +import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur"; +import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; +import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes"; const DEFAULT_FPS = 30; import type { MediaAssetData } from "@/services/storage/types"; import { getProjectId, transformProjectV1ToV2 } from "../transformers/v1-to-v2"; diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts index a19269cd..c4b64e50 100644 --- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts +++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts @@ -1,6 +1,6 @@ -import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/blur"; -import { DEFAULT_BACKGROUND_COLOR } from "@/lib/background/color"; -import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/sizes"; +import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur"; +import { DEFAULT_BACKGROUND_COLOR } from "@/background/color"; +import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes"; const DEFAULT_FPS = 30; import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter"; import type { MediaAssetData } from "@/services/storage/types"; diff --git a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts index 36e3d9a8..5f3c01e9 100644 --- a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts +++ b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts @@ -1,4 +1,4 @@ -import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/lib/stickers/intrinsic-size"; +import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; diff --git a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts index 5dcf6feb..3ef6cde9 100644 --- a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts +++ b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts @@ -1,6 +1,6 @@ import type { MigrationResult, ProjectRecord } from "./types"; -import { VOLUME_DB_MIN } from "@/lib/timeline/audio-constants"; -import { clampDb } from "@/lib/timeline/audio-state"; +import { VOLUME_DB_MIN } from "@/timeline/audio-constants"; +import { clampDb } from "@/timeline/audio-state"; import { getProjectId, isRecord } from "./utils"; export function transformProjectV17ToV18({ diff --git a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts index f5194bda..6c30e5c8 100644 --- a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts +++ b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts @@ -1,4 +1,4 @@ -import { INTENSITY_TO_SIGMA_DIVISOR } from "@/lib/effects/definitions/blur"; +import { INTENSITY_TO_SIGMA_DIVISOR } from "@/effects/definitions/blur"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; diff --git a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts index cde1294e..5e3adc33 100644 --- a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts +++ b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts @@ -1,7 +1,7 @@ import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; -import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { TICKS_PER_SECOND } from "@/wasm"; const ARBITRARY_FPS_DENOMINATOR = 1_000_000; const STANDARD_FRAME_RATES = [ { value: 24_000 / 1_001, numerator: 24_000, denominator: 1_001 }, diff --git a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts index 186ae1e9..820b0087 100644 --- a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts +++ b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts @@ -1,4 +1,4 @@ -import { parseCustomMaskPath } from "@/lib/masks/custom-path"; +import { parseCustomMaskPath } from "@/masks/custom-path"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; diff --git a/apps/web/src/services/storage/service.ts b/apps/web/src/services/storage/service.ts index 1aea0297..8ef77846 100644 --- a/apps/web/src/services/storage/service.ts +++ b/apps/web/src/services/storage/service.ts @@ -1,6 +1,6 @@ -import type { TProject, TProjectMetadata } from "@/lib/project/types"; -import { getProjectDurationFromScenes } from "@/lib/scenes"; -import type { MediaAsset } from "@/lib/media/types"; +import type { TProject, TProjectMetadata } from "@/project/types"; +import { getProjectDurationFromScenes } from "@/timeline/scenes"; +import type { MediaAsset } from "@/media/types"; import { IndexedDBAdapter } from "./indexeddb-adapter"; import { OPFSAdapter } from "./opfs-adapter"; import { @@ -16,12 +16,12 @@ import type { SerializedProject, SerializedScene, } from "./types"; -import type { SavedSoundsData, SavedSound, SoundEffect } from "@/lib/sounds/types"; +import type { SavedSoundsData, SavedSound, SoundEffect } from "@/sounds/types"; import { migrations, runStorageMigrations, } from "@/services/storage/migrations"; -import type { Bookmark, SceneTracks, TScene } from "@/lib/timeline"; +import type { Bookmark, SceneTracks, TScene } from "@/timeline"; function normalizeBookmarks({ raw }: { raw: unknown }): Bookmark[] { if (!Array.isArray(raw)) return []; diff --git a/apps/web/src/services/storage/types.ts b/apps/web/src/services/storage/types.ts index 1dcf6545..58c14b49 100644 --- a/apps/web/src/services/storage/types.ts +++ b/apps/web/src/services/storage/types.ts @@ -1,10 +1,10 @@ -import type { MediaType } from "@/lib/media/types"; +import type { MediaType } from "@/media/types"; import type { TProject, TProjectMetadata, TTimelineViewState, -} from "@/lib/project/types"; -import type { TScene } from "@/lib/timeline"; +} from "@/project/types"; +import type { TScene } from "@/timeline"; export interface StorageAdapter { get(key: string): Promise; diff --git a/apps/web/src/services/storage/use-local-storage.ts b/apps/web/src/services/storage/use-local-storage.ts new file mode 100644 index 00000000..d0188ab3 --- /dev/null +++ b/apps/web/src/services/storage/use-local-storage.ts @@ -0,0 +1,58 @@ +import { useState, useEffect, useCallback, useRef } from "react"; + +export function useLocalStorage({ + key, + defaultValue, +}: { + key: string; + defaultValue: T; +}): [ + T, + ({ value }: { value: T | ((previousValue: T) => T) }) => void, + boolean, +] { + const [value, setValue] = useState(defaultValue); + const [isReady, setIsReady] = useState(false); + const valueRef = useRef(defaultValue); + + // avoid hydration mismatch by reading after mount + useEffect(() => { + try { + const storedValue = localStorage.getItem(key); + if (storedValue !== null) { + const parsedValue = JSON.parse(storedValue) as T; + valueRef.current = parsedValue; + setValue(parsedValue); + } + } catch { + // localstorage might be unavailable + } + setIsReady(true); + }, [key]); + + // sync to localstorage after hydration + useEffect(() => { + if (!isReady) return; + + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch { + // localstorage might be full or disabled + } + }, [key, value, isReady]); + + const setValueWithCallback = useCallback( + ({ value: nextValue }: { value: T | ((previousValue: T) => T) }) => { + const resolvedValue = + typeof nextValue === "function" + ? (nextValue as (previousValue: T) => T)(valueRef.current) + : nextValue; + + valueRef.current = resolvedValue; + setValue(resolvedValue); + }, + [], + ); + + return [value, setValueWithCallback, isReady]; +} diff --git a/apps/web/src/services/storage/use-storage-persistence.ts b/apps/web/src/services/storage/use-storage-persistence.ts new file mode 100644 index 00000000..a026b510 --- /dev/null +++ b/apps/web/src/services/storage/use-storage-persistence.ts @@ -0,0 +1,45 @@ +"use client"; + +import { useEffect, useState } from "react"; + +const DISMISSED_KEY = "opencut-storage-persist-dismissed"; + +function isFirefox(): boolean { + return navigator.userAgent.toLowerCase().includes("firefox"); +} + +export function useStoragePersistence() { + const [showDialog, setShowDialog] = useState(false); + + useEffect(() => { + if (!navigator.storage?.persist) return; + + const run = async () => { + const alreadyPersisted = await navigator.storage.persisted(); + if (alreadyPersisted) return; + + const dismissed = localStorage.getItem(DISMISSED_KEY) === "true"; + if (dismissed) return; + + if (isFirefox()) { + setShowDialog(true); + } else { + await navigator.storage.persist(); + } + }; + + run(); + }, []); + + const onConfirm = async () => { + setShowDialog(false); + await navigator.storage.persist(); + }; + + const onDismiss = () => { + setShowDialog(false); + localStorage.setItem(DISMISSED_KEY, "true"); + }; + + return { showDialog, onConfirm, onDismiss }; +} -- cgit v1.3.1