refactor: restructure files to their domains, new preview overlay system, and dep graph

This commit is contained in:
Maze Winther 2026-04-20 11:31:17 +02:00
parent 729d10592f
commit 3e89d29985
491 changed files with 3565 additions and 2372 deletions

View File

@ -5,8 +5,8 @@ import { toast } from "sonner";
import {
type KeyboardShortcut,
useKeyboardShortcutsHelp,
} from "@/hooks/use-keyboard-shortcuts-help";
import { useKeybindingsStore } from "@/stores/keybindings-store";
} from "@/actions/use-keyboard-shortcuts-help";
import { useKeybindingsStore } from "@/actions/keybindings-store";
import { Button } from "@/components/ui/button";
import {
Dialog,

View File

@ -1,7 +1,7 @@
import type {
KeybindingConfig,
ShortcutKey,
} from "@/lib/actions/keybinding";
} from "@/actions/keybinding";
import type { TActionWithOptionalArgs } from "./types";
export type TActionCategory =

View File

@ -2,11 +2,11 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { TActionWithOptionalArgs } from "@/lib/actions";
import { getDefaultShortcuts } from "@/lib/actions";
import type { TActionWithOptionalArgs } from "@/actions";
import { getDefaultShortcuts } from "@/actions";
import { isTypableDOMElement } from "@/utils/browser";
import { isAppleDevice } from "@/utils/platform";
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
import type { KeybindingConfig, ShortcutKey } from "@/actions/keybinding";
import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations";
const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();

View File

