mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: add ultracite (#452)
* Init ultracite * Update scripts and biome.jsonc * Update biome.jsonc * Update biome.jsonc * Update biome.jsonc * Run format
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import type { MarbleAuthorList, MarbleCategoryList, MarblePost, MarblePostList, MarbleTagList } from '@/types/post';
|
||||
import type {
|
||||
MarbleAuthorList,
|
||||
MarbleCategoryList,
|
||||
MarblePost,
|
||||
MarblePostList,
|
||||
MarbleTagList,
|
||||
} from "@/types/post";
|
||||
import { unified } from "unified";
|
||||
import rehypeParse from "rehype-parse";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
@@ -6,50 +12,53 @@ import rehypeSlug from "rehype-slug";
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
import rehypeSanitize from "rehype-sanitize";
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_MARBLE_API_URL ?? "https://api.marblecms.com";
|
||||
const url =
|
||||
process.env.NEXT_PUBLIC_MARBLE_API_URL ?? "https://api.marblecms.com";
|
||||
const key = process.env.MARBLE_WORKSPACE_KEY ?? "cmd4iw9mm0006l804kwqv0k46";
|
||||
|
||||
async function fetchFromMarble<T>(endpoint: string): Promise<T> {
|
||||
try {
|
||||
const response = await fetch(`${url}/${key}/${endpoint}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${endpoint}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return await response.json() as T;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${endpoint}:`, error);
|
||||
throw error;
|
||||
try {
|
||||
const response = await fetch(`${url}/${key}/${endpoint}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch ${endpoint}: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${endpoint}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
export async function getPosts() {
|
||||
return fetchFromMarble<MarblePostList>('posts');
|
||||
}
|
||||
|
||||
export async function getTags() {
|
||||
return fetchFromMarble<MarbleTagList>('tags');
|
||||
}
|
||||
|
||||
export async function getSinglePost(slug: string) {
|
||||
return fetchFromMarble<MarblePost>(`posts/${slug}`);
|
||||
}
|
||||
|
||||
export async function getCategories() {
|
||||
return fetchFromMarble<MarbleCategoryList>('categories');
|
||||
}
|
||||
|
||||
export async function getAuthors() {
|
||||
return fetchFromMarble<MarbleAuthorList>('authors');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPosts() {
|
||||
return fetchFromMarble<MarblePostList>("posts");
|
||||
}
|
||||
|
||||
export async function getTags() {
|
||||
return fetchFromMarble<MarbleTagList>("tags");
|
||||
}
|
||||
|
||||
export async function getSinglePost(slug: string) {
|
||||
return fetchFromMarble<MarblePost>(`posts/${slug}`);
|
||||
}
|
||||
|
||||
export async function getCategories() {
|
||||
return fetchFromMarble<MarbleCategoryList>("categories");
|
||||
}
|
||||
|
||||
export async function getAuthors() {
|
||||
return fetchFromMarble<MarbleAuthorList>("authors");
|
||||
}
|
||||
|
||||
export async function processHtmlContent(html: string): Promise<string> {
|
||||
const processor = unified()
|
||||
.use(rehypeSanitize)
|
||||
.use(rehypeParse, { fragment: true })
|
||||
.use(rehypeSlug)
|
||||
.use(rehypeAutolinkHeadings, { behavior: "append" })
|
||||
.use(rehypeStringify);
|
||||
const processor = unified()
|
||||
.use(rehypeSanitize)
|
||||
.use(rehypeParse, { fragment: true })
|
||||
.use(rehypeSlug)
|
||||
.use(rehypeAutolinkHeadings, { behavior: "append" })
|
||||
.use(rehypeStringify);
|
||||
|
||||
const file = await processor.process(html);
|
||||
return String(file);
|
||||
const file = await processor.process(html);
|
||||
return String(file);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ export async function getStars(): Promise<string> {
|
||||
|
||||
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";
|
||||
if (count >= 1000)
|
||||
return (count / 1000).toFixed(1).replace(/\.0$/, "") + "k";
|
||||
return count.toString();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch GitHub stars:", error);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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;
|
||||
|
||||
@@ -7,13 +7,13 @@ export const initFFmpeg = async (): Promise<FFmpeg> => {
|
||||
if (ffmpeg) return ffmpeg;
|
||||
|
||||
ffmpeg = new FFmpeg();
|
||||
|
||||
|
||||
// Use locally hosted files instead of CDN
|
||||
const baseURL = '/ffmpeg';
|
||||
|
||||
const baseURL = "/ffmpeg";
|
||||
|
||||
await ffmpeg.load({
|
||||
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
|
||||
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
|
||||
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
|
||||
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
|
||||
});
|
||||
|
||||
return ffmpeg;
|
||||
@@ -21,34 +21,42 @@ export const initFFmpeg = async (): Promise<FFmpeg> => {
|
||||
|
||||
export const generateThumbnail = async (
|
||||
videoFile: File,
|
||||
timeInSeconds: number = 1
|
||||
timeInSeconds = 1
|
||||
): Promise<string> => {
|
||||
const ffmpeg = await initFFmpeg();
|
||||
|
||||
const inputName = 'input.mp4';
|
||||
const outputName = 'thumbnail.jpg';
|
||||
|
||||
|
||||
const inputName = "input.mp4";
|
||||
const outputName = "thumbnail.jpg";
|
||||
|
||||
// Write input file
|
||||
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
|
||||
|
||||
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
|
||||
"-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' });
|
||||
|
||||
const blob = new Blob([data], { type: "image/jpeg" });
|
||||
|
||||
// Cleanup
|
||||
await ffmpeg.deleteFile(inputName);
|
||||
await ffmpeg.deleteFile(outputName);
|
||||
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
@@ -59,43 +67,52 @@ export const trimVideo = async (
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<Blob> => {
|
||||
const ffmpeg = await initFFmpeg();
|
||||
|
||||
const inputName = 'input.mp4';
|
||||
const outputName = 'output.mp4';
|
||||
|
||||
|
||||
const inputName = "input.mp4";
|
||||
const outputName = "output.mp4";
|
||||
|
||||
// Set up progress callback
|
||||
if (onProgress) {
|
||||
ffmpeg.on('progress', ({ progress }) => {
|
||||
ffmpeg.on("progress", ({ progress }) => {
|
||||
onProgress(progress * 100);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Write input file
|
||||
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
|
||||
|
||||
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
|
||||
"-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' });
|
||||
|
||||
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<{
|
||||
export const getVideoInfo = async (
|
||||
videoFile: File
|
||||
): Promise<{
|
||||
duration: number;
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -103,27 +120,32 @@ export const getVideoInfo = async (videoFile: File): Promise<{
|
||||
}> => {
|
||||
const ffmpeg = await initFFmpeg();
|
||||
|
||||
const inputName = 'input.mp4';
|
||||
const inputName = "input.mp4";
|
||||
|
||||
// Write input file
|
||||
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
|
||||
await ffmpeg.writeFile(
|
||||
inputName,
|
||||
new Uint8Array(await videoFile.arrayBuffer())
|
||||
);
|
||||
|
||||
// Capture FFmpeg stderr output with a one-time listener pattern
|
||||
let ffmpegOutput = '';
|
||||
let ffmpegOutput = "";
|
||||
let listening = true;
|
||||
const listener = (data: string) => {
|
||||
if (listening) ffmpegOutput += data;
|
||||
};
|
||||
ffmpeg.on('log', ({ message }) => listener(message));
|
||||
ffmpeg.on("log", ({ message }) => listener(message));
|
||||
|
||||
// Run ffmpeg to get info (stderr will contain the info)
|
||||
try {
|
||||
await ffmpeg.exec(['-i', inputName, '-f', 'null', '-']);
|
||||
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.');
|
||||
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
|
||||
@@ -143,8 +165,12 @@ export const getVideoInfo = async (videoFile: File): Promise<{
|
||||
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;
|
||||
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]);
|
||||
@@ -155,7 +181,7 @@ export const getVideoInfo = async (videoFile: File): Promise<{
|
||||
duration,
|
||||
width,
|
||||
height,
|
||||
fps
|
||||
fps,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -164,68 +190,81 @@ export const convertToWebM = async (
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<Blob> => {
|
||||
const ffmpeg = await initFFmpeg();
|
||||
|
||||
const inputName = 'input.mp4';
|
||||
const outputName = 'output.webm';
|
||||
|
||||
|
||||
const inputName = "input.mp4";
|
||||
const outputName = "output.webm";
|
||||
|
||||
// Set up progress callback
|
||||
if (onProgress) {
|
||||
ffmpeg.on('progress', ({ progress }) => {
|
||||
ffmpeg.on("progress", ({ progress }) => {
|
||||
onProgress(progress * 100);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Write input file
|
||||
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
|
||||
|
||||
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
|
||||
"-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' });
|
||||
|
||||
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'
|
||||
format: "mp3" | "wav" = "mp3"
|
||||
): Promise<Blob> => {
|
||||
const ffmpeg = await initFFmpeg();
|
||||
|
||||
const inputName = 'input.mp4';
|
||||
|
||||
const inputName = "input.mp4";
|
||||
const outputName = `output.${format}`;
|
||||
|
||||
|
||||
// Write input file
|
||||
await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer()));
|
||||
|
||||
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
|
||||
"-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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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 = 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { StorageAdapter } from "./types";
|
||||
export class OPFSAdapter implements StorageAdapter<File> {
|
||||
private directoryName: string;
|
||||
|
||||
constructor(directoryName: string = "media") {
|
||||
constructor(directoryName = "media") {
|
||||
this.directoryName = directoryName;
|
||||
}
|
||||
|
||||
|
||||
+25
-25
@@ -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 = 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")}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
import { TimelineElement } from "@/types/timeline";
|
||||
|
||||
// Helper function to check for element overlaps and prevent invalid timeline states
|
||||
export const checkElementOverlaps = (elements: TimelineElement[]): boolean => {
|
||||
// Sort elements by start time
|
||||
const sortedElements = [...elements].sort(
|
||||
(a, b) => a.startTime - b.startTime
|
||||
);
|
||||
|
||||
for (let i = 0; i < sortedElements.length - 1; i++) {
|
||||
const current = sortedElements[i];
|
||||
const next = sortedElements[i + 1];
|
||||
|
||||
const currentEnd =
|
||||
current.startTime +
|
||||
(current.duration - current.trimStart - current.trimEnd);
|
||||
|
||||
// Check if current element overlaps with next element
|
||||
if (currentEnd > next.startTime) return true; // Overlap detected
|
||||
}
|
||||
|
||||
return false; // No overlaps
|
||||
};
|
||||
|
||||
// Helper function to resolve overlaps by adjusting element positions
|
||||
export const resolveElementOverlaps = (
|
||||
elements: TimelineElement[]
|
||||
): TimelineElement[] => {
|
||||
// Sort elements by start time
|
||||
const sortedElements = [...elements].sort(
|
||||
(a, b) => a.startTime - b.startTime
|
||||
);
|
||||
const resolvedElements: TimelineElement[] = [];
|
||||
|
||||
for (let i = 0; i < sortedElements.length; i++) {
|
||||
const current = { ...sortedElements[i] };
|
||||
|
||||
if (resolvedElements.length > 0) {
|
||||
const previous = resolvedElements[resolvedElements.length - 1];
|
||||
const previousEnd =
|
||||
previous.startTime +
|
||||
(previous.duration - previous.trimStart - previous.trimEnd);
|
||||
|
||||
// If current element would overlap with previous, push it after previous ends
|
||||
if (current.startTime < previousEnd) {
|
||||
current.startTime = previousEnd;
|
||||
}
|
||||
}
|
||||
|
||||
resolvedElements.push(current);
|
||||
}
|
||||
|
||||
return resolvedElements;
|
||||
};
|
||||
import { TimelineElement } from "@/types/timeline";
|
||||
|
||||
// Helper function to check for element overlaps and prevent invalid timeline states
|
||||
export const checkElementOverlaps = (elements: TimelineElement[]): boolean => {
|
||||
// Sort elements by start time
|
||||
const sortedElements = [...elements].sort(
|
||||
(a, b) => a.startTime - b.startTime
|
||||
);
|
||||
|
||||
for (let i = 0; i < sortedElements.length - 1; i++) {
|
||||
const current = sortedElements[i];
|
||||
const next = sortedElements[i + 1];
|
||||
|
||||
const currentEnd =
|
||||
current.startTime +
|
||||
(current.duration - current.trimStart - current.trimEnd);
|
||||
|
||||
// Check if current element overlaps with next element
|
||||
if (currentEnd > next.startTime) return true; // Overlap detected
|
||||
}
|
||||
|
||||
return false; // No overlaps
|
||||
};
|
||||
|
||||
// Helper function to resolve overlaps by adjusting element positions
|
||||
export const resolveElementOverlaps = (
|
||||
elements: TimelineElement[]
|
||||
): TimelineElement[] => {
|
||||
// Sort elements by start time
|
||||
const sortedElements = [...elements].sort(
|
||||
(a, b) => a.startTime - b.startTime
|
||||
);
|
||||
const resolvedElements: TimelineElement[] = [];
|
||||
|
||||
for (let i = 0; i < sortedElements.length; i++) {
|
||||
const current = { ...sortedElements[i] };
|
||||
|
||||
if (resolvedElements.length > 0) {
|
||||
const previous = resolvedElements[resolvedElements.length - 1];
|
||||
const previousEnd =
|
||||
previous.startTime +
|
||||
(previous.duration - previous.trimStart - previous.trimEnd);
|
||||
|
||||
// If current element would overlap with previous, push it after previous ends
|
||||
if (current.startTime < previousEnd) {
|
||||
current.startTime = previousEnd;
|
||||
}
|
||||
}
|
||||
|
||||
resolvedElements.push(current);
|
||||
}
|
||||
|
||||
return resolvedElements;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user