diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/services/storage | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz | |
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/services/storage')
12 files changed, 164 insertions, 20 deletions
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 (
+ <Dialog open={showDialog} onOpenChange={(open) => !open && onDismiss()}>
+ <DialogContent className="sm:max-w-md">
+ <DialogHeader>
+ <DialogTitle>Don't lose your projects</DialogTitle>
+ </DialogHeader>
+ <DialogBody>
+ <p className="text-base text-muted-foreground">
+ Your browser can automatically delete your projects when storage
+ runs low.
+ </p>
+ <p className="text-base text-muted-foreground">
+ Allow OpenCut to protect them?
+ </p>
+ </DialogBody>
+ <DialogFooter>
+ <Button variant="outline" onClick={onDismiss}>
+ Not now
+ </Button>
+ <Button onClick={onConfirm}>Allow</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
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<T> { get(key: string): Promise<T | null>; 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<T>({ + key, + defaultValue, +}: { + key: string; + defaultValue: T; +}): [ + T, + ({ value }: { value: T | ((previousValue: T) => T) }) => void, + boolean, +] { + const [value, setValue] = useState<T>(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 };
+}
|
