summaryrefslogtreecommitdiff
path: root/apps/web/src/constants
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-01-31 00:20:04 +0100
committerGitHub <noreply@github.com>2026-01-31 00:20:04 +0100
commit7bf0984698e43b9aa2dd879f65951318ed410390 (patch)
treeccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/constants
parent0173db99448d1c4c6415dcf15272e17551b48b04 (diff)
downloadopencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/constants')
-rw-r--r--apps/web/src/constants/actions.ts295
-rw-r--r--apps/web/src/constants/editor-constants.ts15
-rw-r--r--apps/web/src/constants/export-constants.ts12
-rw-r--r--apps/web/src/constants/font-constants.ts122
-rw-r--r--apps/web/src/constants/language-constants.ts11
-rw-r--r--apps/web/src/constants/project-constants.ts27
-rw-r--r--apps/web/src/constants/site-constants.ts66
-rw-r--r--apps/web/src/constants/site.ts33
-rw-r--r--apps/web/src/constants/stickers-constants.ts16
-rw-r--r--apps/web/src/constants/text-constants.ts50
-rw-r--r--apps/web/src/constants/timeline-constants.ts107
-rw-r--r--apps/web/src/constants/timeline-constants.tsx72
-rw-r--r--apps/web/src/constants/transcription-constants.ts58
13 files changed, 364 insertions, 520 deletions
diff --git a/apps/web/src/constants/actions.ts b/apps/web/src/constants/actions.ts
deleted file mode 100644
index 50e8ae5d..00000000
--- a/apps/web/src/constants/actions.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-/* An `action` is a unique verb that is associated with certain thing that can be done on OpenCut.
- * For example, toggling playback or seeking.
- */
-
-import {
- useEffect,
- useRef,
- useState,
- useCallback,
- MutableRefObject,
-} from "react";
-
-// Simple event emitter for action changes
-class ActionEmitter {
- private listeners: Array<(actions: Action[]) => void> = [];
-
- subscribe(listener: (actions: Action[]) => void) {
- this.listeners.push(listener);
- return () => {
- this.listeners = this.listeners.filter((l) => l !== listener);
- };
- }
-
- emit(actions: Action[]) {
- this.listeners.forEach((listener) => listener(actions));
- }
-}
-
-const actionEmitter = new ActionEmitter();
-
-export type Action =
- | "toggle-play" // Toggle play/pause state
- | "stop-playback" // Stop playback
- | "seek-forward" // Seek forward in playback
- | "seek-backward" // Seek backward in playback
- | "frame-step-forward" // Step forward by one frame
- | "frame-step-backward" // Step backward by one frame
- | "jump-forward" // Jump forward by 5 seconds
- | "jump-backward" // Jump backward by 5 seconds
- | "goto-start" // Go to timeline start
- | "goto-end" // Go to timeline end
- | "split-element" // Split element at current time
- | "delete-selected" // Delete selected elements
- | "select-all" // Select all elements
- | "duplicate-selected" // Duplicate selected element
- | "toggle-snapping" // Toggle snapping
- | "undo" // Undo last action
- | "redo" // Redo last undone action
- | "copy-selected" // Copy selected elements to clipboard
- | "paste-selected"; // Paste elements from clipboard at playhead
-
-/**
- * Defines the arguments, if present for a given type that is required to be passed on
- * invocation and will be passed to action handlers.
- *
- * This type is supposed to be an object with the key being one of the actions mentioned above.
- * The value to the key can be anything.
- * If an action has no argument, you do not need to add it to this type.
- *
- * NOTE: We can't enforce type checks to make sure the key is Action, you
- * will know if you got something wrong if there is a type error in this file
- */
-type ActionArgsMap = {
- "seek-forward": { seconds: number } | undefined; // Args needed for seeking forward (default: 1)
- "seek-backward": { seconds: number } | undefined; // Args needed for seeking backward (default: 1)
- "jump-forward": { seconds: number } | undefined; // Args needed for jumping forward (default: 5)
- "jump-backward": { seconds: number } | undefined; // Args needed for jumping backward (default: 5)
-};
-
-type KeysWithValueUndefined<T> = {
- [K in keyof T]: undefined extends T[K] ? K : never;
-}[keyof T];
-
-/**
- * Actions which require arguments for their invocation
- */
-export type ActionWithArgs = keyof ActionArgsMap;
-
-/**
- * Actions which optionally takes in arguments for their invocation
- */
-
-export type ActionWithOptionalArgs =
- | ActionWithNoArgs
- | KeysWithValueUndefined<ActionArgsMap>;
-
-/**
- * Actions which do not require arguments for their invocation
- */
-export type ActionWithNoArgs = Exclude<Action, ActionWithArgs>;
-
-/**
- * Resolves the argument type for a given Action
- */
-type ArgOfHoppAction<A extends Action> = A extends ActionWithArgs
- ? ActionArgsMap[A]
- : undefined;
-
-/**
- * Resolves the action function for a given Action, used by action handler function defs
- */
-type ActionFunc<A extends Action> = A extends ActionWithArgs
- ? (arg: ArgOfHoppAction<A>, trigger?: InvocationTriggers) => void
- : (_?: undefined, trigger?: InvocationTriggers) => void;
-
-type BoundActionList = {
- [A in Action]?: Array<ActionFunc<A>>;
-};
-
-const boundActions: BoundActionList = {};
-
-let currentActiveActions: Action[] = [];
-
-function updateActiveActions() {
- const newActions = Object.keys(boundActions) as Action[];
- currentActiveActions = newActions;
- actionEmitter.emit(newActions);
-}
-
-export function bindAction<A extends Action>(
- action: A,
- handler: ActionFunc<A>
-) {
- if (boundActions[action]) {
- boundActions[action]?.push(handler);
- } else {
- // 'any' assertion because TypeScript doesn't seem to be able to figure out the links.
- boundActions[action] = [handler] as any;
- }
-
- updateActiveActions();
-}
-
-export type InvocationTriggers = "keypress" | "mouseclick";
-
-type InvokeActionFunc = {
- (
- action: ActionWithOptionalArgs,
- args?: undefined,
- trigger?: InvocationTriggers
- ): void;
- <A extends ActionWithArgs>(action: A, args: ActionArgsMap[A]): void;
-};
-
-/**
- * Invokes an action, triggering action handlers if any registered.
- * The second and third arguments are optional
- * @param action The action to fire
- * @param args The argument passed to the action handler. Optional if action has no args required
- * @param trigger Optionally supply the trigger that invoked the action (keypress/mouseclick)
- */
-export const invokeAction: InvokeActionFunc = <A extends Action>(
- action: A,
- args?: ArgOfHoppAction<A>,
- trigger?: InvocationTriggers
-) => {
- boundActions[action]?.forEach((handler) => (handler as any)(args, trigger));
-};
-
-export function unbindAction<A extends Action>(
- action: A,
- handler: ActionFunc<A>
-) {
- // 'any' assertion because TypeScript doesn't seem to be able to figure out the links.
- boundActions[action] = boundActions[action]?.filter(
- (x) => x !== handler
- ) as any;
-
- if (boundActions[action]?.length === 0) {
- delete boundActions[action];
- }
-
- updateActiveActions();
-}
-
-/**
- * Returns whether a given action is bound at a given time
- *
- * @param action The action to check
- */
-export function isActionBound(action: Action): boolean {
- return !!boundActions[action];
-}
-
-/**
- * A React hook that defines a component can handle a given
- * Action. The handler will be bound when the component is mounted
- * and unbound when the component is unmounted.
- * @param action The action to be bound
- * @param handler The function to be called when the action is invoked
- * @param isActive A ref that indicates whether the action is active
- */
-export function useActionHandler<A extends Action>(
- action: A,
- handler: ActionFunc<A>,
- isActive: MutableRefObject<boolean> | boolean | undefined
-) {
- const handlerRef = useRef(handler);
- const [isBound, setIsBound] = useState(false);
-
- // Update handler ref when handler changes
- useEffect(() => {
- handlerRef.current = handler;
- }, [handler]);
-
- // Create a stable handler wrapper
- const stableHandler = useCallback(
- (args: any, trigger?: InvocationTriggers) => {
- (handlerRef.current as any)(args, trigger);
- },
- []
- ) as ActionFunc<A>;
-
- useEffect(() => {
- const shouldBind =
- isActive === undefined ||
- (typeof isActive === "boolean" ? isActive : isActive.current);
-
- if (shouldBind && !isBound) {
- bindAction(action, stableHandler);
- setIsBound(true);
- } else if (!shouldBind && isBound) {
- unbindAction(action, stableHandler);
- setIsBound(false);
- }
-
- return () => {
- if (isBound) {
- unbindAction(action, stableHandler);
- setIsBound(false);
- }
- };
- }, [action, stableHandler, isActive, isBound]);
-
- // Handle ref-based isActive changes
- useEffect(() => {
- if (isActive && typeof isActive === "object" && "current" in isActive) {
- // Poll for ref changes
- const interval = setInterval(() => {
- const shouldBind = isActive.current;
- if (shouldBind !== isBound) {
- if (shouldBind) {
- bindAction(action, stableHandler);
- } else {
- unbindAction(action, stableHandler);
- }
- setIsBound(shouldBind);
- }
- }, 100);
- return () => clearInterval(interval);
- }
- }, [action, stableHandler, isActive, isBound]);
-}
-
-/**
- * A React hook that returns the current list of active actions
- * and re-renders when the list changes
- */
-export function useActiveActions(): Action[] {
- const [activeActions, setActiveActions] = useState<Action[]>([]);
-
- useEffect(() => {
- // Set initial value
- setActiveActions(currentActiveActions);
-
- // Subscribe to changes
- const unsubscribe = actionEmitter.subscribe(setActiveActions);
- return unsubscribe;
- }, []);
-
- return activeActions;
-}
-
-/**
- * A React hook that returns whether a specific action is currently bound
- * and re-renders when the binding state changes
- */
-export function useIsActionBound(action: Action): boolean {
- const [isBound, setIsBound] = useState(() => isActionBound(action));
-
- useEffect(() => {
- const updateBoundState = () => {
- setIsBound(isActionBound(action));
- };
-
- // Set initial value
- updateBoundState();
-
- // Subscribe to changes
- const unsubscribe = actionEmitter.subscribe(updateBoundState);
- return unsubscribe;
- }, [action]);
-
- return isBound;
-}
diff --git a/apps/web/src/constants/editor-constants.ts b/apps/web/src/constants/editor-constants.ts
new file mode 100644
index 00000000..d9570825
--- /dev/null
+++ b/apps/web/src/constants/editor-constants.ts
@@ -0,0 +1,15 @@
+import type { TPlatformLayout } from "@/types/editor";
+
+export const PLATFORM_LAYOUTS: Record<TPlatformLayout, string> = {
+ tiktok: "TikTok",
+};
+
+export const PANEL_CONFIG = {
+ panels: {
+ tools: 25,
+ preview: 50,
+ properties: 25,
+ mainContent: 70,
+ timeline: 30,
+ },
+};
diff --git a/apps/web/src/constants/export-constants.ts b/apps/web/src/constants/export-constants.ts
new file mode 100644
index 00000000..4e27aae5
--- /dev/null
+++ b/apps/web/src/constants/export-constants.ts
@@ -0,0 +1,12 @@
+import type { ExportOptions } from "@/types/export";
+
+export const DEFAULT_EXPORT_OPTIONS = {
+ format: "mp4",
+ quality: "high",
+ includeAudio: true,
+} satisfies ExportOptions;
+
+export const EXPORT_MIME_TYPES = {
+ webm: "video/webm",
+ mp4: "video/mp4",
+} as const;
diff --git a/apps/web/src/constants/font-constants.ts b/apps/web/src/constants/font-constants.ts
index a1c4e4d5..46f84848 100644
--- a/apps/web/src/constants/font-constants.ts
+++ b/apps/web/src/constants/font-constants.ts
@@ -1,66 +1,66 @@
export interface FontOption {
- value: string;
- label: string;
- category: "system" | "google" | "custom";
- weights?: number[];
- hasClassName?: boolean;
+ value: string;
+ label: string;
+ category: "system" | "google" | "custom";
+ weights?: number[];
+ hasClassName?: boolean;
}
export const FONT_OPTIONS: FontOption[] = [
- // System fonts (always available)
- { value: "Arial", label: "Arial", category: "system", hasClassName: false },
- {
- value: "Helvetica",
- label: "Helvetica",
- category: "system",
- hasClassName: false,
- },
- {
- value: "Times New Roman",
- label: "Times New Roman",
- category: "system",
- hasClassName: false,
- },
- {
- value: "Georgia",
- label: "Georgia",
- category: "system",
- hasClassName: false,
- },
+ // System fonts (always available)
+ { value: "Arial", label: "Arial", category: "system", hasClassName: false },
+ {
+ value: "Helvetica",
+ label: "Helvetica",
+ category: "system",
+ hasClassName: false,
+ },
+ {
+ value: "Times New Roman",
+ label: "Times New Roman",
+ category: "system",
+ hasClassName: false,
+ },
+ {
+ value: "Georgia",
+ label: "Georgia",
+ category: "system",
+ hasClassName: false,
+ },
- // Google Fonts (loaded in layout.tsx)
- {
- value: "Inter",
- label: "Inter",
- category: "google",
- weights: [400, 700],
- hasClassName: true,
- },
- {
- value: "Roboto",
- label: "Roboto",
- category: "google",
- weights: [400, 700],
- hasClassName: true,
- },
- {
- value: "Open Sans",
- label: "Open Sans",
- category: "google",
- hasClassName: true,
- },
- {
- value: "Playfair Display",
- label: "Playfair Display",
- category: "google",
- hasClassName: true,
- },
- {
- value: "Comic Neue",
- label: "Comic Neue",
- category: "google",
- hasClassName: false,
- },
+ // Google Fonts (loaded in layout.tsx)
+ {
+ value: "Inter",
+ label: "Inter",
+ category: "google",
+ weights: [400, 700],
+ hasClassName: true,
+ },
+ {
+ value: "Roboto",
+ label: "Roboto",
+ category: "google",
+ weights: [400, 700],
+ hasClassName: true,
+ },
+ {
+ value: "Open Sans",
+ label: "Open Sans",
+ category: "google",
+ hasClassName: true,
+ },
+ {
+ value: "Playfair Display",
+ label: "Playfair Display",
+ category: "google",
+ hasClassName: true,
+ },
+ {
+ value: "Comic Neue",
+ label: "Comic Neue",
+ category: "google",
+ hasClassName: false,
+ },
] as const;
export const DEFAULT_FONT = "Arial";
@@ -70,10 +70,10 @@ export type FontFamily = (typeof FONT_OPTIONS)[number]["value"];
// Helper functions
export const getFontByValue = (value: string): FontOption | undefined =>
- FONT_OPTIONS.find((font) => font.value === value);
+ FONT_OPTIONS.find((font) => font.value === value);
export const getGoogleFonts = (): FontOption[] =>
- FONT_OPTIONS.filter((font) => font.category === "google");
+ FONT_OPTIONS.filter((font) => font.category === "google");
export const getSystemFonts = (): FontOption[] =>
- FONT_OPTIONS.filter((font) => font.category === "system");
+ FONT_OPTIONS.filter((font) => font.category === "system");
diff --git a/apps/web/src/constants/language-constants.ts b/apps/web/src/constants/language-constants.ts
new file mode 100644
index 00000000..3da36661
--- /dev/null
+++ b/apps/web/src/constants/language-constants.ts
@@ -0,0 +1,11 @@
+export const LANGUAGES = [
+ { code: "en", name: "English" },
+ { code: "es", name: "Spanish" },
+ { code: "it", name: "Italian" },
+ { code: "fr", name: "French" },
+ { code: "de", name: "German" },
+ { code: "pt", name: "Portuguese" },
+ { code: "ru", name: "Russian" },
+ { code: "ja", name: "Japanese" },
+ { code: "zh", name: "Chinese" },
+] as const;
diff --git a/apps/web/src/constants/project-constants.ts b/apps/web/src/constants/project-constants.ts
new file mode 100644
index 00000000..7e3f06b6
--- /dev/null
+++ b/apps/web/src/constants/project-constants.ts
@@ -0,0 +1,27 @@
+import type { TCanvasSize } from "@/types/project";
+
+export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] = [
+ { width: 1920, height: 1080 },
+ { width: 1080, height: 1920 },
+ { width: 1080, height: 1080 },
+ { width: 1440, height: 1080 },
+];
+
+export const FPS_PRESETS = [
+ { value: "24", label: "24 fps" },
+ { value: "25", label: "25 fps" },
+ { value: "30", label: "30 fps" },
+ { value: "60", label: "60 fps" },
+ { value: "120", label: "120 fps" },
+] as const;
+
+export const BLUR_INTENSITY_PRESETS: { label: string; value: number }[] = [
+ { label: "Light", value: 4 },
+ { label: "Medium", value: 8 },
+ { label: "Heavy", value: 18 },
+] as const;
+
+export const DEFAULT_CANVAS_SIZE: TCanvasSize = { width: 1920, height: 1080 };
+export const DEFAULT_FPS = 30;
+export const DEFAULT_BLUR_INTENSITY = 8;
+export const DEFAULT_COLOR = "#000000";
diff --git a/apps/web/src/constants/site-constants.ts b/apps/web/src/constants/site-constants.ts
new file mode 100644
index 00000000..ac6cd2cd
--- /dev/null
+++ b/apps/web/src/constants/site-constants.ts
@@ -0,0 +1,66 @@
+import { OcDataBuddyIcon, OcMarbleIcon, } from "@opencut/ui/icons";
+
+export const SITE_URL = "https://opencut.app";
+
+export const SITE_INFO = {
+ title: "OpenCut",
+ description:
+ "A simple but powerful video editor that gets the job done. In your browser.",
+ url: SITE_URL,
+ openGraphImage: "/open-graph/default.jpg",
+ twitterImage: "/open-graph/default.jpg",
+ favicon: "/favicon.ico",
+};
+
+export type ExternalTool = {
+ name: string;
+ description: string;
+ url: string;
+ icon: React.ElementType;
+};
+
+export const EXTERNAL_TOOLS: ExternalTool[] = [
+ {
+ name: "Marble",
+ description:
+ "Modern headless CMS for content management and the blog for OpenCut",
+ url: "https://marblecms.com?utm_source=opencut",
+ icon: OcMarbleIcon,
+ },
+ {
+ name: "Databuddy",
+ description: "GDPR compliant analytics and user insights for OpenCut",
+ url: "https://databuddy.cc?utm_source=opencut",
+ icon: OcDataBuddyIcon,
+ },
+];
+
+export const DEFAULT_LOGO_URL = "/logos/opencut/svg/logo.svg";
+
+export const SOCIAL_LINKS = {
+ x: "https://x.com/opencutapp",
+ github: "https://github.com/OpenCut-app/OpenCut",
+ discord: "https://discord.com/invite/Mu3acKZvCp",
+};
+
+export type Sponsor = {
+ name: string;
+ url: string;
+ logo: string;
+ description: string;
+};
+
+export const SPONSORS: Sponsor[] = [
+ {
+ name: "Fal.ai",
+ url: "https://fal.ai?utm_source=opencut",
+ logo: "/logos/others/fal.svg",
+ description: "Generative image, video, and audio models all in one place.",
+ },
+ {
+ name: "Vercel",
+ url: "https://vercel.com?utm_source=opencut",
+ logo: "/logos/others/vercel.svg",
+ description: "Platform where we deploy and host OpenCut.",
+ },
+];
diff --git a/apps/web/src/constants/site.ts b/apps/web/src/constants/site.ts
deleted file mode 100644
index 00bd649f..00000000
--- a/apps/web/src/constants/site.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-export const SITE_URL = "https://opencut.app";
-
-export const SITE_INFO = {
- title: "OpenCut",
- description:
- "A simple but powerful video editor that gets the job done. In your browser.",
- url: SITE_URL,
- openGraphImage: "/open-graph/default.jpg",
- twitterImage: "/open-graph/default.jpg",
- favicon: "/favicon.ico",
-};
-
-export const EXTERNAL_TOOLS = [
- {
- name: "Marble",
- description:
- "Modern headless CMS for content management and the blog for OpenCut",
- url: "https://marblecms.com?utm_source=opencut",
- icon: "MarbleIcon" as const,
- },
- {
- name: "Vercel",
- description: "Platform where we deploy and host OpenCut",
- url: "https://vercel.com?utm_source=opencut",
- icon: "VercelIcon" as const,
- },
- {
- name: "Databuddy",
- description: "GDPR compliant analytics and user insights for OpenCut",
- url: "https://databuddy.cc?utm_source=opencut",
- icon: "DataBuddyIcon" as const,
- },
-];
diff --git a/apps/web/src/constants/stickers-constants.ts b/apps/web/src/constants/stickers-constants.ts
new file mode 100644
index 00000000..3c98d4f7
--- /dev/null
+++ b/apps/web/src/constants/stickers-constants.ts
@@ -0,0 +1,16 @@
+export const STICKER_CATEGORIES = [
+ "all",
+ "general",
+ "brands",
+ "emoji",
+] as const;
+
+export const STICKER_CATEGORY_CONFIG: Record<
+ (typeof STICKER_CATEGORIES)[number],
+ string | undefined
+> = {
+ all: undefined,
+ general: "General",
+ brands: "Brands / Social",
+ emoji: "Emoji",
+};
diff --git a/apps/web/src/constants/text-constants.ts b/apps/web/src/constants/text-constants.ts
index e3fcce38..c8a23655 100644
--- a/apps/web/src/constants/text-constants.ts
+++ b/apps/web/src/constants/text-constants.ts
@@ -1,27 +1,29 @@
-import { TextElement } from "@/types/timeline";
+import type { TextElement } from "@/types/timeline";
import { TIMELINE_CONSTANTS } from "./timeline-constants";
-export const DEFAULT_TEXT_ELEMENT: Omit<
- TextElement,
- "id"
-> = {
- type: "text",
- name: "Text",
- content: "Default Text",
- fontSize: 48,
- fontFamily: "Arial",
- color: "#ffffff",
- backgroundColor: "transparent",
- textAlign: "center",
- fontWeight: "normal",
- fontStyle: "normal",
- textDecoration: "none",
- x: 0,
- y: 0,
- rotation: 0,
- opacity: 1,
- duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
+export const DEFAULT_TEXT_ELEMENT: Omit<TextElement, "id"> = {
+ type: "text",
+ name: "Text",
+ content: "Default Text",
+ fontSize: 48,
+ fontFamily: "Arial",
+ color: "#ffffff",
+ backgroundColor: "transparent",
+ textAlign: "center",
+ fontWeight: "normal",
+ fontStyle: "normal",
+ textDecoration: "none",
+ duration: TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: {
+ scale: 1,
+ position: {
+ x: 0,
+ y: 0,
+ },
+ rotate: 0,
+ },
+ opacity: 1,
};
diff --git a/apps/web/src/constants/timeline-constants.ts b/apps/web/src/constants/timeline-constants.ts
deleted file mode 100644
index 4516775f..00000000
--- a/apps/web/src/constants/timeline-constants.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import type { TrackType } from "@/types/timeline";
-
-// Track color definitions
-export const TRACK_COLORS: Record<
- TrackType,
- { solid: string; background: string; border: string }
-> = {
- media: {
- solid: "bg-blue-500",
- background: "",
- border: "",
- },
- text: {
- solid: "bg-[#5DBAA0]",
- background: "bg-[#5DBAA0]",
- border: "",
- },
- audio: {
- solid: "bg-green-500",
- background: "bg-[#915DBE]",
- border: "",
- },
-} as const;
-
-// Utility functions
-export function getTrackColors(type: TrackType) {
- return TRACK_COLORS[type];
-}
-
-export function getTrackElementClasses(type: TrackType) {
- const colors = getTrackColors(type);
- return `${colors.background} ${colors.border}`;
-}
-
-// Track height definitions
-export const TRACK_HEIGHTS: Record<TrackType, number> = {
- media: 60,
- text: 25,
- audio: 50,
-} as const;
-
-// Utility function for track heights
-export function getTrackHeight(type: TrackType): number {
- return TRACK_HEIGHTS[type];
-}
-
-// Calculate cumulative height up to (but not including) a track index
-export function getCumulativeHeightBefore(
- tracks: Array<{ type: TrackType }>,
- trackIndex: number
-): number {
- const GAP = 4; // 4px gap between tracks (equivalent to Tailwind's gap-1)
- return tracks
- .slice(0, trackIndex)
- .reduce((sum, track) => sum + getTrackHeight(track.type) + GAP, 0);
-}
-
-// Calculate total height of all tracks
-export function getTotalTracksHeight(
- tracks: Array<{ type: TrackType }>
-): number {
- const GAP = 4; // 4px gap between tracks (equivalent to Tailwind's gap-1)
- const tracksHeight = tracks.reduce(
- (sum, track) => sum + getTrackHeight(track.type),
- 0
- );
- const gapsHeight = Math.max(0, tracks.length - 1) * GAP; // n-1 gaps for n tracks
- return tracksHeight + gapsHeight;
-}
-
-// Other timeline constants
-export const TIMELINE_CONSTANTS = {
- ELEMENT_MIN_WIDTH: 80,
- PIXELS_PER_SECOND: 50,
- TRACK_HEIGHT: 60, // Default fallback
- DEFAULT_TEXT_DURATION: 5,
- DEFAULT_IMAGE_DURATION: 5,
- ZOOM_LEVELS: [0.25, 0.5, 1, 1.5, 2, 3, 4],
-} as const;
-
-// FPS presets for project settings
-export const FPS_PRESETS = [
- { value: "24", label: "24 fps" },
- { value: "25", label: "25 fps" },
- { value: "30", label: "30 fps" },
- { value: "60", label: "60 fps" },
- { value: "120", label: "120 fps" },
-] as const;
-
-// Frame snapping utilities
-export function timeToFrame(time: number, fps: number): number {
- return Math.round(time * fps);
-}
-
-export function frameToTime(frame: number, fps: number): number {
- return frame / fps;
-}
-
-export function snapTimeToFrame(time: number, fps: number): number {
- if (fps <= 0) return time; // Fallback for invalid FPS
- const frame = timeToFrame(time, fps);
- return frameToTime(frame, fps);
-}
-
-export function getFrameDuration(fps: number): number {
- return 1 / fps;
-}
diff --git a/apps/web/src/constants/timeline-constants.tsx b/apps/web/src/constants/timeline-constants.tsx
new file mode 100644
index 00000000..692ccd65
--- /dev/null
+++ b/apps/web/src/constants/timeline-constants.tsx
@@ -0,0 +1,72 @@
+import type { TTimelineViewState } from "@/types/project";
+import type { TrackType } from "@/types/timeline";
+import {
+ Happy01Icon,
+ MusicNote03Icon,
+ TextIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { OcVideoIcon } from "@opencut/ui/icons";
+
+export const TRACK_COLORS: Record<TrackType, { background: string }> = {
+ video: {
+ background: "transparent",
+ },
+ text: {
+ background: "bg-[#5DBAA0]",
+ },
+ audio: {
+ background: "bg-[#915DBE]",
+ },
+ sticker: {
+ background: "bg-amber-500",
+ },
+} as const;
+
+export const TRACK_HEIGHTS: Record<TrackType, number> = {
+ video: 60,
+ text: 25,
+ audio: 50,
+ sticker: 50,
+} as const;
+
+export const TRACK_GAP = 4;
+
+export const TIMELINE_CONSTANTS = {
+ PIXELS_PER_SECOND: 50,
+ DEFAULT_ELEMENT_DURATION: 5,
+ PLAYHEAD_LOOKAHEAD_SECONDS: 30, // padding ahead
+ PADDING_TOP_PX: 0,
+ ZOOM_LEVELS: [0.1, 0.25, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 10, 15, 20, 30, 50],
+ ZOOM_MIN: 0.1,
+ ZOOM_MAX: 100,
+ ZOOM_STEP: 0.1,
+} as const;
+
+export const DEFAULT_TIMELINE_VIEW_STATE: TTimelineViewState = {
+ zoomLevel: 1,
+ scrollLeft: 0,
+ playheadTime: 0,
+};
+
+export const TRACK_ICONS: Record<TrackType, React.ReactNode> = {
+ video: <OcVideoIcon className="text-muted-foreground size-4 shrink-0" />,
+ text: (
+ <HugeiconsIcon
+ icon={TextIcon}
+ className="text-muted-foreground size-4 shrink-0"
+ />
+ ),
+ audio: (
+ <HugeiconsIcon
+ icon={MusicNote03Icon}
+ className="text-muted-foreground size-4 shrink-0"
+ />
+ ),
+ sticker: (
+ <HugeiconsIcon
+ icon={Happy01Icon}
+ className="text-muted-foreground size-4 shrink-0"
+ />
+ ),
+} as const;
diff --git a/apps/web/src/constants/transcription-constants.ts b/apps/web/src/constants/transcription-constants.ts
new file mode 100644
index 00000000..3fa914f2
--- /dev/null
+++ b/apps/web/src/constants/transcription-constants.ts
@@ -0,0 +1,58 @@
+import { LANGUAGES } from "@/constants/language-constants";
+import type {
+ TranscriptionModel,
+ TranscriptionModelId,
+} from "@/types/transcription";
+import type { LanguageCode } from "@/types/language";
+
+const SUPPORTED_TRANSCRIPTION_LANGS: ReadonlyArray<LanguageCode> = [
+ "en",
+ "es",
+ "it",
+ "fr",
+ "de",
+ "pt",
+ "ru",
+ "ja",
+ "zh",
+];
+
+export const TRANSCRIPTION_LANGUAGES = LANGUAGES.filter((language) =>
+ SUPPORTED_TRANSCRIPTION_LANGS.includes(language.code),
+);
+
+export const TRANSCRIPTION_MODELS: TranscriptionModel[] = [
+ {
+ id: "whisper-tiny",
+ name: "Tiny",
+ huggingFaceId: "onnx-community/whisper-tiny",
+ description: "Fastest, lower accuracy",
+ },
+ {
+ id: "whisper-small",
+ name: "Small",
+ huggingFaceId: "onnx-community/whisper-small",
+ description: "Good balance of speed and accuracy",
+ },
+ {
+ id: "whisper-medium",
+ name: "Medium",
+ huggingFaceId: "onnx-community/whisper-medium",
+ description: "Higher accuracy, slower",
+ },
+ {
+ id: "whisper-large-v3-turbo",
+ name: "Large v3 Turbo",
+ huggingFaceId: "onnx-community/whisper-large-v3-turbo",
+ description: "Best accuracy, requires WebGPU for good performance",
+ },
+];
+
+export const DEFAULT_TRANSCRIPTION_MODEL: TranscriptionModelId =
+ "whisper-small";
+
+export const DEFAULT_CHUNK_LENGTH_SECONDS = 30;
+export const DEFAULT_STRIDE_SECONDS = 5;
+
+export const DEFAULT_WORDS_PER_CAPTION = 3;
+export const MIN_CAPTION_DURATION_SECONDS = 0.8;