summaryrefslogtreecommitdiff
path: root/apps/web/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib')
-rw-r--r--apps/web/src/lib/fetch-github-stars.ts48
-rw-r--r--apps/web/src/lib/ffmpeg-utils.ts431
-rw-r--r--apps/web/src/lib/font-config.ts78
-rw-r--r--apps/web/src/lib/media-processing.ts162
-rw-r--r--apps/web/src/lib/rate-limit.ts32
-rw-r--r--apps/web/src/lib/storage/indexeddb-adapter.ts178
-rw-r--r--apps/web/src/lib/storage/opfs-adapter.ts114
-rw-r--r--apps/web/src/lib/storage/storage-service.ts440
-rw-r--r--apps/web/src/lib/storage/types.ts54
-rw-r--r--apps/web/src/lib/time.ts50
-rw-r--r--apps/web/src/lib/utils.ts60
-rw-r--r--apps/web/src/lib/waitlist.ts26
12 files changed, 817 insertions, 856 deletions
diff --git a/apps/web/src/lib/fetch-github-stars.ts b/apps/web/src/lib/fetch-github-stars.ts
index 74675be9..c4444a62 100644
--- a/apps/web/src/lib/fetch-github-stars.ts
+++ b/apps/web/src/lib/fetch-github-stars.ts
@@ -1,29 +1,29 @@
export async function getStars(): Promise<string> {
- try {
- const res = await fetch(
- "https://api.github.com/repos/OpenCut-app/OpenCut",
- {
- next: { revalidate: 3600 },
- },
- );
+ try {
+ const res = await fetch(
+ "https://api.github.com/repos/OpenCut-app/OpenCut",
+ {
+ next: { revalidate: 3600 },
+ }
+ );
- if (!res.ok) {
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
- }
- const data = (await res.json()) as { stargazers_count: number };
- const count = data.stargazers_count;
+ if (!res.ok) {
+ throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
+ }
+ const data = (await res.json()) as { stargazers_count: number };
+ const count = data.stargazers_count;
- if (typeof count !== "number") {
- throw new Error("Invalid stargazers_count from GitHub API");
- }
+ if (typeof count !== "number") {
+ throw new Error("Invalid stargazers_count from GitHub API");
+ }
- if (count >= 1_000_000)
- return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
- if (count >= 1_000)
- return (count / 1_000).toFixed(1).replace(/\.0$/, "") + "k";
- return count.toString();
- } catch (error) {
- console.error("Failed to fetch GitHub stars:", error);
- return "1.5k";
- }
+ if (count >= 1_000_000)
+ return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
+ if (count >= 1_000)
+ return (count / 1_000).toFixed(1).replace(/\.0$/, "") + "k";
+ return count.toString();
+ } catch (error) {
+ console.error("Failed to fetch GitHub stars:", error);
+ return "1.5k";
+ }
}
diff --git a/apps/web/src/lib/ffmpeg-utils.ts b/apps/web/src/lib/ffmpeg-utils.ts
index 80d28c21..ae0c267b 100644
--- a/apps/web/src/lib/ffmpeg-utils.ts
+++ b/apps/web/src/lib/ffmpeg-utils.ts
@@ -1,270 +1,231 @@
-import { FFmpeg } from "@ffmpeg/ffmpeg";
-import { toBlobURL } from "@ffmpeg/util";
+import { FFmpeg } from '@ffmpeg/ffmpeg';
+import { toBlobURL } from '@ffmpeg/util';
let ffmpeg: FFmpeg | null = null;
export const initFFmpeg = async (): Promise<FFmpeg> => {
- if (ffmpeg) return ffmpeg;
+ if (ffmpeg) return ffmpeg;
- ffmpeg = new FFmpeg();
+ ffmpeg = new FFmpeg();
+
+ // Use locally hosted files instead of CDN
+ const baseURL = '/ffmpeg';
+
+ await ffmpeg.load({
+ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
+ wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
+ });
- // Use locally hosted files instead of CDN
- const baseURL = "/ffmpeg";
-
- await ffmpeg.load({
- coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
- wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
- });
-
- return ffmpeg;
+ return ffmpeg;
};
export const generateThumbnail = async (
- videoFile: File,
- timeInSeconds: number = 1,
+ videoFile: File,
+ timeInSeconds: number = 1
): Promise<string> => {
- const ffmpeg = await initFFmpeg();
-
- const inputName = "input.mp4";
- const outputName = "thumbnail.jpg";
-
- // Write input file
- await ffmpeg.writeFile(
- inputName,
- new Uint8Array(await videoFile.arrayBuffer()),
- );
-
- // Generate thumbnail at specific time
- await ffmpeg.exec([
- "-i",
- inputName,
- "-ss",
- timeInSeconds.toString(),
- "-vframes",
- "1",
- "-vf",
- "scale=320:240",
- "-q:v",
- "2",
- outputName,
- ]);
-
- // Read output file
- const data = await ffmpeg.readFile(outputName);
- const blob = new Blob([data], { type: "image/jpeg" });
-
- // Cleanup
- await ffmpeg.deleteFile(inputName);
- await ffmpeg.deleteFile(outputName);
-
- return URL.createObjectURL(blob);
+ const ffmpeg = await initFFmpeg();
+
+ const inputName = 'input.mp4';
+ const outputName = 'thumbnail.jpg';
+
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
+
+ // Generate thumbnail at specific time
+ await ffmpeg.exec([
+ '-i', inputName,
+ '-ss', timeInSeconds.toString(),
+ '-vframes', '1',
+ '-vf', 'scale=320:240',
+ '-q:v', '2',
+ outputName
+ ]);
+
+ // Read output file
+ const data = await ffmpeg.readFile(outputName);
+ const blob = new Blob([data], { type: 'image/jpeg' });
+
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
+ await ffmpeg.deleteFile(outputName);
+
+ return URL.createObjectURL(blob);
};
export const trimVideo = async (
- videoFile: File,
- startTime: number,
- endTime: number,
- onProgress?: (progress: number) => void,
+ videoFile: File,
+ startTime: number,
+ endTime: number,
+ onProgress?: (progress: number) => void
): Promise<Blob> => {
- const ffmpeg = await initFFmpeg();
-
- const inputName = "input.mp4";
- const outputName = "output.mp4";
-
- // Set up progress callback
- if (onProgress) {
- ffmpeg.on("progress", ({ progress }) => {
- onProgress(progress * 100);
- });
- }
-
- // Write input file
- await ffmpeg.writeFile(
- inputName,
- new Uint8Array(await videoFile.arrayBuffer()),
- );
-
- const duration = endTime - startTime;
-
- // Trim video
- await ffmpeg.exec([
- "-i",
- inputName,
- "-ss",
- startTime.toString(),
- "-t",
- duration.toString(),
- "-c",
- "copy", // Use stream copy for faster processing
- outputName,
- ]);
-
- // Read output file
- const data = await ffmpeg.readFile(outputName);
- const blob = new Blob([data], { type: "video/mp4" });
-
- // Cleanup
- await ffmpeg.deleteFile(inputName);
- await ffmpeg.deleteFile(outputName);
-
- return blob;
+ const ffmpeg = await initFFmpeg();
+
+ const inputName = 'input.mp4';
+ const outputName = 'output.mp4';
+
+ // Set up progress callback
+ if (onProgress) {
+ ffmpeg.on('progress', ({ progress }) => {
+ onProgress(progress * 100);
+ });
+ }
+
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
+
+ const duration = endTime - startTime;
+
+ // Trim video
+ await ffmpeg.exec([
+ '-i', inputName,
+ '-ss', startTime.toString(),
+ '-t', duration.toString(),
+ '-c', 'copy', // Use stream copy for faster processing
+ outputName
+ ]);
+
+ // Read output file
+ const data = await ffmpeg.readFile(outputName);
+ const blob = new Blob([data], { type: 'video/mp4' });
+
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
+ await ffmpeg.deleteFile(outputName);
+
+ return blob;
};
-export const getVideoInfo = async (
- videoFile: File,
-): Promise<{
- duration: number;
- width: number;
- height: number;
- fps: number;
+export const getVideoInfo = async (videoFile: File): Promise<{
+ duration: number;
+ width: number;
+ height: number;
+ fps: number;
}> => {
- const ffmpeg = await initFFmpeg();
+ const ffmpeg = await initFFmpeg();
- const inputName = "input.mp4";
+ const inputName = 'input.mp4';
- // Write input file
- await ffmpeg.writeFile(
- inputName,
- new Uint8Array(await videoFile.arrayBuffer()),
- );
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
- // Capture FFmpeg stderr output with a one-time listener pattern
- let ffmpegOutput = "";
- let listening = true;
- const listener = (data: string) => {
- if (listening) ffmpegOutput += data;
- };
- ffmpeg.on("log", ({ message }) => listener(message));
+ // Capture FFmpeg stderr output with a one-time listener pattern
+ let ffmpegOutput = '';
+ let listening = true;
+ const listener = (data: string) => {
+ if (listening) ffmpegOutput += data;
+ };
+ ffmpeg.on('log', ({ message }) => listener(message));
- // Run ffmpeg to get info (stderr will contain the info)
- try {
- await ffmpeg.exec(["-i", inputName, "-f", "null", "-"]);
- } catch (error) {
- listening = false;
- await ffmpeg.deleteFile(inputName);
- console.error("FFmpeg execution failed:", error);
- throw new Error(
- "Failed to extract video info. The file may be corrupted or in an unsupported format.",
- );
- }
+ // Run ffmpeg to get info (stderr will contain the info)
+ try {
+ await ffmpeg.exec(['-i', inputName, '-f', 'null', '-']);
+ } catch (error) {
+ listening = false;
+ await ffmpeg.deleteFile(inputName);
+ console.error('FFmpeg execution failed:', error);
+ throw new Error('Failed to extract video info. The file may be corrupted or in an unsupported format.');
+ }
- // Disable listener after exec completes
- listening = false;
+ // Disable listener after exec completes
+ listening = false;
- // Cleanup
- await ffmpeg.deleteFile(inputName);
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
- // Parse output for duration, resolution, and fps
- // Example: Duration: 00:00:10.00, start: 0.000000, bitrate: 1234 kb/s
- // Example: Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn, 60 tbc
+ // Parse output for duration, resolution, and fps
+ // Example: Duration: 00:00:10.00, start: 0.000000, bitrate: 1234 kb/s
+ // Example: Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn, 60 tbc
- const durationMatch = ffmpegOutput.match(/Duration: (\d+):(\d+):([\d.]+)/);
- let duration = 0;
- if (durationMatch) {
- const [, h, m, s] = durationMatch;
- duration = parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s);
- }
+ const durationMatch = ffmpegOutput.match(/Duration: (\d+):(\d+):([\d.]+)/);
+ let duration = 0;
+ if (durationMatch) {
+ const [, h, m, s] = durationMatch;
+ duration = parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s);
+ }
- const videoStreamMatch = ffmpegOutput.match(
- /Video:.* (\d+)x(\d+)[^,]*, ([\d.]+) fps/,
- );
- let width = 0,
- height = 0,
- fps = 0;
- if (videoStreamMatch) {
- width = parseInt(videoStreamMatch[1]);
- height = parseInt(videoStreamMatch[2]);
- fps = parseFloat(videoStreamMatch[3]);
- }
+ const videoStreamMatch = ffmpegOutput.match(/Video:.* (\d+)x(\d+)[^,]*, ([\d.]+) fps/);
+ let width = 0, height = 0, fps = 0;
+ if (videoStreamMatch) {
+ width = parseInt(videoStreamMatch[1]);
+ height = parseInt(videoStreamMatch[2]);
+ fps = parseFloat(videoStreamMatch[3]);
+ }
- return {
- duration,
- width,
- height,
- fps,
- };
+ return {
+ duration,
+ width,
+ height,
+ fps
+ };
};
export const convertToWebM = async (
- videoFile: File,
- onProgress?: (progress: number) => void,
+ videoFile: File,
+ onProgress?: (progress: number) => void
): Promise<Blob> => {
- const ffmpeg = await initFFmpeg();
-
- const inputName = "input.mp4";
- const outputName = "output.webm";
-
- // Set up progress callback
- if (onProgress) {
- ffmpeg.on("progress", ({ progress }) => {
- onProgress(progress * 100);
- });
- }
-
- // Write input file
- await ffmpeg.writeFile(
- inputName,
- new Uint8Array(await videoFile.arrayBuffer()),
- );
-
- // Convert to WebM
- await ffmpeg.exec([
- "-i",
- inputName,
- "-c:v",
- "libvpx-vp9",
- "-crf",
- "30",
- "-b:v",
- "0",
- "-c:a",
- "libopus",
- outputName,
- ]);
-
- // Read output file
- const data = await ffmpeg.readFile(outputName);
- const blob = new Blob([data], { type: "video/webm" });
-
- // Cleanup
- await ffmpeg.deleteFile(inputName);
- await ffmpeg.deleteFile(outputName);
-
- return blob;
+ const ffmpeg = await initFFmpeg();
+
+ const inputName = 'input.mp4';
+ const outputName = 'output.webm';
+
+ // Set up progress callback
+ if (onProgress) {
+ ffmpeg.on('progress', ({ progress }) => {
+ onProgress(progress * 100);
+ });
+ }
+
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
+
+ // Convert to WebM
+ await ffmpeg.exec([
+ '-i', inputName,
+ '-c:v', 'libvpx-vp9',
+ '-crf', '30',
+ '-b:v', '0',
+ '-c:a', 'libopus',
+ outputName
+ ]);
+
+ // Read output file
+ const data = await ffmpeg.readFile(outputName);
+ const blob = new Blob([data], { type: 'video/webm' });
+
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
+ await ffmpeg.deleteFile(outputName);
+
+ return blob;
};
export const extractAudio = async (
- videoFile: File,
- format: "mp3" | "wav" = "mp3",
+ videoFile: File,
+ format: 'mp3' | 'wav' = 'mp3'
): Promise<Blob> => {
- const ffmpeg = await initFFmpeg();
-
- const inputName = "input.mp4";
- const outputName = `output.${format}`;
-
- // Write input file
- await ffmpeg.writeFile(
- inputName,
- new Uint8Array(await videoFile.arrayBuffer()),
- );
-
- // Extract audio
- await ffmpeg.exec([
- "-i",
- inputName,
- "-vn", // Disable video
- "-acodec",
- format === "mp3" ? "libmp3lame" : "pcm_s16le",
- outputName,
- ]);
-
- // Read output file
- const data = await ffmpeg.readFile(outputName);
- const blob = new Blob([data], { type: `audio/${format}` });
-
- // Cleanup
- await ffmpeg.deleteFile(inputName);
- await ffmpeg.deleteFile(outputName);
-
- return blob;
-};
+ const ffmpeg = await initFFmpeg();
+
+ const inputName = 'input.mp4';
+ const outputName = `output.${format}`;
+
+ // Write input file
+ await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
+
+ // Extract audio
+ await ffmpeg.exec([
+ '-i', inputName,
+ '-vn', // Disable video
+ '-acodec', format === 'mp3' ? 'libmp3lame' : 'pcm_s16le',
+ outputName
+ ]);
+
+ // Read output file
+ const data = await ffmpeg.readFile(outputName);
+ const blob = new Blob([data], { type: `audio/${format}` });
+
+ // Cleanup
+ await ffmpeg.deleteFile(inputName);
+ await ffmpeg.deleteFile(outputName);
+
+ return blob;
+}; \ No newline at end of file
diff --git a/apps/web/src/lib/font-config.ts b/apps/web/src/lib/font-config.ts
index 8b19609a..0389054a 100644
--- a/apps/web/src/lib/font-config.ts
+++ b/apps/web/src/lib/font-config.ts
@@ -1,39 +1,39 @@
-import {
- Inter,
- Roboto,
- Open_Sans,
- Playfair_Display,
- Comic_Neue,
-} from "next/font/google";
-
-// Configure all fonts
-const inter = Inter({ subsets: ["latin"] });
-const roboto = Roboto({ subsets: ["latin"], weight: ["400", "700"] });
-const openSans = Open_Sans({ subsets: ["latin"] });
-const playfairDisplay = Playfair_Display({ subsets: ["latin"] });
-const comicNeue = Comic_Neue({ subsets: ["latin"], weight: ["400", "700"] });
-
-// Export font class mapping for use in components
-export const FONT_CLASS_MAP = {
- Inter: inter.className,
- Roboto: roboto.className,
- "Open Sans": openSans.className,
- "Playfair Display": playfairDisplay.className,
- "Comic Neue": comicNeue.className,
- Arial: "",
- Helvetica: "",
- "Times New Roman": "",
- Georgia: "",
-} as const;
-
-// Export individual fonts for use in layout
-export const fonts = {
- inter,
- roboto,
- openSans,
- playfairDisplay,
- comicNeue,
-};
-
-// Default font for the body
-export const defaultFont = inter;
+import {
+ Inter,
+ Roboto,
+ Open_Sans,
+ Playfair_Display,
+ Comic_Neue,
+} from "next/font/google";
+
+// Configure all fonts
+const inter = Inter({ subsets: ["latin"] });
+const roboto = Roboto({ subsets: ["latin"], weight: ["400", "700"] });
+const openSans = Open_Sans({ subsets: ["latin"] });
+const playfairDisplay = Playfair_Display({ subsets: ["latin"] });
+const comicNeue = Comic_Neue({ subsets: ["latin"], weight: ["400", "700"] });
+
+// Export font class mapping for use in components
+export const FONT_CLASS_MAP = {
+ Inter: inter.className,
+ Roboto: roboto.className,
+ "Open Sans": openSans.className,
+ "Playfair Display": playfairDisplay.className,
+ "Comic Neue": comicNeue.className,
+ Arial: "",
+ Helvetica: "",
+ "Times New Roman": "",
+ Georgia: "",
+} as const;
+
+// Export individual fonts for use in layout
+export const fonts = {
+ inter,
+ roboto,
+ openSans,
+ playfairDisplay,
+ comicNeue,
+};
+
+// Default font for the body
+export const defaultFont = inter;
diff --git a/apps/web/src/lib/media-processing.ts b/apps/web/src/lib/media-processing.ts
index 738621b7..353c3113 100644
--- a/apps/web/src/lib/media-processing.ts
+++ b/apps/web/src/lib/media-processing.ts
@@ -1,101 +1,101 @@
import { toast } from "sonner";
import {
- getFileType,
- generateVideoThumbnail,
- getMediaDuration,
- getImageDimensions,
- type MediaItem,
+ getFileType,
+ generateVideoThumbnail,
+ getMediaDuration,
+ getImageDimensions,
+ type MediaItem,
} from "@/stores/media-store";
import { generateThumbnail, getVideoInfo } from "./ffmpeg-utils";
export interface ProcessedMediaItem extends Omit<MediaItem, "id"> {}
export async function processMediaFiles(
- files: FileList | File[],
- onProgress?: (progress: number) => void,
+ files: FileList | File[],
+ onProgress?: (progress: number) => void
): Promise<ProcessedMediaItem[]> {
- const fileArray = Array.from(files);
- const processedItems: ProcessedMediaItem[] = [];
+ const fileArray = Array.from(files);
+ const processedItems: ProcessedMediaItem[] = [];
- const total = fileArray.length;
- let completed = 0;
+ const total = fileArray.length;
+ let completed = 0;
- for (const file of fileArray) {
- const fileType = getFileType(file);
+ for (const file of fileArray) {
+ const fileType = getFileType(file);
- if (!fileType) {
- toast.error(`Unsupported file type: ${file.name}`);
- continue;
- }
+ if (!fileType) {
+ toast.error(`Unsupported file type: ${file.name}`);
+ continue;
+ }
- const url = URL.createObjectURL(file);
- let thumbnailUrl: string | undefined;
- let duration: number | undefined;
- let width: number | undefined;
- let height: number | undefined;
- let fps: number | undefined;
+ const url = URL.createObjectURL(file);
+ let thumbnailUrl: string | undefined;
+ let duration: number | undefined;
+ let width: number | undefined;
+ let height: number | undefined;
+ let fps: number | undefined;
- try {
- if (fileType === "image") {
- // Get image dimensions
- const dimensions = await getImageDimensions(file);
- width = dimensions.width;
- height = dimensions.height;
- } else if (fileType === "video") {
- try {
- // Use FFmpeg for comprehensive video info extraction
- const videoInfo = await getVideoInfo(file);
- duration = videoInfo.duration;
- width = videoInfo.width;
- height = videoInfo.height;
- fps = videoInfo.fps;
+ try {
+ if (fileType === "image") {
+ // Get image dimensions
+ const dimensions = await getImageDimensions(file);
+ width = dimensions.width;
+ height = dimensions.height;
+ } else if (fileType === "video") {
+ try {
+ // Use FFmpeg for comprehensive video info extraction
+ const videoInfo = await getVideoInfo(file);
+ duration = videoInfo.duration;
+ width = videoInfo.width;
+ height = videoInfo.height;
+ fps = videoInfo.fps;
- // Generate thumbnail using FFmpeg
- thumbnailUrl = await generateThumbnail(file, 1);
- } catch (error) {
- console.warn(
- "FFmpeg processing failed, falling back to basic processing:",
- error,
- );
- // Fallback to basic processing
- const videoResult = await generateVideoThumbnail(file);
- thumbnailUrl = videoResult.thumbnailUrl;
- width = videoResult.width;
- height = videoResult.height;
- duration = await getMediaDuration(file);
- // FPS will remain undefined for fallback
- }
- } else if (fileType === "audio") {
- // For audio, we don't set width/height/fps (they'll be undefined)
- duration = await getMediaDuration(file);
- }
+ // Generate thumbnail using FFmpeg
+ thumbnailUrl = await generateThumbnail(file, 1);
+ } catch (error) {
+ console.warn(
+ "FFmpeg processing failed, falling back to basic processing:",
+ error
+ );
+ // Fallback to basic processing
+ const videoResult = await generateVideoThumbnail(file);
+ thumbnailUrl = videoResult.thumbnailUrl;
+ width = videoResult.width;
+ height = videoResult.height;
+ duration = await getMediaDuration(file);
+ // FPS will remain undefined for fallback
+ }
+ } else if (fileType === "audio") {
+ // For audio, we don't set width/height/fps (they'll be undefined)
+ duration = await getMediaDuration(file);
+ }
- processedItems.push({
- name: file.name,
- type: fileType,
- file,
- url,
- thumbnailUrl,
- duration,
- width,
- height,
- fps,
- });
+ processedItems.push({
+ name: file.name,
+ type: fileType,
+ file,
+ url,
+ thumbnailUrl,
+ duration,
+ width,
+ height,
+ fps,
+ });
- // Yield back to the event loop to keep the UI responsive
- await new Promise((resolve) => setTimeout(resolve, 0));
+ // Yield back to the event loop to keep the UI responsive
+ await new Promise((resolve) => setTimeout(resolve, 0));
- completed += 1;
- if (onProgress) {
- const percent = Math.round((completed / total) * 100);
- onProgress(percent);
- }
- } catch (error) {
- console.error("Error processing file:", file.name, error);
- toast.error(`Failed to process ${file.name}`);
- URL.revokeObjectURL(url); // Clean up on error
- }
- }
+ completed += 1;
+ if (onProgress) {
+ const percent = Math.round((completed / total) * 100);
+ onProgress(percent);
+ }
+ } catch (error) {
+ console.error("Error processing file:", file.name, error);
+ toast.error(`Failed to process ${file.name}`);
+ URL.revokeObjectURL(url); // Clean up on error
+ }
+ }
- return processedItems;
+ return processedItems;
}
diff --git a/apps/web/src/lib/rate-limit.ts b/apps/web/src/lib/rate-limit.ts
index c139471c..ac63a64c 100644
--- a/apps/web/src/lib/rate-limit.ts
+++ b/apps/web/src/lib/rate-limit.ts
@@ -1,16 +1,16 @@
-// lib/rate-limit.ts
-import { Ratelimit } from "@upstash/ratelimit";
-import { Redis } from "@upstash/redis";
-import { env } from "@/env";
-
-const redis = new Redis({
- url: env.UPSTASH_REDIS_REST_URL,
- token: env.UPSTASH_REDIS_REST_TOKEN,
-});
-
-export const waitlistRateLimit = new Ratelimit({
- redis,
- limiter: Ratelimit.slidingWindow(5, "1 m"), // 5 requests per minute
- analytics: true,
- prefix: "waitlist-rate-limit",
-});
+// lib/rate-limit.ts
+import { Ratelimit } from "@upstash/ratelimit";
+import { Redis } from "@upstash/redis";
+import { env } from "@/env";
+
+const redis = new Redis({
+ url: env.UPSTASH_REDIS_REST_URL,
+ token: env.UPSTASH_REDIS_REST_TOKEN,
+});
+
+export const waitlistRateLimit = new Ratelimit({
+ redis,
+ limiter: Ratelimit.slidingWindow(5, "1 m"), // 5 requests per minute
+ analytics: true,
+ prefix: "waitlist-rate-limit",
+});
diff --git a/apps/web/src/lib/storage/indexeddb-adapter.ts b/apps/web/src/lib/storage/indexeddb-adapter.ts
index b599e2df..f43a8798 100644
--- a/apps/web/src/lib/storage/indexeddb-adapter.ts
+++ b/apps/web/src/lib/storage/indexeddb-adapter.ts
@@ -1,89 +1,89 @@
-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: number = 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();
- });
- }
-}
+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: number = 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
index ccd9735d..c01954e5 100644
--- a/apps/web/src/lib/storage/opfs-adapter.ts
+++ b/apps/web/src/lib/storage/opfs-adapter.ts
@@ -1,73 +1,73 @@
import { StorageAdapter } from "./types";
export class OPFSAdapter implements StorageAdapter<File> {
- private directoryName: string;
+ private directoryName: string;
- constructor(directoryName: string = "media") {
- this.directoryName = directoryName;
- }
+ constructor(directoryName: string = "media") {
+ this.directoryName = directoryName;
+ }
- private async getDirectory(): Promise<FileSystemDirectoryHandle> {
- const opfsRoot = await navigator.storage.getDirectory();
- return await opfsRoot.getDirectoryHandle(this.directoryName, {
- create: true,
- });
- }
+ 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 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();
+ 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();
- }
+ 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 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[] = [];
+ async list(): Promise<string[]> {
+ const directory = await this.getDirectory();
+ const keys: string[] = [];
- for await (const name of directory.keys()) {
- keys.push(name);
- }
+ for await (const name of directory.keys()) {
+ keys.push(name);
+ }
- return keys;
- }
+ return keys;
+ }
- async clear(): Promise<void> {
- const directory = await this.getDirectory();
+ async clear(): Promise<void> {
+ const directory = await this.getDirectory();
- for await (const name of directory.keys()) {
- await directory.removeEntry(name);
- }
- }
+ 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;
- }
+ // 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
index 044f2dff..0037dbdb 100644
--- a/apps/web/src/lib/storage/storage-service.ts
+++ b/apps/web/src/lib/storage/storage-service.ts
@@ -3,275 +3,275 @@ import { MediaItem } from "@/stores/media-store";
import { IndexedDBAdapter } from "./indexeddb-adapter";
import { OPFSAdapter } from "./opfs-adapter";
import {
- MediaFileData,
- StorageConfig,
- SerializedProject,
- TimelineData,
+ MediaFileData,
+ StorageConfig,
+ SerializedProject,
+ TimelineData,
} from "./types";
import { TimelineTrack } from "@/types/timeline";
class StorageService {
- private projectsAdapter: IndexedDBAdapter<SerializedProject>;
- private config: StorageConfig;
+ private projectsAdapter: IndexedDBAdapter<SerializedProject>;
+ private config: StorageConfig;
- constructor() {
- this.config = {
- projectsDb: "video-editor-projects",
- mediaDb: "video-editor-media",
- timelineDb: "video-editor-timelines",
- version: 1,
- };
+ constructor() {
+ this.config = {
+ projectsDb: "video-editor-projects",
+ mediaDb: "video-editor-media",
+ timelineDb: "video-editor-timelines",
+ version: 1,
+ };
- this.projectsAdapter = new IndexedDBAdapter<SerializedProject>(
- this.config.projectsDb,
- "projects",
- this.config.version,
- );
- }
+ this.projectsAdapter = new IndexedDBAdapter<SerializedProject>(
+ this.config.projectsDb,
+ "projects",
+ this.config.version
+ );
+ }
- // Helper to get project-specific media adapters
- private getProjectMediaAdapters(projectId: string) {
- const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>(
- `${this.config.mediaDb}-${projectId}`,
- "media-metadata",
- this.config.version,
- );
+ // Helper to get project-specific media adapters
+ private getProjectMediaAdapters(projectId: string) {
+ const mediaMetadataAdapter = new IndexedDBAdapter<MediaFileData>(
+ `${this.config.mediaDb}-${projectId}`,
+ "media-metadata",
+ this.config.version
+ );
- const mediaFilesAdapter = new OPFSAdapter(`media-files-${projectId}`);
+ const mediaFilesAdapter = new OPFSAdapter(`media-files-${projectId}`);
- return { mediaMetadataAdapter, mediaFilesAdapter };
- }
+ return { mediaMetadataAdapter, mediaFilesAdapter };
+ }
- // Helper to get project-specific timeline adapter
- private getProjectTimelineAdapter(projectId: string) {
- return new IndexedDBAdapter<TimelineData>(
- `${this.config.timelineDb}-${projectId}`,
- "timeline",
- this.config.version,
- );
- }
+ // Helper to get project-specific timeline adapter
+ private getProjectTimelineAdapter(projectId: string) {
+ return new IndexedDBAdapter<TimelineData>(
+ `${this.config.timelineDb}-${projectId}`,
+ "timeline",
+ this.config.version
+ );
+ }
- // Project operations
- async saveProject(project: TProject): Promise<void> {
- // Convert TProject to serializable format
- const serializedProject: SerializedProject = {
- id: project.id,
- name: project.name,
- thumbnail: project.thumbnail,
- createdAt: project.createdAt.toISOString(),
- updatedAt: project.updatedAt.toISOString(),
- backgroundColor: project.backgroundColor,
- backgroundType: project.backgroundType,
- blurIntensity: project.blurIntensity,
- };
+ // Project operations
+ async saveProject(project: TProject): Promise<void> {
+ // Convert TProject to serializable format
+ const serializedProject: SerializedProject = {
+ id: project.id,
+ name: project.name,
+ thumbnail: project.thumbnail,
+ createdAt: project.createdAt.toISOString(),
+ updatedAt: project.updatedAt.toISOString(),
+ backgroundColor: project.backgroundColor,
+ backgroundType: project.backgroundType,
+ blurIntensity: project.blurIntensity,
+ };
- await this.projectsAdapter.set(project.id, serializedProject);
- }
+ await this.projectsAdapter.set(project.id, serializedProject);
+ }
- async loadProject(id: string): Promise<TProject | null> {
- const serializedProject = await this.projectsAdapter.get(id);
+ async loadProject(id: string): Promise<TProject | null> {
+ const serializedProject = await this.projectsAdapter.get(id);
- if (!serializedProject) return null;
+ if (!serializedProject) return null;
- // Convert back to TProject format
- return {
- id: serializedProject.id,
- name: serializedProject.name,
- thumbnail: serializedProject.thumbnail,
- createdAt: new Date(serializedProject.createdAt),
- updatedAt: new Date(serializedProject.updatedAt),
- backgroundColor: serializedProject.backgroundColor,
- backgroundType: serializedProject.backgroundType,
- blurIntensity: serializedProject.blurIntensity,
- };
- }
+ // Convert back to TProject format
+ return {
+ id: serializedProject.id,
+ name: serializedProject.name,
+ thumbnail: serializedProject.thumbnail,
+ createdAt: new Date(serializedProject.createdAt),
+ updatedAt: new Date(serializedProject.updatedAt),
+ backgroundColor: serializedProject.backgroundColor,
+ backgroundType: serializedProject.backgroundType,
+ blurIntensity: serializedProject.blurIntensity,
+ };
+ }
- async loadAllProjects(): Promise<TProject[]> {
- const projectIds = await this.projectsAdapter.list();
- const projects: TProject[] = [];
+ 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);
- }
- }
+ 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(),
- );
- }
+ // Sort by last updated (most recent first)
+ return projects.sort(
+ (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
+ );
+ }
- async deleteProject(id: string): Promise<void> {
- await this.projectsAdapter.remove(id);
- }
+ async deleteProject(id: string): Promise<void> {
+ await this.projectsAdapter.remove(id);
+ }
- // Media operations - now project-specific
- async saveMediaItem(projectId: string, mediaItem: MediaItem): Promise<void> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters(projectId);
+ // Media operations - now project-specific
+ async saveMediaItem(projectId: string, mediaItem: MediaItem): Promise<void> {
+ const { mediaMetadataAdapter, mediaFilesAdapter } =
+ this.getProjectMediaAdapters(projectId);
- // Save file to project-specific OPFS
- await mediaFilesAdapter.set(mediaItem.id, mediaItem.file);
+ // 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,
- };
+ // 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,
+ };
- await mediaMetadataAdapter.set(mediaItem.id, metadata);
- }
+ await mediaMetadataAdapter.set(mediaItem.id, metadata);
+ }
- async loadMediaItem(
- projectId: string,
- id: string,
- ): Promise<MediaItem | null> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters(projectId);
+ async loadMediaItem(
+ projectId: string,
+ id: string
+ ): Promise<MediaItem | null> {
+ const { mediaMetadataAdapter, mediaFilesAdapter } =
+ this.getProjectMediaAdapters(projectId);
- const [file, metadata] = await Promise.all([
- mediaFilesAdapter.get(id),
- mediaMetadataAdapter.get(id),
- ]);
+ const [file, metadata] = await Promise.all([
+ mediaFilesAdapter.get(id),
+ mediaMetadataAdapter.get(id),
+ ]);
- if (!file || !metadata) return null;
+ if (!file || !metadata) return null;
- // Create new object URL for the file
- const url = URL.createObjectURL(file);
+ // Create new object URL for the file
+ const 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,
- // thumbnailUrl would need to be regenerated or cached separately
- };
- }
+ return {
+ id: metadata.id,
+ name: metadata.name,
+ type: metadata.type,
+ file,
+ url,
+ width: metadata.width,
+ height: metadata.height,
+ duration: metadata.duration,
+ // thumbnailUrl would need to be regenerated or cached separately
+ };
+ }
- async loadAllMediaItems(projectId: string): Promise<MediaItem[]> {
- const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId);
+ async loadAllMediaItems(projectId: string): Promise<MediaItem[]> {
+ const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId);
- const mediaIds = await mediaMetadataAdapter.list();
- const mediaItems: MediaItem[] = [];
+ const mediaIds = await mediaMetadataAdapter.list();
+ const mediaItems: MediaItem[] = [];
- for (const id of mediaIds) {
- const item = await this.loadMediaItem(projectId, id);
- if (item) {
- mediaItems.push(item);
- }
- }
+ for (const id of mediaIds) {
+ const item = await this.loadMediaItem(projectId, id);
+ if (item) {
+ mediaItems.push(item);
+ }
+ }
- return mediaItems;
- }
+ return mediaItems;
+ }
- async deleteMediaItem(projectId: string, id: string): Promise<void> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters(projectId);
+ async deleteMediaItem(projectId: string, id: string): Promise<void> {
+ const { mediaMetadataAdapter, mediaFilesAdapter } =
+ this.getProjectMediaAdapters(projectId);
- await Promise.all([
- mediaFilesAdapter.remove(id),
- mediaMetadataAdapter.remove(id),
- ]);
- }
+ await Promise.all([
+ mediaFilesAdapter.remove(id),
+ mediaMetadataAdapter.remove(id),
+ ]);
+ }
- async deleteProjectMedia(projectId: string): Promise<void> {
- const { mediaMetadataAdapter, mediaFilesAdapter } =
- this.getProjectMediaAdapters(projectId);
+ async deleteProjectMedia(projectId: string): Promise<void> {
+ const { mediaMetadataAdapter, mediaFilesAdapter } =
+ this.getProjectMediaAdapters(projectId);
- await Promise.all([
- mediaMetadataAdapter.clear(),
- mediaFilesAdapter.clear(),
- ]);
- }
+ await Promise.all([
+ mediaMetadataAdapter.clear(),
+ mediaFilesAdapter.clear(),
+ ]);
+ }
- // Timeline operations - now project-specific
- async saveTimeline(
- projectId: string,
- tracks: TimelineTrack[],
- ): Promise<void> {
- const timelineAdapter = this.getProjectTimelineAdapter(projectId);
- const timelineData: TimelineData = {
- tracks,
- lastModified: new Date().toISOString(),
- };
- await timelineAdapter.set("timeline", timelineData);
- }
+ // Timeline operations - now project-specific
+ async saveTimeline(
+ projectId: string,
+ tracks: TimelineTrack[]
+ ): Promise<void> {
+ const timelineAdapter = this.getProjectTimelineAdapter(projectId);
+ const timelineData: TimelineData = {
+ tracks,
+ lastModified: new Date().toISOString(),
+ };
+ await timelineAdapter.set("timeline", timelineData);
+ }
- async loadTimeline(projectId: string): Promise<TimelineTrack[] | null> {
- const timelineAdapter = this.getProjectTimelineAdapter(projectId);
- const timelineData = await timelineAdapter.get("timeline");
- return timelineData ? timelineData.tracks : null;
- }
+ async loadTimeline(projectId: string): Promise<TimelineTrack[] | null> {
+ const timelineAdapter = this.getProjectTimelineAdapter(projectId);
+ const timelineData = await timelineAdapter.get("timeline");
+ return timelineData ? timelineData.tracks : null;
+ }
- async deleteProjectTimeline(projectId: string): Promise<void> {
- const timelineAdapter = this.getProjectTimelineAdapter(projectId);
- await timelineAdapter.remove("timeline");
- }
+ async deleteProjectTimeline(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();
+ // 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
- }
+ // 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();
+ 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(),
- };
- }
+ return {
+ projects: projectIds.length,
+ isOPFSSupported: this.isOPFSSupported(),
+ isIndexedDBSupported: this.isIndexedDBSupported(),
+ };
+ }
- async getProjectStorageInfo(projectId: string): Promise<{
- mediaItems: number;
- hasTimeline: boolean;
- }> {
- const { mediaMetadataAdapter } = this.getProjectMediaAdapters(projectId);
- const timelineAdapter = this.getProjectTimelineAdapter(projectId);
+ async getProjectStorageInfo(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"),
- ]);
+ const [mediaIds, timelineData] = await Promise.all([
+ mediaMetadataAdapter.list(),
+ timelineAdapter.get("timeline"),
+ ]);
- return {
- mediaItems: mediaIds.length,
- hasTimeline: !!timelineData,
- };
- }
+ return {
+ mediaItems: mediaIds.length,
+ hasTimeline: !!timelineData,
+ };
+ }
- // Check browser support
- isOPFSSupported(): boolean {
- return OPFSAdapter.isSupported();
- }
+ // Check browser support
+ isOPFSSupported(): boolean {
+ return OPFSAdapter.isSupported();
+ }
- isIndexedDBSupported(): boolean {
- return "indexedDB" in window;
- }
+ isIndexedDBSupported(): boolean {
+ return "indexedDB" in window;
+ }
- isFullySupported(): boolean {
- return this.isIndexedDBSupported() && this.isOPFSSupported();
- }
+ isFullySupported(): boolean {
+ return this.isIndexedDBSupported() && this.isOPFSSupported();
+ }
}
// Export singleton instance
diff --git a/apps/web/src/lib/storage/types.ts b/apps/web/src/lib/storage/types.ts
index 1adbb3b8..b662c164 100644
--- a/apps/web/src/lib/storage/types.ts
+++ b/apps/web/src/lib/storage/types.ts
@@ -2,48 +2,48 @@ import { TProject } 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>;
+ 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;
- // File will be stored separately in OPFS
+ id: string;
+ name: string;
+ type: "image" | "video" | "audio";
+ size: number;
+ lastModified: number;
+ width?: number;
+ height?: number;
+ duration?: number;
+ // File will be stored separately in OPFS
}
export interface TimelineData {
- tracks: TimelineTrack[];
- lastModified: string;
+ tracks: TimelineTrack[];
+ lastModified: string;
}
export interface StorageConfig {
- projectsDb: string;
- mediaDb: string;
- timelineDb: string;
- version: number;
+ projectsDb: string;
+ mediaDb: string;
+ timelineDb: string;
+ version: number;
}
// Helper type for serialization - converts Date objects to strings
export type SerializedProject = Omit<TProject, "createdAt" | "updatedAt"> & {
- createdAt: string;
- updatedAt: string;
+ createdAt: string;
+ updatedAt: string;
};
// Extend FileSystemDirectoryHandle with missing async iterator methods
declare global {
- interface FileSystemDirectoryHandle {
- keys(): AsyncIterableIterator<string>;
- values(): AsyncIterableIterator<FileSystemHandle>;
- entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
- }
+ interface FileSystemDirectoryHandle {
+ keys(): AsyncIterableIterator<string>;
+ values(): AsyncIterableIterator<FileSystemHandle>;
+ entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
+ }
}
diff --git a/apps/web/src/lib/time.ts b/apps/web/src/lib/time.ts
index 6afcd03e..b3147946 100644
--- a/apps/web/src/lib/time.ts
+++ b/apps/web/src/lib/time.ts
@@ -1,25 +1,25 @@
-// Time-related utility functions
-
-// Helper function to format time in various formats (MM:SS, HH:MM:SS, HH:MM:SS:CS, HH:MM:SS:FF)
-export const formatTimeCode = (
- timeInSeconds: number,
- format: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF" = "HH:MM:SS:CS",
- fps: number = 30,
-): string => {
- const hours = Math.floor(timeInSeconds / 3600);
- const minutes = Math.floor((timeInSeconds % 3600) / 60);
- const seconds = Math.floor(timeInSeconds % 60);
- const centiseconds = Math.floor((timeInSeconds % 1) * 100);
- const frames = Math.floor((timeInSeconds % 1) * fps);
-
- switch (format) {
- case "MM:SS":
- return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
- case "HH:MM:SS":
- return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
- case "HH:MM:SS:CS":
- return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${centiseconds.toString().padStart(2, "0")}`;
- case "HH:MM:SS:FF":
- return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`;
- }
-};
+// Time-related utility functions
+
+// Helper function to format time in various formats (MM:SS, HH:MM:SS, HH:MM:SS:CS, HH:MM:SS:FF)
+export const formatTimeCode = (
+ timeInSeconds: number,
+ format: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF" = "HH:MM:SS:CS",
+ fps: number = 30
+): string => {
+ const hours = Math.floor(timeInSeconds / 3600);
+ const minutes = Math.floor((timeInSeconds % 3600) / 60);
+ const seconds = Math.floor(timeInSeconds % 60);
+ const centiseconds = Math.floor((timeInSeconds % 1) * 100);
+ const frames = Math.floor((timeInSeconds % 1) * fps);
+
+ switch (format) {
+ case "MM:SS":
+ return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
+ case "HH:MM:SS":
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
+ case "HH:MM:SS:CS":
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${centiseconds.toString().padStart(2, "0")}`;
+ case "HH:MM:SS:FF":
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`;
+ }
+};
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
index dc0463d9..fd47cc13 100644
--- a/apps/web/src/lib/utils.ts
+++ b/apps/web/src/lib/utils.ts
@@ -4,7 +4,7 @@ import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
+ return twMerge(clsx(inputs));
}
/**
@@ -12,46 +12,46 @@ export function cn(...inputs: ClassValue[]) {
* Uses crypto.randomUUID() if available, otherwise falls back to a custom implementation
*/
export function generateUUID(): string {
- // Use the native crypto.randomUUID if available
- if (
- typeof crypto !== "undefined" &&
- typeof crypto.randomUUID === "function"
- ) {
- return crypto.randomUUID();
- }
+ // Use the native crypto.randomUUID if available
+ if (
+ typeof crypto !== "undefined" &&
+ typeof crypto.randomUUID === "function"
+ ) {
+ return crypto.randomUUID();
+ }
- // Secure fallback using crypto.getRandomValues
- const bytes = new Uint8Array(16);
- crypto.getRandomValues(bytes);
+ // Secure fallback using crypto.getRandomValues
+ const bytes = new Uint8Array(16);
+ crypto.getRandomValues(bytes);
- // Set version 4 (UUIDv4)
- bytes[6] = (bytes[6] & 0x0f) | 0x40;
- // Set variant 10xxxxxx
- bytes[8] = (bytes[8] & 0x3f) | 0x80;
+ // Set version 4 (UUIDv4)
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
+ // Set variant 10xxxxxx
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
- const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
- return (
- hex.slice(0, 4).join("") +
- "-" +
- hex.slice(4, 6).join("") +
- "-" +
- hex.slice(6, 8).join("") +
- "-" +
- hex.slice(8, 10).join("") +
- "-" +
- hex.slice(10, 16).join("")
- );
+ return (
+ hex.slice(0, 4).join("") +
+ "-" +
+ hex.slice(4, 6).join("") +
+ "-" +
+ hex.slice(6, 8).join("") +
+ "-" +
+ hex.slice(8, 10).join("") +
+ "-" +
+ hex.slice(10, 16).join("")
+ );
}
export function isAppleDevice() {
- return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
+ return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
}
export function getPlatformSpecialKey() {
- return isAppleDevice() ? "⌘" : "Ctrl";
+ return isAppleDevice() ? "⌘" : "Ctrl";
}
export function getPlatformAlternateKey() {
- return isAppleDevice() ? "⌥" : "Alt";
+ return isAppleDevice() ? "⌥" : "Alt";
}
diff --git a/apps/web/src/lib/waitlist.ts b/apps/web/src/lib/waitlist.ts
index dacf2615..22b15b78 100644
--- a/apps/web/src/lib/waitlist.ts
+++ b/apps/web/src/lib/waitlist.ts
@@ -1,13 +1,13 @@
-import { db, sql, waitlist } from "@opencut/db";
-
-export async function getWaitlistCount() {
- try {
- const result = await db
- .select({ count: sql<number>`count(*)` })
- .from(waitlist);
- return result[0]?.count || 0;
- } catch (error) {
- console.error("Failed to fetch waitlist count:", error);
- return 0;
- }
-}
+import { db, sql, waitlist } from "@opencut/db";
+
+export async function getWaitlistCount() {
+ try {
+ const result = await db
+ .select({ count: sql<number>`count(*)` })
+ .from(waitlist);
+ return result[0]?.count || 0;
+ } catch (error) {
+ console.error("Failed to fetch waitlist count:", error);
+ return 0;
+ }
+}