refactor: restructure files to their domains, new preview overlay system, and dep graph
This commit is contained in:
parent
729d10592f
commit
3e89d29985
|
|
@ -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,
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type {
|
||||
KeybindingConfig,
|
||||
ShortcutKey,
|
||||
} from "@/lib/actions/keybinding";
|
||||
} from "@/actions/keybinding";
|
||||
import type { TActionWithOptionalArgs } from "./types";
|
||||
|
||||
export type TActionCategory =
|
||||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
import type { KeybindingConfig } from "@/actions/keybinding";
|
||||
|
||||
interface V4State {
|
||||
keybindings: KeybindingConfig;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
import type { KeybindingConfig } from "@/actions/keybinding";
|
||||
|
||||
interface V5State {
|
||||
keybindings: KeybindingConfig;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
||||
import type { KeybindingConfig } from "@/actions/keybinding";
|
||||
|
||||
interface V6State {
|
||||
keybindings: KeybindingConfig;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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();
|
||||
|
|
@ -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";
|
||||
|
||||
/**
|
||||
|
|
@ -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,
|
||||
|
|
@ -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", () => {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ScalarAnimationKey } from "@/lib/animation/types";
|
||||
import type { ScalarAnimationKey } from "@/animation/types";
|
||||
|
||||
const BEZIER_SOLVE_ITERATIONS = 20;
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ import type {
|
|||
DiscreteValue,
|
||||
Vector2AnimationBinding,
|
||||
VectorValue,
|
||||
} from "@/lib/animation/types";
|
||||
} from "@/animation/types";
|
||||
import { clamp } from "@/utils/math";
|
||||
|
||||
interface LinearRgba {
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ import type {
|
|||
ScalarAnimationChannel,
|
||||
ScalarGraphChannel,
|
||||
ScalarGraphKeyframeContext,
|
||||
} from "@/lib/animation/types";
|
||||
} from "@/animation/types";
|
||||
|
||||
export interface EditableScalarChannels {
|
||||
binding: AnimationBindingInstance;
|
||||
|
|
@ -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.";
|
||||
|
|
@ -7,7 +7,7 @@ import type {
|
|||
ScalarAnimationChannel,
|
||||
ScalarAnimationKey,
|
||||
ScalarSegmentType,
|
||||
} from "@/lib/animation/types";
|
||||
} from "@/animation/types";
|
||||
import { clamp } from "@/utils/math";
|
||||
import {
|
||||
getBezierPoint,
|
||||
|
|
@ -4,7 +4,7 @@ import type {
|
|||
AnimationPath,
|
||||
ElementAnimations,
|
||||
ElementKeyframe,
|
||||
} from "@/lib/animation/types";
|
||||
} from "@/animation/types";
|
||||
import {
|
||||
type AnimationComponentValue,
|
||||
composeAnimationValue,
|
||||
|
|
@ -13,7 +13,7 @@ import type {
|
|||
ScalarAnimationKey,
|
||||
ScalarCurveKeyframePatch,
|
||||
ScalarSegmentType,
|
||||
} from "@/lib/animation/types";
|
||||
} from "@/animation/types";
|
||||
import { generateUUID } from "@/utils/id";
|
||||
import {
|
||||
cloneAnimationBinding,
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ParamValues } from "@/lib/params";
|
||||
import type { ParamValues } from "@/params";
|
||||
|
||||
export const ANIMATION_PROPERTY_PATHS = [
|
||||
"transform.positionX",
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 }>;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }> };
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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"] });
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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";
|
||||
|
|
@ -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 },
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { PasteCommand } from "@/lib/commands/timeline";
|
||||
import { PasteCommand } from "@/commands/timeline";
|
||||
import type { ClipboardHandler } from "../types";
|
||||
|
||||
export const ElementsClipboardHandler = {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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({
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue