summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/storage
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib/storage')
-rw-r--r--apps/web/src/lib/storage/indexeddb-adapter.ts89
-rw-r--r--apps/web/src/lib/storage/opfs-adapter.ts73
-rw-r--r--apps/web/src/lib/storage/storage-service.ts480
-rw-r--r--apps/web/src/lib/storage/types.ts69
4 files changed, 0 insertions, 711 deletions
diff --git a/apps/web/src/lib/storage/indexeddb-adapter.ts b/apps/web/src/lib/storage/indexeddb-adapter.ts
deleted file mode 100644
index 597305c7..00000000
--- a/apps/web/src/lib/storage/indexeddb-adapter.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { StorageAdapter } from "./types";
-
-export class IndexedDBAdapter<T> implements StorageAdapter<T> {
- private dbName: string;
- private storeName: string;
- private version: number;
-
- constructor(dbName: string, storeName: string, version = 1) {
- this.dbName = dbName;
- this.storeName = storeName;
- this.version = version;
- }
-
- private async getDB(): Promise<IDBDatabase> {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(this.dbName, this.version);
-
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve(request.result);
-
- request.onupgradeneeded = (event) => {
- const db = (event.target as IDBOpenDBRequest).result;
- if (!db.objectStoreNames.contains(this.storeName)) {
- db.createObjectStore(this.storeName, { keyPath: "id" });
- }
- };
- });
- }
-
- async get(key: string): Promise<T | null> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readonly");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.get(key);
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve(request.result || null);
- });
- }
-
- async set(key: string, value: T): Promise<void> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readwrite");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.put({ id: key, ...value });
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve();
- });
- }
-
- async remove(key: string): Promise<void> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readwrite");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.delete(key);
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve();
- });
- }
-
- async list(): Promise<string[]> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readonly");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.getAllKeys();
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve(request.result as string[]);
- });
- }
-
- async clear(): Promise<void> {
- const db = await this.getDB();
- const transaction = db.transaction([this.storeName], "readwrite");
- const store = transaction.objectStore(this.storeName);
-
- return new Promise((resolve, reject) => {
- const request = store.clear();
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve();
- });
- }
-}
diff --git a/apps/web/src/lib/storage/opfs-adapter.ts b/apps/web/src/lib/storage/opfs-adapter.ts
deleted file mode 100644
index 87b5c886..00000000
--- a/apps/web/src/lib/storage/opfs-adapter.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { StorageAdapter } from "./types";
-
-export class OPFSAdapter implements StorageAdapter<File> {
- private directoryName: string;
-
- constructor(directoryName = "media") {
- this.directoryName = directoryName;
- }
-
- private async getDirectory(): Promise<FileSystemDirectoryHandle> {
- const opfsRoot = await navigator.storage.getDirectory();
- return await opfsRoot.getDirectoryHandle(this.directoryName, {
- create: true,
- });
- }
-
- async get(key: string): Promise<File | null> {
- try {
- const directory = await this.getDirectory();
- const fileHandle = await directory.getFileHandle(key);
- return await fileHandle.getFile();
- } catch (error) {
- if ((error as Error).name === "NotFoundError") {
- return null;
- }
- throw error;
- }
- }
-
- async set(key: string, file: File): Promise<void> {
- const directory = await this.getDirectory();
- const fileHandle = await directory.getFileHandle(key, { create: true });
- const writable = await fileHandle.createWritable();
-
- await writable.write(file);
- await writable.close();
- }
-
- async remove(key: string): Promise<void> {
- try {
- const directory = await this.getDirectory();
- await directory.removeEntry(key);
- } catch (error) {
- if ((error as Error).name !== "NotFoundError") {
- throw error;
- }
- }
- }
-
- async list(): Promise<string[]> {
- const directory = await this.getDirectory();
- const keys: string[] = [];
-
- for await (const name of directory.keys()) {
- keys.push(name);
- }
-
- return keys;
- }
-
- async clear(): Promise<void> {
- const directory = await this.getDirectory();
-
- for await (const name of directory.keys()) {
- await directory.removeEntry(name);
- }
- }
-
- // Helper method to check OPFS support
- static isSupported(): boolean {
- return "storage" in navigator && "getDirectory" in navigator.storage;
- }
-}
diff --git a/apps/web/src/lib/storage/storage-service.ts b/apps/web/src/lib/storage/storage-service.ts
deleted file mode 100644
index 2688f6e8..00000000
--- a/apps/web/src/lib/storage/storage-service.ts
+++ /dev/null
@@ -1,480 +0,0 @@
-import { TProject } from "@/types/project";
-import { MediaFile } from "@/types/media";
-import { IndexedDBAdapter } from "./indexeddb-adapter";
-import { OPFSAdapter } from "./opfs-adapter";
-import {
- MediaFileData,
- StorageConfig,
- SerializedProject,
- SerializedScene,
- TimelineData,
-} from "./types";
-import { TimelineTrack } from "@/types/timeline";
-import { SavedSoundsData, SavedSound, SoundEffect } from "@/types/sounds";
-
-class StorageService {
- private projectsAdapter: IndexedDBAdapter<SerializedProject>;
- private savedSoundsAdapter: IndexedDBAdapter<SavedSoundsData>;
- private config: StorageConfig;
-
- constructor() {
- this.config = {
- projectsDb: "video-editor-projects",
- mediaDb: "video-editor-media",
- timelineDb: "video-editor-timelines",
- savedSoundsDb: "video-editor-saved-sounds",
- version: 1,
- };
-
- this.projectsAdapter = new IndexedDBAdapter<SerializedProject>(
- this.config.projectsDb,
- "projects",
- this.config.version
- );
-
- this.savedSoundsAdapter = new IndexedDBAdapter<SavedSoundsData>(
- this.config.savedSoundsDb,
- "saved-sounds",
- this.config.version
- );
- }
-
- // Helper to get project-specific media adapters
- private getProjectMediaAdapters({ projectId }: { projectId: string }) {
- const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>(
- `${this.config.mediaDb}-${projectId}`,
- "media-metadata",
- this.config.version
- );
-
- const mediaFilesAdapter = new OPFSAdapter(`media-files-${projectId}`);
-
- return { mediaMetadataAdapter, mediaFilesAdapter };
- }
-
- // Helper to get project-specific timeline adapter
- private getProjectTimelineAdapter({
- projectId,
- sceneId,
- }: {
- projectId: string;
- sceneId?: string;
- }) {
- const dbName = sceneId
- ? `${this.config.timelineDb}-${projectId}-${sceneId}`
- : `${this.config.timelineDb}-${projectId}`;
-
- return new IndexedDBAdapter<TimelineData>(
- dbName,
- "timeline",
- this.config.version
- );
- }
-
- // Project operations
- async saveProject({ project }: { project: TProject }): Promise<void> {
- // Convert TProject to serializable format
- const serializedScenes: SerializedScene[] = project.scenes.map((scene) => ({
- id: scene.id,
- name: scene.name,
- isMain: scene.isMain,
- createdAt: scene.createdAt.toISOString(),
- updatedAt: scene.updatedAt.toISOString(),
- }));
-
- const serializedProject: SerializedProject = {
- id: project.id,
- name: project.name,
- thumbnail: project.thumbnail,
- createdAt: project.createdAt.toISOString(),
- updatedAt: project.updatedAt.toISOString(),
- scenes: serializedScenes,
- currentSceneId: project.currentSceneId,
- backgroundColor: project.backgroundColor,
- backgroundType: project.backgroundType,
- blurIntensity: project.blurIntensity,
- bookmarks: project.bookmarks,
- fps: project.fps,
- canvasSize: project.canvasSize,
- canvasMode: project.canvasMode,
- };
-
- await this.projectsAdapter.set(project.id, serializedProject);
- }
-
- async loadProject({ id }: { id: string }): Promise<TProject | null> {
- const serializedProject = await this.projectsAdapter.get(id);
-
- if (!serializedProject) return null;
-
- // Now convert serialized scenes back to Scene objects
- const scenes =
- serializedProject.scenes?.map((scene) => ({
- id: scene.id,
- name: scene.name,
- isMain: scene.isMain,
- createdAt: new Date(scene.createdAt),
- updatedAt: new Date(scene.updatedAt),
- })) || [];
-
- // Convert back to TProject format
- const project = {
- id: serializedProject.id,
- name: serializedProject.name,
- thumbnail: serializedProject.thumbnail,
- createdAt: new Date(serializedProject.createdAt),
- updatedAt: new Date(serializedProject.updatedAt),
- scenes,
- currentSceneId: serializedProject.currentSceneId || "",
- backgroundColor: serializedProject.backgroundColor,
- backgroundType: serializedProject.backgroundType,
- blurIntensity: serializedProject.blurIntensity,
- bookmarks: serializedProject.bookmarks,
- fps: serializedProject.fps,
- canvasSize: serializedProject.canvasSize,
- canvasMode: serializedProject.canvasMode,
- };
- return project;
- }
-
- async loadAllProjects(): Promise<TProject[]> {
- const projectIds = await this.projectsAdapter.list();
- const projects: TProject[] = [];
-
- for (const id of projectIds) {
- const project = await this.loadProject({ id });
- if (project) {
- projects.push(project);
- }
- }
-
- // Sort by last updated (most recent first)
- return projects.sort(
- (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
- );
- }
-
- async deleteProject({ id }: { id: string }): Promise<void> {
- await this.projectsAdapter.remove(id);
- }
-
- // Media operations
- async saveMediaFile({
- projectId,
- mediaItem,
- }: {
- projectId: string;
- mediaItem: MediaFile;
- }): Promise<void> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters({ projectId });
-
- // Save file to project-specific OPFS
- await mediaFilesAdapter.set(mediaItem.id, mediaItem.file);
-
- // Save metadata to project-specific IndexedDB
- const metadata: MediaFileData = {
- id: mediaItem.id,
- name: mediaItem.name,
- type: mediaItem.type,
- size: mediaItem.file.size,
- lastModified: mediaItem.file.lastModified,
- width: mediaItem.width,
- height: mediaItem.height,
- duration: mediaItem.duration,
- ephemeral: mediaItem.ephemeral,
- };
-
- await mediaMetadataAdapter.set(mediaItem.id, metadata);
- }
-
- async loadMediaFile({
- projectId,
- id,
- }: {
- projectId: string;
- id: string;
- }): Promise<MediaFile | null> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters({ projectId });
-
- const [file, metadata] = await Promise.all([
- mediaFilesAdapter.get(id),
- mediaMetadataAdapter.get(id),
- ]);
-
- if (!file || !metadata) return null;
-
- let url: string;
- if (metadata.type === "image" && (!file.type || file.type === "")) {
- try {
- const text = await file.text();
- if (text.trim().startsWith("<svg")) {
- const svgBlob = new Blob([text], { type: "image/svg+xml" });
- url = URL.createObjectURL(svgBlob);
- } else {
- url = URL.createObjectURL(file);
- }
- } catch {
- url = URL.createObjectURL(file);
- }
- } else {
- url = URL.createObjectURL(file);
- }
-
- return {
- id: metadata.id,
- name: metadata.name,
- type: metadata.type,
- file,
- url,
- width: metadata.width,
- height: metadata.height,
- duration: metadata.duration,
- ephemeral: metadata.ephemeral,
- };
- }
-
- async loadAllMediaFiles({
- projectId,
- }: {
- projectId: string;
- }): Promise<MediaFile[]> {
- const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
- projectId,
- });
-
- const mediaIds = await mediaMetadataAdapter.list();
- const mediaItems: MediaFile[] = [];
-
- for (const id of mediaIds) {
- const item = await this.loadMediaFile({ projectId, id });
- if (item) {
- mediaItems.push(item);
- }
- }
-
- return mediaItems;
- }
-
- async deleteMediaFile({
- projectId,
- id,
- }: {
- projectId: string;
- id: string;
- }): Promise<void> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters({ projectId });
-
- await Promise.all([
- mediaFilesAdapter.remove(id),
- mediaMetadataAdapter.remove(id),
- ]);
- }
-
- async deleteProjectMedia({
- projectId,
- }: {
- projectId: string;
- }): Promise<void> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters({ projectId });
-
- await Promise.all([
- mediaMetadataAdapter.clear(),
- mediaFilesAdapter.clear(),
- ]);
- }
-
- // Timeline operations - supports both legacy and scene-based storage
- async saveTimeline({
- projectId,
- tracks,
- sceneId,
- }: {
- projectId: string;
- tracks: TimelineTrack[];
- sceneId?: string;
- }): Promise<void> {
- const timelineAdapter = this.getProjectTimelineAdapter({
- projectId,
- sceneId,
- });
- const timelineData: TimelineData = {
- tracks,
- lastModified: new Date().toISOString(),
- };
- await timelineAdapter.set("timeline", timelineData);
- }
-
- async loadTimeline({
- projectId,
- sceneId,
- }: {
- projectId: string;
- sceneId?: string;
- }): Promise<TimelineTrack[] | null> {
- const timelineAdapter = this.getProjectTimelineAdapter({
- projectId,
- sceneId,
- });
- const timelineData = await timelineAdapter.get("timeline");
- return timelineData ? timelineData.tracks : null;
- }
-
- async deleteProjectTimeline({
- projectId,
- }: {
- projectId: string;
- }): Promise<void> {
- const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
- await timelineAdapter.remove("timeline");
- }
-
- // Utility methods
- async clearAllData(): Promise<void> {
- // Clear all projects
- await this.projectsAdapter.clear();
-
- // Note: Project-specific media and timelines will be cleaned up when projects are deleted
- }
-
- async getStorageInfo(): Promise<{
- projects: number;
- isOPFSSupported: boolean;
- isIndexedDBSupported: boolean;
- }> {
- const projectIds = await this.projectsAdapter.list();
-
- return {
- projects: projectIds.length,
- isOPFSSupported: this.isOPFSSupported(),
- isIndexedDBSupported: this.isIndexedDBSupported(),
- };
- }
-
- async getProjectStorageInfo({ projectId }: { projectId: string }): Promise<{
- mediaItems: number;
- hasTimeline: boolean;
- }> {
- const { mediaMetadataAdapter } = this.getProjectMediaAdapters({
- projectId,
- });
- const timelineAdapter = this.getProjectTimelineAdapter({ projectId });
-
- const [mediaIds, timelineData] = await Promise.all([
- mediaMetadataAdapter.list(),
- timelineAdapter.get("timeline"),
- ]);
-
- return {
- mediaItems: mediaIds.length,
- hasTimeline: !!timelineData,
- };
- }
-
- async loadSavedSounds(): Promise<SavedSoundsData> {
- try {
- const savedSoundsData = await this.savedSoundsAdapter.get("user-sounds");
- return (
- savedSoundsData || {
- sounds: [],
- lastModified: new Date().toISOString(),
- }
- );
- } catch (error) {
- console.error("Failed to load saved sounds:", error);
- return { sounds: [], lastModified: new Date().toISOString() };
- }
- }
-
- async saveSoundEffect({
- soundEffect,
- }: {
- soundEffect: SoundEffect;
- }): Promise<void> {
- try {
- const currentData = await this.loadSavedSounds();
-
- // Check if sound is already saved
- if (currentData.sounds.some((sound) => sound.id === soundEffect.id)) {
- return; // Already saved
- }
-
- const savedSound: SavedSound = {
- id: soundEffect.id,
- name: soundEffect.name,
- username: soundEffect.username,
- previewUrl: soundEffect.previewUrl,
- downloadUrl: soundEffect.downloadUrl,
- duration: soundEffect.duration,
- tags: soundEffect.tags,
- license: soundEffect.license,
- savedAt: new Date().toISOString(),
- };
-
- const updatedData: SavedSoundsData = {
- sounds: [...currentData.sounds, savedSound],
- lastModified: new Date().toISOString(),
- };
-
- await this.savedSoundsAdapter.set("user-sounds", updatedData);
- } catch (error) {
- console.error("Failed to save sound effect:", error);
- throw error;
- }
- }
-
- async removeSavedSound({ soundId }: { soundId: number }): Promise<void> {
- try {
- const currentData = await this.loadSavedSounds();
-
- const updatedData: SavedSoundsData = {
- sounds: currentData.sounds.filter((sound) => sound.id !== soundId),
- lastModified: new Date().toISOString(),
- };
-
- await this.savedSoundsAdapter.set("user-sounds", updatedData);
- } catch (error) {
- console.error("Failed to remove saved sound:", error);
- throw error;
- }
- }
-
- async isSoundSaved({ soundId }: { soundId: number }): Promise<boolean> {
- try {
- const currentData = await this.loadSavedSounds();
- return currentData.sounds.some((sound) => sound.id === soundId);
- } catch (error) {
- console.error("Failed to check if sound is saved:", error);
- return false;
- }
- }
-
- async clearSavedSounds(): Promise<void> {
- try {
- await this.savedSoundsAdapter.remove("user-sounds");
- } catch (error) {
- console.error("Failed to clear saved sounds:", error);
- throw error;
- }
- }
-
- // Check browser support
- isOPFSSupported(): boolean {
- return OPFSAdapter.isSupported();
- }
-
- isIndexedDBSupported(): boolean {
- return "indexedDB" in window;
- }
-
- isFullySupported(): boolean {
- return this.isIndexedDBSupported() && this.isOPFSSupported();
- }
-}
-
-// Export singleton instance
-export const storageService = new StorageService();
-export { StorageService };
diff --git a/apps/web/src/lib/storage/types.ts b/apps/web/src/lib/storage/types.ts
deleted file mode 100644
index e66239cd..00000000
--- a/apps/web/src/lib/storage/types.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { TProject, Scene } from "@/types/project";
-import { TimelineTrack } from "@/types/timeline";
-
-export interface StorageAdapter<T> {
- get(key: string): Promise<T | null>;
- set(key: string, value: T): Promise<void>;
- remove(key: string): Promise<void>;
- list(): Promise<string[]>;
- clear(): Promise<void>;
-}
-
-export interface MediaFileData {
- id: string;
- name: string;
- type: "image" | "video" | "audio";
- size: number;
- lastModified: number;
- width?: number;
- height?: number;
- duration?: number;
- ephemeral?: boolean;
- sourceStickerIconName?: string;
- // File will be stored separately in OPFS
-}
-
-// Legacy timeline data, kept for backward compatibility
-export interface TimelineData {
- tracks: TimelineTrack[];
- lastModified: string;
-}
-
-export interface SceneTimelineData {
- sceneId: string;
- tracks: TimelineTrack[];
- lastModified: string;
-}
-
-export type SerializedScene = Omit<Scene, "createdAt" | "updatedAt"> & {
- createdAt: string;
- updatedAt: string;
-};
-
-export interface StorageConfig {
- projectsDb: string;
- mediaDb: string;
- timelineDb: string;
- savedSoundsDb: string;
- version: number;
-}
-
-// Helper type for serialization - converts Date objects to strings
-export type SerializedProject = Omit<
- TProject,
- "createdAt" | "updatedAt" | "scenes"
-> & {
- createdAt: string;
- updatedAt: string;
- scenes: SerializedScene[];
- bookmarks?: number[];
-};
-
-// Extend FileSystemDirectoryHandle with missing async iterator methods
-declare global {
- interface FileSystemDirectoryHandle {
- keys(): AsyncIterableIterator<string>;
- values(): AsyncIterableIterator<FileSystemHandle>;
- entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
- }
-}