@ -1,5 +1,5 @@
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
import type { TActionWithOptionalArgs } from "@/lib/actions";
import type { KeybindingConfig, ShortcutKey } from "@/actions/keybinding";
import type { TActionWithOptionalArgs } from "@/actions";
interface V2State {
keybindings: KeybindingConfig;

View File

@ -1,6 +1,6 @@
import type { TActionWithOptionalArgs } from "@/lib/actions";
import type { ShortcutKey } from "@/lib/actions/keybinding";
import type { KeybindingConfig } from "@/lib/actions/keybinding";
import type { TActionWithOptionalArgs } from "@/actions";
import type { ShortcutKey } from "@/actions/keybinding";
import type { KeybindingConfig } from "@/actions/keybinding";
interface V3State {
keybindings: KeybindingConfig;

View File

@ -1,4 +1,4 @@
import type { KeybindingConfig } from "@/lib/actions/keybinding";
import type { KeybindingConfig } from "@/actions/keybinding";
interface V4State {
keybindings: KeybindingConfig;

View File

@ -1,4 +1,4 @@
import type { KeybindingConfig } from "@/lib/actions/keybinding";
import type { KeybindingConfig } from "@/actions/keybinding";
interface V5State {
keybindings: KeybindingConfig;

View File

@ -1,4 +1,4 @@
import type { KeybindingConfig } from "@/lib/actions/keybinding";
import type { KeybindingConfig } from "@/actions/keybinding";
interface V6State {
keybindings: KeybindingConfig;

View File

@ -5,8 +5,8 @@ import type {
TActionHandlerOptions,
TArgOfAction,
TInvocationTrigger,
} from "@/lib/actions";
import { bindAction, unbindAction } from "@/lib/actions";
} from "@/actions";
import { bindAction, unbindAction } from "@/actions";
export function useActionHandler<A extends TAction>(
action: A,

View File

@ -1,21 +1,21 @@
"use client";
import { useEffect, useRef } from "react";
import { useTimelineStore } from "@/stores/timeline-store";
import { useActionHandler } from "@/hooks/actions/use-action-handler";
import { useEditor } from "../use-editor";
import { useElementSelection } from "../timeline/element/use-element-selection";
import { TICKS_PER_SECOND } from "@/lib/wasm";
import { useKeyframeSelection } from "../timeline/element/use-keyframe-selection";
import { getElementsAtTime, hasMediaId } from "@/lib/timeline";
import { cancelInteraction } from "@/lib/cancel-interaction";
import { invokeAction } from "@/lib/actions";
import { canToggleSourceAudio } from "@/lib/timeline/audio-separation";
import { useTimelineStore } from "@/timeline/timeline-store";
import { useActionHandler } from "@/actions/use-action-handler";
import { useEditor } from "@/editor/use-editor";
import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
import { TICKS_PER_SECOND } from "@/wasm";
import { useKeyframeSelection } from "@/timeline/hooks/element/use-keyframe-selection";
import { getElementsAtTime, hasMediaId } from "@/timeline";
import { cancelInteraction } from "@/editor/cancel-interaction";
import { invokeAction } from "@/actions";
import { canToggleSourceAudio } from "@/timeline/audio-separation";
import {
activateScope,
clearActiveScope,
type ScopeEntry,
} from "@/lib/selection/scope";
} from "@/selection/scope";
export function useEditorActions() {
const editor = useEditor();

View File

@ -1,7 +1,7 @@
import { useEffect } from "react";
import { invokeAction } from "@/lib/actions";
import { useEditor } from "@/hooks/use-editor";
import { useKeybindingsStore } from "@/stores/keybindings-store";
import { invokeAction } from "@/actions";
import { useEditor } from "@/editor/use-editor";
import { useKeybindingsStore } from "@/actions/keybindings-store";
import { isTypableDOMElement } from "@/utils/browser";
/**

View File

@ -1,8 +1,8 @@
"use client";
import { useMemo } from "react";
import { useKeybindingsStore } from "@/stores/keybindings-store";
import { ACTIONS, type TActionWithOptionalArgs } from "@/lib/actions";
import { useKeybindingsStore } from "@/actions/keybindings-store";
import { ACTIONS, type TActionWithOptionalArgs } from "@/actions";
import {
getPlatformAlternateKey,
getPlatformSpecialKey,

View File

@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
composeAnimationValue,
createAnimationBinding,
} from "@/lib/animation/binding-values";
} from "@/animation/binding-values";
describe("binding values", () => {
test("formats composed animated colors as hex", () => {

View File

@ -1,4 +1,4 @@
import type { ScalarAnimationKey } from "@/lib/animation/types";
import type { ScalarAnimationKey } from "@/animation/types";
const BEZIER_SOLVE_ITERATIONS = 20;

View File

@ -12,7 +12,7 @@ import type {
DiscreteValue,
Vector2AnimationBinding,
VectorValue,
} from "@/lib/animation/types";
} from "@/animation/types";
import { clamp } from "@/utils/math";
interface LinearRgba {

View File

@ -1,12 +1,12 @@
import {
getDefaultLeftHandle,
getDefaultRightHandle,
} from "@/lib/animation/bezier";
} from "@/animation/bezier";
import type {
CurveHandle,
NormalizedCubicBezier,
ScalarAnimationKey,
} from "@/lib/animation/types";
} from "@/animation/types";
const VALUE_EPSILON = 1e-6;

View File

@ -1,9 +1,9 @@
import type { ParamValues } from "@/lib/params";
import type { Effect } from "@/lib/effects/types";
import type { ParamValues } from "@/params";
import type { Effect } from "@/effects/types";
import type {
ElementAnimations,
EffectParamPath,
} from "@/lib/animation/types";
} from "@/animation/types";
import { removeElementKeyframe } from "./keyframes";
import { resolveAnimationPathValueAtTime } from "./resolve";

View File

@ -5,7 +5,7 @@ import type {
ScalarAnimationChannel,
ScalarGraphChannel,
ScalarGraphKeyframeContext,
} from "@/lib/animation/types";
} from "@/animation/types";
export interface EditableScalarChannels {
binding: AnimationBindingInstance;

View File

@ -1,12 +1,12 @@
import type {
ElementAnimations,
GraphicParamPath,
} from "@/lib/animation/types";
import type { ParamValues } from "@/lib/params";
} from "@/animation/types";
import type { ParamValues } from "@/params";
import {
getGraphicDefinition,
resolveGraphicParams,
} from "@/lib/graphics";
} from "@/graphics";
import { resolveAnimationPathValueAtTime } from "./resolve";
export const GRAPHIC_PARAM_PATH_PREFIX = "params.";

View File

@ -7,7 +7,7 @@ import type {
ScalarAnimationChannel,
ScalarAnimationKey,
ScalarSegmentType,
} from "@/lib/animation/types";
} from "@/animation/types";
import { clamp } from "@/utils/math";
import {
getBezierPoint,

View File

@ -4,7 +4,7 @@ import type {
AnimationPath,
ElementAnimations,
ElementKeyframe,
} from "@/lib/animation/types";
} from "@/animation/types";
import {
type AnimationComponentValue,
composeAnimationValue,

View File

@ -13,7 +13,7 @@ import type {
ScalarAnimationKey,
ScalarCurveKeyframePatch,
ScalarSegmentType,
} from "@/lib/animation/types";
} from "@/animation/types";
import { generateUUID } from "@/utils/id";
import {
cloneAnimationBinding,

View File

@ -2,8 +2,8 @@ import type {
AnimationPropertyGroup,
AnimationPropertyPath,
ElementAnimations,
} from "@/lib/animation/types";
import { ANIMATION_PROPERTY_GROUPS } from "@/lib/animation/types";
} from "@/animation/types";
import { ANIMATION_PROPERTY_GROUPS } from "@/animation/types";
import { getKeyframeAtTime } from "./keyframe-query";
export interface GroupKeyframeRef {

View File

@ -3,20 +3,20 @@ import type {
AnimationInterpolation,
AnimationPropertyPath,
AnimationValue,
} from "@/lib/animation/types";
} from "@/animation/types";
import { parseColorToLinearRgba } from "./binding-values";
import type { TimelineElement } from "@/lib/timeline";
import { MIN_TRANSFORM_SCALE } from "@/lib/animation/transform";
import type { TimelineElement } from "@/timeline";
import { MIN_TRANSFORM_SCALE } from "@/animation/transform";
import {
CORNER_RADIUS_MAX,
CORNER_RADIUS_MIN,
} from "@/lib/text/background";
} from "@/text/background";
import {
canElementHaveAudio,
isVisualElement,
} from "@/lib/timeline/element-utils";
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants";
import { DEFAULTS } from "@/lib/timeline/defaults";
} from "@/timeline/element-utils";
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/timeline/audio-constants";
import { DEFAULTS } from "@/timeline/defaults";
import { snapToStep } from "@/utils/math";
export interface NumericSpec {

View File

@ -5,8 +5,8 @@ import type {
AnimationPropertyPath,
AnimationValueForPath,
ElementAnimations,
} from "@/lib/animation/types";
import type { Transform } from "@/lib/rendering";
} from "@/animation/types";
import type { Transform } from "@/rendering";
import {
type AnimationComponentValue,
composeAnimationValue,

View File

@ -3,20 +3,20 @@ import type {
AnimationInterpolation,
AnimationPath,
AnimationValue,
} from "@/lib/animation/types";
} from "@/animation/types";
import {
parseEffectParamPath,
isEffectParamPath,
} from "@/lib/animation/effect-param-channel";
} from "@/animation/effect-param-channel";
import {
isGraphicParamPath,
parseGraphicParamPath,
} from "@/lib/animation/graphic-param-channel";
import type { ParamDefinition } from "@/lib/params";
import { effectsRegistry, registerDefaultEffects } from "@/lib/effects";
import { getGraphicDefinition } from "@/lib/graphics";
import type { TimelineElement } from "@/lib/timeline";
import { isVisualElement } from "@/lib/timeline/element-utils";
} from "@/animation/graphic-param-channel";
import type { ParamDefinition } from "@/params";
import { effectsRegistry, registerDefaultEffects } from "@/effects";
import { getGraphicDefinition } from "@/graphics";
import type { TimelineElement } from "@/timeline";
import { isVisualElement } from "@/timeline/element-utils";
import { snapToStep } from "@/utils/math";
import {
coerceAnimationValueForProperty,

View File

@ -0,0 +1,35 @@
import { getElementKeyframes } from "@/animation";
import type { SceneTracks } from "@/timeline";
import type { SnapPoint } from "@/timeline/snapping";
export function getAnimationKeyframeSnapPointsForTimeline({
tracks,
excludeElementIds,
}: {
tracks: SceneTracks;
excludeElementIds?: Set<string>;
}): SnapPoint[] {
const snapPoints: SnapPoint[] = [];
const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio];
for (const track of orderedTracks) {
for (const element of track.elements) {
if (excludeElementIds?.has(element.id)) {
continue;
}
for (const keyframe of getElementKeyframes({
animations: element.animations,
})) {
snapPoints.push({
time: element.startTime + keyframe.time,
type: "keyframe",
elementId: element.id,
trackId: track.id,
});
}
}
}
return snapPoints;
}

View File

@ -1,4 +1,4 @@
import type { ParamValues } from "@/lib/params";
import type { ParamValues } from "@/params";
export const ANIMATION_PROPERTY_PATHS = [
"transform.positionX",

View File

@ -1,4 +1,4 @@
import { auth } from "@/lib/auth/server";
import { auth } from "@/auth/server";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);

View File

@ -1,7 +1,7 @@
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { checkRateLimit } from "@/lib/rate-limit";
import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/lib/feedback";
import { checkRateLimit } from "@/auth/rate-limit";
import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/feedback";
const submitSchema = z.object({
message: z

View File

@ -1,7 +1,7 @@
import { webEnv } from "@/lib/env/web";
import { webEnv } from "@/env/web";
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { checkRateLimit } from "@/lib/rate-limit";
import { checkRateLimit } from "@/auth/rate-limit";
const searchParamsSchema = z.object({
q: z.string().max(500, "Query too long").optional(),

View File

@ -4,8 +4,8 @@ import { notFound } from "next/navigation";
import { BasePage } from "@/app/base-page";
import Prose from "@/components/ui/prose";
import { Separator } from "@/components/ui/separator";
import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query";
import type { Author, Post } from "@/lib/blog/types";
import { getPosts, getSinglePost, processHtmlContent } from "@/blog/query";
import type { Author, Post } from "@/blog/types";
type PageProps = {
params: Promise<{ slug: string }>;

View File

@ -2,8 +2,8 @@ import type { Metadata } from "next";
import Link from "next/link";
import { BasePage } from "@/app/base-page";
import { Separator } from "@/components/ui/separator";
import { getPosts } from "@/lib/blog/query";
import type { Post } from "@/lib/blog/types";
import { getPosts } from "@/blog/query";
import type { Post } from "@/blog/types";
export const metadata: Metadata = {
title: "Blog - OpenCut",

View File

@ -3,15 +3,15 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { BasePage } from "@/app/base-page";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { getReleaseByVersion, getSortedReleases } from "@/lib/changelog/utils";
import { getReleaseByVersion, getSortedReleases } from "@/changelog/utils";
import {
ReleaseArticle,
ReleaseMeta,
ReleaseTitle,
ReleaseDescription,
ReleaseChanges,
} from "@/lib/changelog/components/release";
import { CopyMarkdownButton } from "@/lib/changelog/components/copy-markdown-button";
} from "@/changelog/components/release";
import { CopyMarkdownButton } from "@/changelog/components/copy-markdown-button";
type Props = { params: Promise<{ version: string }> };

View File

@ -4,14 +4,14 @@ import { Separator } from "@/components/ui/separator";
import {
type Release as ReleaseType,
getSortedReleases,
} from "@/lib/changelog/utils";
} from "@/changelog/utils";
import {
ReleaseArticle,
ReleaseMeta,
ReleaseTitle,
ReleaseDescription,
ReleaseChanges,
} from "@/lib/changelog/components/release";
} from "@/changelog/components/release";
export const metadata: Metadata = {
title: "Changelog - OpenCut",

View File

@ -3,7 +3,7 @@ import Link from "next/link";
import { GitHubContributeSection } from "@/components/gitHub-contribute-section";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import { EXTERNAL_TOOLS } from "@/lib/site/external-tools";
import { EXTERNAL_TOOLS } from "@/site/external-tools";
import { BasePage } from "../base-page";
export const metadata: Metadata = {

View File

@ -8,21 +8,32 @@ import {
} from "@/components/ui/resizable";
import { AssetsPanel } from "@/components/editor/panels/assets";
import { PropertiesPanel } from "@/components/editor/panels/properties";
import { Timeline } from "@/components/editor/panels/timeline";
import { PreviewPanel } from "@/components/editor/panels/preview";
import { Timeline } from "@/timeline/components";
import { PreviewPanel } from "@/preview/components";
import { EditorHeader } from "@/components/editor/editor-header";
import { EditorProvider } from "@/components/providers/editor-provider";
import { Onboarding } from "@/components/editor/onboarding";
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
import { usePanelStore } from "@/stores/panel-store";
import { usePasteMedia } from "@/hooks/use-paste-media";
import { MigrationDialog } from "@/project/components/migration-dialog";
import { usePanelStore } from "@/editor/panel-store";
import { usePasteMedia } from "@/media/use-paste-media";
import { MobileGate } from "@/components/editor/mobile-gate";
import { useState } from "react";
import { useEditor } from "@/hooks/use-editor";
import { useMemo, useState } from "react";
import { useEditor } from "@/editor/use-editor";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button } from "@/components/ui/button";
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
import { ChangelogNotification } from "@/changelog/components/changelog-notification";
import {
createPreviewOverlayControl,
isPreviewOverlayVisible,
mergePreviewOverlaySources,
} from "@/preview/overlays";
import { usePreviewStore } from "@/preview/preview-store";
import { getGuidePreviewOverlaySource } from "@/guides";
import {
bookmarkNotesPreviewOverlay,
getBookmarkPreviewOverlaySource,
} from "@/timeline/bookmarks/index";
export default function Editor() {
const params = useParams();
@ -70,6 +81,49 @@ function DegradedRendererBanner() {
function EditorLayout() {
usePasteMedia();
const { panels, setPanel } = usePanelStore();
const activeScene = useEditor((editor) =>
editor.scenes.getActiveSceneOrNull(),
);
const currentTime = useEditor((editor) => editor.playback.getCurrentTime());
const activeGuide = usePreviewStore((state) => state.activeGuide);
const overlays = usePreviewStore((state) => state.overlays);
const setOverlayVisibility = usePreviewStore(
(state) => state.setOverlayVisibility,
);
const showBookmarkNotes = isPreviewOverlayVisible({
overlay: bookmarkNotesPreviewOverlay,
overlays,
});
const overlaySource = useMemo(
() =>
mergePreviewOverlaySources({
sources: [
getGuidePreviewOverlaySource({
guideId: activeGuide,
}),
activeScene
? getBookmarkPreviewOverlaySource({
bookmarks: activeScene.bookmarks,
time: currentTime,
isVisible: showBookmarkNotes,
})
: {
definitions: [bookmarkNotesPreviewOverlay],
instances: [],
},
],
}),
[activeGuide, activeScene, currentTime, showBookmarkNotes],
);
const overlayControls = useMemo(
() =>
overlaySource.definitions.map((overlay) =>
createPreviewOverlayControl({ overlay, overlays }),
),
[overlaySource.definitions, overlays],
);
return (
<ResizablePanelGroup
@ -111,7 +165,11 @@ function EditorLayout() {
minSize={30}
className="min-h-0 min-w-0 flex-1"
>
<PreviewPanel />
<PreviewPanel
overlayControls={overlayControls}
overlayInstances={overlaySource.instances}
onOverlayVisibilityChange={setOverlayVisibility}
/>
</ResizablePanel>
<ResizableHandle withHandle />

View File

@ -2,11 +2,11 @@ import { ThemeProvider } from "next-themes";
import Script from "next/script";
import "./globals.css";
import { Toaster } from "../components/ui/sonner";
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
import { ChangelogNotification } from "@/changelog/components/changelog-notification";
import { TooltipProvider } from "../components/ui/tooltip";
import { baseMetaData } from "./metadata";
import { BotIdClient } from "botid/client";
import { webEnv } from "@/lib/env/web";
import { webEnv } from "@/env/web";
import { Inter } from "next/font/google";
const siteFont = Inter({ subsets: ["latin"] });

View File

@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { SITE_INFO, SITE_URL } from "@/lib/site/brand";
import { SITE_INFO, SITE_URL } from "@/site/brand";
export const baseMetaData: Metadata = {
metadataBase: new URL(SITE_URL),

View File

@ -2,7 +2,7 @@ import { Hero } from "@/components/landing/hero";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import type { Metadata } from "next";
import { SITE_URL } from "@/lib/site/brand";
import { SITE_URL } from "@/site/brand";
export const metadata: Metadata = {
alternates: {

View File

@ -7,7 +7,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Separator } from "@/components/ui/separator";
import { SOCIAL_LINKS } from "@/lib/site/social";
import { SOCIAL_LINKS } from "@/site/social";
export const metadata: Metadata = {
title: "Privacy Policy - OpenCut",

View File

@ -7,20 +7,20 @@ import type { KeyboardEvent, MouseEvent } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import type { EditorCore } from "@/core";
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
import { StoragePersistenceDialog } from "@/components/editor/dialogs/storage-persistence-dialog";
import { MigrationDialog } from "@/project/components/migration-dialog";
import { StoragePersistenceDialog } from "@/services/storage/components/storage-persistence-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { useEditor } from "@/hooks/use-editor";
import { useEditor } from "@/editor/use-editor";
import { useProjectsStore } from "./store";
import type {
TProjectMetadata,
TProjectSortKey,
TProjectSortOption,
} from "@/lib/project/types";
} from "@/project/types";
import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm";
import { formatDate } from "@/utils/date";
import { HugeiconsIcon } from "@hugeicons/react";
@ -62,11 +62,11 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { DeleteProjectDialog } from "@/components/editor/dialogs/delete-project-dialog";
import { ProjectInfoDialog } from "@/components/editor/dialogs/project-info-dialog";
import { RenameProjectDialog } from "@/components/editor/dialogs/rename-project-dialog";
import { DeleteProjectDialog } from "@/project/components/delete-project-dialog";
import { ProjectInfoDialog } from "@/project/components/project-info-dialog";
import { RenameProjectDialog } from "@/project/components/rename-project-dialog";
import { cn } from "@/utils/ui";
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
import { ChangelogNotification } from "@/changelog/components/changelog-notification";
const formatProjectDuration = ({
duration,
}: {

View File

@ -1,6 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { TProjectSortKey } from "@/lib/project/types";
import type { TProjectSortKey } from "@/project/types";
export type ProjectsViewMode = "grid" | "list";

View File

@ -1,5 +1,5 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/site/brand";
import { SITE_URL } from "@/site/brand";
export default function robots(): MetadataRoute.Robots {
return {

View File

@ -1,6 +1,6 @@
import { Feed } from "feed";
import { getPosts } from "@/lib/blog/query";
import { SITE_INFO, SITE_URL } from "@/lib/site/brand";
import { getPosts } from "@/blog/query";
import { SITE_INFO, SITE_URL } from "@/site/brand";
export async function GET() {
try {

View File

@ -1,5 +1,5 @@
import { SITE_URL } from "@/lib/site/brand";
import { getPosts } from "@/lib/blog/query";
import { SITE_URL } from "@/site/brand";
import { getPosts } from "@/blog/query";
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {

View File

@ -3,7 +3,7 @@ import Image from "next/image";
import Link from "next/link";
import { BasePage } from "@/app/base-page";
import { Card, CardContent } from "@/components/ui/card";
import { SPONSORS, type Sponsor } from "@/lib/site/sponsors";
import { SPONSORS, type Sponsor } from "@/site/sponsors";
import { HugeiconsIcon } from "@hugeicons/react";
import { LinkSquare02Icon } from "@hugeicons/core-free-icons";
import { cn } from "@/utils/ui";

View File

@ -7,7 +7,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Separator } from "@/components/ui/separator";
import { SOCIAL_LINKS } from "@/lib/site/social";
import { SOCIAL_LINKS } from "@/site/social";
export const metadata: Metadata = {
title: "Terms of Service - OpenCut",

View File

@ -1,5 +1,5 @@
import { createAuthClient } from "better-auth/react";
import { webEnv } from "@/lib/env/web";
import { webEnv } from "@/env/web";
export const { signIn, signUp, useSession } = createAuthClient({
baseURL: webEnv.NEXT_PUBLIC_SITE_URL,

View File

@ -1,6 +1,6 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { webEnv } from "@/lib/env/web";
import { webEnv } from "@/env/web";
const redis = new Redis({
url: webEnv.UPSTASH_REDIS_REST_URL,

View File

@ -1,8 +1,8 @@
import { betterAuth, type RateLimit } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { Redis } from "@upstash/redis";
import { db } from "@/lib/db";
import { webEnv } from "@/lib/env/web";
import { db } from "@/db";
import { webEnv } from "@/env/web";
const redis = new Redis({
url: webEnv.UPSTASH_REDIS_REST_URL,

View File

@ -5,7 +5,7 @@ import type {
MarblePost,
MarblePostList,
MarbleTagList,
} from "@/lib/blog/types";
} from "@/blog/types";
import { unified } from "unified";
import rehypeParse from "rehype-parse";
import rehypeStringify from "rehype-stringify";

View File

@ -1,4 +1,4 @@
import type { TCanvasSize } from "@/lib/project/types";
import type { TCanvasSize } from "@/project/types";
export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] = [
{ width: 1920, height: 1080 },

View File

@ -1,4 +1,4 @@
import { PasteCommand } from "@/lib/commands/timeline";
import { PasteCommand } from "@/commands/timeline";
import type { ClipboardHandler } from "../types";
export const ElementsClipboardHandler = {

View File

@ -1,7 +1,7 @@
import { getKeyframeById } from "@/lib/animation";
import type { SelectedKeyframeRef } from "@/lib/animation/types";
import type { TimelineElement } from "@/lib/timeline";
import { PasteKeyframesCommand } from "@/lib/commands/timeline";
import { getKeyframeById } from "@/animation";
import type { SelectedKeyframeRef } from "@/animation/types";
import type { TimelineElement } from "@/timeline";
import { PasteKeyframesCommand } from "@/commands/timeline";
import type {
ClipboardHandler,
KeyframeClipboardCurvePatch,

View File

@ -5,13 +5,13 @@ import type {
AnimationValue,
ScalarCurveKeyframePatch,
SelectedKeyframeRef,
} from "@/lib/animation/types";
import type { Command } from "@/lib/commands/base-command";
} from "@/animation/types";
import type { Command } from "@/commands/base-command";
import type {
CreateTimelineElement,
ElementRef,
TrackType,
} from "@/lib/timeline";
} from "@/timeline";
export interface ElementClipboardItem {
trackId: string;

View File

@ -1,5 +1,5 @@
import type { EditorSelectionPatch } from "@/lib/selection/editor-selection";
import type { ElementRef } from "@/lib/timeline/types";
import type { EditorSelectionPatch } from "@/selection/editor-selection";
import type { ElementRef } from "@/timeline/types";
export interface CommandResult {
selection?: EditorSelectionPatch;

View File

@ -1,13 +1,13 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { toast } from "sonner";
import type { MediaAsset } from "@/lib/media/types";
import type { MediaAsset } from "@/media/types";
import { generateUUID } from "@/utils/id";
import { storageService } from "@/services/storage/service";
import type { FrameRate } from "opencut-wasm";
import { hasMediaId } from "@/lib/timeline/element-utils";
import { frameRatesEqual, getHighestImportedVideoFps } from "@/lib/fps/utils";
import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
import { hasMediaId } from "@/timeline/element-utils";
import { frameRatesEqual, getHighestImportedVideoFps } from "@/fps/utils";
import { UpdateProjectSettingsCommand } from "@/commands/project";
export class AddMediaAssetCommand extends Command {
private assetId: string;

View File

@ -1,12 +1,12 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { MediaAsset } from "@/lib/media/types";
import { buildWaveformSourceKey } from "@/lib/media/waveform-summary";
import type { MediaAsset } from "@/media/types";
import { buildWaveformSourceKey } from "@/media/waveform-summary";
import { storageService } from "@/services/storage/service";
import { videoCache } from "@/services/video-cache/service";
import { waveformCache } from "@/services/waveform-cache/service";
import { hasMediaId } from "@/lib/timeline/element-utils";
import type { SceneTracks } from "@/lib/timeline";
import { hasMediaId } from "@/timeline/element-utils";
import type { SceneTracks } from "@/timeline";
export class RemoveMediaAssetCommand extends Command {
private savedAssets: MediaAsset[] | null = null;

View File

@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TProject, TProjectSettings } from "@/lib/project/types";
import type { TProject, TProjectSettings } from "@/project/types";
export class UpdateProjectSettingsCommand extends Command {
private savedSettings: TProjectSettings | null = null;

View File

@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { buildDefaultScene } from "@/lib/scenes";
import type { TScene } from "@/timeline";
import { buildDefaultScene } from "@/timeline/scenes";
export class CreateSceneCommand extends Command {
private savedScenes: TScene[] | null = null;

View File

@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes";
import type { TScene } from "@/timeline";
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/timeline/scenes";
export class DeleteSceneCommand extends Command {
private savedScenes: TScene[] | null = null;

View File

@ -1,8 +1,8 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import { getFrameTime, moveBookmarkInArray } from "@/lib/timeline/bookmarks";
import type { TScene } from "@/timeline";
import { updateSceneInArray } from "@/timeline/scenes";
import { getFrameTime, moveBookmarkInArray } from "@/timeline/bookmarks/index";
export class MoveBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;

View File

@ -1,11 +1,11 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import type { TScene } from "@/timeline";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,
removeBookmarkFromArray,
} from "@/lib/timeline/bookmarks";
} from "@/timeline/bookmarks/index";
export class RemoveBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;

View File

@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import type { TScene } from "@/timeline";
import { updateSceneInArray } from "@/timeline/scenes";
export class RenameSceneCommand extends Command {
private savedScenes: TScene[] | null = null;

View File

@ -1,8 +1,11 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import { getFrameTime, toggleBookmarkInArray } from "@/lib/timeline/bookmarks";
import type { TScene } from "@/timeline";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,
toggleBookmarkInArray,
} from "@/timeline/bookmarks/index";
export class ToggleBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;

View File

@ -1,8 +1,11 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { Bookmark, TScene } from "@/lib/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import { getFrameTime, updateBookmarkInArray } from "@/lib/timeline/bookmarks";
import type { Bookmark, TScene } from "@/timeline";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,
updateBookmarkInArray,
} from "@/timeline/bookmarks/index";
export class UpdateBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;

View File

@ -4,11 +4,11 @@ import {
resolveAnimationTarget,
updateScalarKeyframeCurve,
upsertPathKeyframe,
} from "@/lib/animation";
import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { KeyframeClipboardItem } from "@/lib/clipboard";
import type { SceneTracks, TimelineElement } from "@/lib/timeline";
import { updateElementInSceneTracks } from "@/lib/timeline";
} from "@/animation";
import { Command, type CommandResult } from "@/commands/base-command";
import type { KeyframeClipboardItem } from "@/clipboard";
import type { SceneTracks, TimelineElement } from "@/timeline";
import { updateElementInSceneTracks } from "@/timeline";
import { generateUUID } from "@/utils/id";
function pasteKeyframesIntoElement({

View File

@ -0,0 +1,193 @@
import {
Command,
createElementSelectionResult,
type CommandResult,
} from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { ElementClipboardItem } from "@/clipboard";
import { generateUUID } from "@/utils/id";
import {
applyPlacement,
resolveTrackPlacement,
enforceMainTrackStart,
} from "@/timeline/placement";
import { cloneAnimations } from "@/animation";
export class PasteCommand extends Command {
private savedState: SceneTracks | null = null;
private pastedElements: { trackId: string; elementId: string }[] = [];
private readonly time: number;
private readonly clipboardItems: ElementClipboardItem[];
constructor({
time,
clipboardItems,
}: {
time: number;
clipboardItems: ElementClipboardItem[];
}) {
super();
this.time = time;
this.clipboardItems = clipboardItems;
}
execute(): CommandResult | undefined {
if (this.clipboardItems.length === 0) return undefined;
const editor = EditorCore.getInstance();
this.savedState = editor.scenes.getActiveScene().tracks;
this.pastedElements = [];
const minStart = Math.min(
...this.clipboardItems.map((item) => item.element.startTime),
);
let updatedTracks = this.savedState;
const itemsByTrackId = groupClipboardItemsByTrackId({
clipboardItems: this.clipboardItems,
});
for (const [trackId, items] of itemsByTrackId) {
const elementsToAdd = buildPastedElements({
items,
minStart,
time: this.time,
});
if (elementsToAdd.length === 0) {
continue;
}
const trackType = items[0].trackType;
const sourceTrackIndex = [
...updatedTracks.overlay,
updatedTracks.main,
...updatedTracks.audio,
].findIndex((track) => track.id === trackId);
const placementResult = resolveTrackPlacement({
tracks: updatedTracks,
trackType,
timeSpans: elementsToAdd.map((element) => ({
startTime: element.startTime,
duration: element.duration,
})),
strategy: { type: "aboveSource", sourceTrackIndex },
});
if (!placementResult) {
continue;
}
let elementsForPlacement = elementsToAdd;
if (placementResult.kind === "existingTrack") {
const targetTrack =
placementResult.trackIndex < updatedTracks.overlay.length
? updatedTracks.overlay[placementResult.trackIndex]
: placementResult.trackIndex === updatedTracks.overlay.length
? updatedTracks.main
: updatedTracks.audio[
placementResult.trackIndex - updatedTracks.overlay.length - 1
];
if (targetTrack?.id === updatedTracks.main.id) {
const earliestElement = elementsToAdd.reduce((earliest, element) =>
element.startTime < earliest.startTime ? element : earliest,
);
const adjustedEarliestStartTime = enforceMainTrackStart({
tracks: updatedTracks,
targetTrackId: targetTrack.id,
requestedStartTime: earliestElement.startTime,
});
const delta = adjustedEarliestStartTime - earliestElement.startTime;
if (delta !== 0) {
elementsForPlacement = elementsToAdd.map((element) => ({
...element,
startTime: Math.max(0, element.startTime + delta),
}));
}
}
}
const applied = applyPlacement({
tracks: updatedTracks,
placementResult,
elements: elementsForPlacement,
});
if (!applied) {
continue;
}
updatedTracks = applied.updatedTracks;
for (const element of elementsForPlacement) {
this.pastedElements.push({
trackId: applied.targetTrackId,
elementId: element.id,
});
}
}
editor.timeline.updateTracks(updatedTracks);
if (this.pastedElements.length > 0) {
return createElementSelectionResult(this.pastedElements);
}
return undefined;
}
undo(): void {
if (this.savedState) {
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState);
}
}
getPastedElements(): { trackId: string; elementId: string }[] {
return this.pastedElements;
}
}
function groupClipboardItemsByTrackId({
clipboardItems,
}: {
clipboardItems: ElementClipboardItem[];
}): Map<string, ElementClipboardItem[]> {
const groupedItems = new Map<string, ElementClipboardItem[]>();
for (const item of clipboardItems) {
const existingItems = groupedItems.get(item.trackId) ?? [];
groupedItems.set(item.trackId, [...existingItems, item]);
}
return groupedItems;
}
function buildPastedElements({
items,
minStart,
time,
}: {
items: ElementClipboardItem[];
minStart: number;
time: number;
}): TimelineElement[] {
const elementsToAdd: TimelineElement[] = [];
for (const item of items) {
const relativeOffset = item.element.startTime - minStart;
const startTime = Math.max(0, time + relativeOffset);
const newElementId = generateUUID();
elementsToAdd.push({
...item.element,
id: newElementId,
startTime,
animations: cloneAnimations({
animations: item.element.animations,
shouldRegenerateKeyframeIds: true,
}),
} as TimelineElement);
}
return elementsToAdd;
}

View File

@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { SceneTracks } from "@/lib/timeline";
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks } from "@/timeline";
import { EditorCore } from "@/core";
import type { TimelineTrack } from "@/lib/timeline";
import type { TimelineTrack } from "@/timeline";
function removeTrackElements<TTrack extends TimelineTrack>({
track,

View File

@ -0,0 +1,134 @@
import {
Command,
createElementSelectionResult,
type CommandResult,
} from "@/commands/base-command";
import type { SceneTracks, TimelineElement } from "@/timeline";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import { applyPlacement, resolveTrackPlacement } from "@/timeline/placement";
import { cloneAnimations } from "@/animation";
interface DuplicateElementsParams {
elements: { trackId: string; elementId: string }[];
}
export class DuplicateElementsCommand extends Command {
private duplicatedElements: { trackId: string; elementId: string }[] = [];
private savedState: SceneTracks | null = null;
private elements: DuplicateElementsParams["elements"];
constructor({ elements }: DuplicateElementsParams) {
super();
this.elements = elements;
}
execute(): CommandResult | undefined {
const editor = EditorCore.getInstance();
this.savedState = editor.scenes.getActiveScene().tracks;
this.duplicatedElements = [];
let updatedTracks = this.savedState;
for (const track of [
...this.savedState.overlay,
this.savedState.main,
...this.savedState.audio,
]) {
const elementsToDuplicate = this.elements.filter(
(elementEntry) => elementEntry.trackId === track.id,
);
if (elementsToDuplicate.length === 0) {
continue;
}
const elementIdsToDuplicate = new Set(
elementsToDuplicate.map((element) => element.elementId),
);
const newTrackElements: TimelineElement[] = [];
for (const element of track.elements) {
if (!elementIdsToDuplicate.has(element.id)) {
continue;
}
const newId = generateUUID();
newTrackElements.push(
buildDuplicateElement({
element,
id: newId,
startTime: element.startTime,
}),
);
}
const placementResult = resolveTrackPlacement({
tracks: updatedTracks,
trackType: track.type,
timeSpans: [],
strategy: { type: "alwaysNew", position: "highest" },
});
if (!placementResult || placementResult.kind !== "newTrack") {
continue;
}
const applied = applyPlacement({
tracks: updatedTracks,
placementResult,
elements: newTrackElements,
});
if (!applied) {
continue;
}
updatedTracks = applied.updatedTracks;
for (const element of newTrackElements) {
this.duplicatedElements.push({
trackId: applied.targetTrackId,
elementId: element.id,
});
}
}
editor.timeline.updateTracks(updatedTracks);
if (this.duplicatedElements.length > 0) {
return createElementSelectionResult(this.duplicatedElements);
}
return undefined;
}
undo(): void {
if (this.savedState) {
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState);
}
}
getDuplicatedElements(): { trackId: string; elementId: string }[] {
return this.duplicatedElements;
}
}
function buildDuplicateElement({
element,
id,
startTime,
}: {
element: TimelineElement;
id: string;
startTime: number;
}): TimelineElement {
return {
...element,
id,
name: `${element.name} (copy)`,
startTime,
animations: cloneAnimations({
animations: element.animations,
shouldRegenerateKeyframeIds: true,
}),
};
}

View File

@ -1,8 +1,8 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline";
import type { SceneTracks, VisualElement } from "@/lib/timeline";
import { buildDefaultEffectInstance } from "@/lib/effects";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/timeline";
import { buildDefaultEffectInstance } from "@/effects";
function addEffectToElement({
element,

Some files were not shown because too many files have changed in this diff Show More