summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/services/storage')
-rw-r--r--apps/web/src/services/storage/components/storage-persistence-dialog.tsx41
-rw-r--r--apps/web/src/services/storage/migrations/__tests__/v1-to-v2.test.ts6
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts6
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts2
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts4
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts2
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts2
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts2
-rw-r--r--apps/web/src/services/storage/service.ts10
-rw-r--r--apps/web/src/services/storage/types.ts6
-rw-r--r--apps/web/src/services/storage/use-local-storage.ts58
-rw-r--r--apps/web/src/services/storage/use-storage-persistence.ts45
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 };
+}