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 {
|
import {
|
||||||
type KeyboardShortcut,
|
type KeyboardShortcut,
|
||||||
useKeyboardShortcutsHelp,
|
useKeyboardShortcutsHelp,
|
||||||
} from "@/hooks/use-keyboard-shortcuts-help";
|
} from "@/actions/use-keyboard-shortcuts-help";
|
||||||
import { useKeybindingsStore } from "@/stores/keybindings-store";
|
import { useKeybindingsStore } from "@/actions/keybindings-store";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type {
|
import type {
|
||||||
KeybindingConfig,
|
KeybindingConfig,
|
||||||
ShortcutKey,
|
ShortcutKey,
|
||||||
} from "@/lib/actions/keybinding";
|
} from "@/actions/keybinding";
|
||||||
import type { TActionWithOptionalArgs } from "./types";
|
import type { TActionWithOptionalArgs } from "./types";
|
||||||
|
|
||||||
export type TActionCategory =
|
export type TActionCategory =
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
import type { TActionWithOptionalArgs } from "@/actions";
|
||||||
import { getDefaultShortcuts } from "@/lib/actions";
|
import { getDefaultShortcuts } from "@/actions";
|
||||||
import { isTypableDOMElement } from "@/utils/browser";
|
import { isTypableDOMElement } from "@/utils/browser";
|
||||||
import { isAppleDevice } from "@/utils/platform";
|
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";
|
import { runMigrations, CURRENT_VERSION } from "./keybindings/migrations";
|
||||||
|
|
||||||
const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
|
const defaultKeybindings: KeybindingConfig = getDefaultShortcuts();
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { KeybindingConfig, ShortcutKey } from "@/lib/actions/keybinding";
|
import type { KeybindingConfig, ShortcutKey } from "@/actions/keybinding";
|
||||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
import type { TActionWithOptionalArgs } from "@/actions";
|
||||||
|
|
||||||
interface V2State {
|
interface V2State {
|
||||||
keybindings: KeybindingConfig;
|
keybindings: KeybindingConfig;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { TActionWithOptionalArgs } from "@/lib/actions";
|
import type { TActionWithOptionalArgs } from "@/actions";
|
||||||
import type { ShortcutKey } from "@/lib/actions/keybinding";
|
import type { ShortcutKey } from "@/actions/keybinding";
|
||||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
import type { KeybindingConfig } from "@/actions/keybinding";
|
||||||
|
|
||||||
interface V3State {
|
interface V3State {
|
||||||
keybindings: KeybindingConfig;
|
keybindings: KeybindingConfig;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
import type { KeybindingConfig } from "@/actions/keybinding";
|
||||||
|
|
||||||
interface V4State {
|
interface V4State {
|
||||||
keybindings: KeybindingConfig;
|
keybindings: KeybindingConfig;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
import type { KeybindingConfig } from "@/actions/keybinding";
|
||||||
|
|
||||||
interface V5State {
|
interface V5State {
|
||||||
keybindings: KeybindingConfig;
|
keybindings: KeybindingConfig;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { KeybindingConfig } from "@/lib/actions/keybinding";
|
import type { KeybindingConfig } from "@/actions/keybinding";
|
||||||
|
|
||||||
interface V6State {
|
interface V6State {
|
||||||
keybindings: KeybindingConfig;
|
keybindings: KeybindingConfig;
|
||||||
|
|
@ -5,8 +5,8 @@ import type {
|
||||||
TActionHandlerOptions,
|
TActionHandlerOptions,
|
||||||
TArgOfAction,
|
TArgOfAction,
|
||||||
TInvocationTrigger,
|
TInvocationTrigger,
|
||||||
} from "@/lib/actions";
|
} from "@/actions";
|
||||||
import { bindAction, unbindAction } from "@/lib/actions";
|
import { bindAction, unbindAction } from "@/actions";
|
||||||
|
|
||||||
export function useActionHandler<A extends TAction>(
|
export function useActionHandler<A extends TAction>(
|
||||||
action: A,
|
action: A,
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/timeline/timeline-store";
|
||||||
import { useActionHandler } from "@/hooks/actions/use-action-handler";
|
import { useActionHandler } from "@/actions/use-action-handler";
|
||||||
import { useEditor } from "../use-editor";
|
import { useEditor } from "@/editor/use-editor";
|
||||||
import { useElementSelection } from "../timeline/element/use-element-selection";
|
import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
|
||||||
import { TICKS_PER_SECOND } from "@/lib/wasm";
|
import { TICKS_PER_SECOND } from "@/wasm";
|
||||||
import { useKeyframeSelection } from "../timeline/element/use-keyframe-selection";
|
import { useKeyframeSelection } from "@/timeline/hooks/element/use-keyframe-selection";
|
||||||
import { getElementsAtTime, hasMediaId } from "@/lib/timeline";
|
import { getElementsAtTime, hasMediaId } from "@/timeline";
|
||||||
import { cancelInteraction } from "@/lib/cancel-interaction";
|
import { cancelInteraction } from "@/editor/cancel-interaction";
|
||||||
import { invokeAction } from "@/lib/actions";
|
import { invokeAction } from "@/actions";
|
||||||
import { canToggleSourceAudio } from "@/lib/timeline/audio-separation";
|
import { canToggleSourceAudio } from "@/timeline/audio-separation";
|
||||||
import {
|
import {
|
||||||
activateScope,
|
activateScope,
|
||||||
clearActiveScope,
|
clearActiveScope,
|
||||||
type ScopeEntry,
|
type ScopeEntry,
|
||||||
} from "@/lib/selection/scope";
|
} from "@/selection/scope";
|
||||||
|
|
||||||
export function useEditorActions() {
|
export function useEditorActions() {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { invokeAction } from "@/lib/actions";
|
import { invokeAction } from "@/actions";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/editor/use-editor";
|
||||||
import { useKeybindingsStore } from "@/stores/keybindings-store";
|
import { useKeybindingsStore } from "@/actions/keybindings-store";
|
||||||
import { isTypableDOMElement } from "@/utils/browser";
|
import { isTypableDOMElement } from "@/utils/browser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useKeybindingsStore } from "@/stores/keybindings-store";
|
import { useKeybindingsStore } from "@/actions/keybindings-store";
|
||||||
import { ACTIONS, type TActionWithOptionalArgs } from "@/lib/actions";
|
import { ACTIONS, type TActionWithOptionalArgs } from "@/actions";
|
||||||
import {
|
import {
|
||||||
getPlatformAlternateKey,
|
getPlatformAlternateKey,
|
||||||
getPlatformSpecialKey,
|
getPlatformSpecialKey,
|
||||||
|
|
@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
||||||
import {
|
import {
|
||||||
composeAnimationValue,
|
composeAnimationValue,
|
||||||
createAnimationBinding,
|
createAnimationBinding,
|
||||||
} from "@/lib/animation/binding-values";
|
} from "@/animation/binding-values";
|
||||||
|
|
||||||
describe("binding values", () => {
|
describe("binding values", () => {
|
||||||
test("formats composed animated colors as hex", () => {
|
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;
|
const BEZIER_SOLVE_ITERATIONS = 20;
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ import type {
|
||||||
DiscreteValue,
|
DiscreteValue,
|
||||||
Vector2AnimationBinding,
|
Vector2AnimationBinding,
|
||||||
VectorValue,
|
VectorValue,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import { clamp } from "@/utils/math";
|
import { clamp } from "@/utils/math";
|
||||||
|
|
||||||
interface LinearRgba {
|
interface LinearRgba {
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import {
|
import {
|
||||||
getDefaultLeftHandle,
|
getDefaultLeftHandle,
|
||||||
getDefaultRightHandle,
|
getDefaultRightHandle,
|
||||||
} from "@/lib/animation/bezier";
|
} from "@/animation/bezier";
|
||||||
import type {
|
import type {
|
||||||
CurveHandle,
|
CurveHandle,
|
||||||
NormalizedCubicBezier,
|
NormalizedCubicBezier,
|
||||||
ScalarAnimationKey,
|
ScalarAnimationKey,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
|
|
||||||
const VALUE_EPSILON = 1e-6;
|
const VALUE_EPSILON = 1e-6;
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { ParamValues } from "@/lib/params";
|
import type { ParamValues } from "@/params";
|
||||||
import type { Effect } from "@/lib/effects/types";
|
import type { Effect } from "@/effects/types";
|
||||||
import type {
|
import type {
|
||||||
ElementAnimations,
|
ElementAnimations,
|
||||||
EffectParamPath,
|
EffectParamPath,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import { removeElementKeyframe } from "./keyframes";
|
import { removeElementKeyframe } from "./keyframes";
|
||||||
import { resolveAnimationPathValueAtTime } from "./resolve";
|
import { resolveAnimationPathValueAtTime } from "./resolve";
|
||||||
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type {
|
||||||
ScalarAnimationChannel,
|
ScalarAnimationChannel,
|
||||||
ScalarGraphChannel,
|
ScalarGraphChannel,
|
||||||
ScalarGraphKeyframeContext,
|
ScalarGraphKeyframeContext,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
|
|
||||||
export interface EditableScalarChannels {
|
export interface EditableScalarChannels {
|
||||||
binding: AnimationBindingInstance;
|
binding: AnimationBindingInstance;
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import type {
|
import type {
|
||||||
ElementAnimations,
|
ElementAnimations,
|
||||||
GraphicParamPath,
|
GraphicParamPath,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import type { ParamValues } from "@/lib/params";
|
import type { ParamValues } from "@/params";
|
||||||
import {
|
import {
|
||||||
getGraphicDefinition,
|
getGraphicDefinition,
|
||||||
resolveGraphicParams,
|
resolveGraphicParams,
|
||||||
} from "@/lib/graphics";
|
} from "@/graphics";
|
||||||
import { resolveAnimationPathValueAtTime } from "./resolve";
|
import { resolveAnimationPathValueAtTime } from "./resolve";
|
||||||
|
|
||||||
export const GRAPHIC_PARAM_PATH_PREFIX = "params.";
|
export const GRAPHIC_PARAM_PATH_PREFIX = "params.";
|
||||||
|
|
@ -7,7 +7,7 @@ import type {
|
||||||
ScalarAnimationChannel,
|
ScalarAnimationChannel,
|
||||||
ScalarAnimationKey,
|
ScalarAnimationKey,
|
||||||
ScalarSegmentType,
|
ScalarSegmentType,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import { clamp } from "@/utils/math";
|
import { clamp } from "@/utils/math";
|
||||||
import {
|
import {
|
||||||
getBezierPoint,
|
getBezierPoint,
|
||||||
|
|
@ -4,7 +4,7 @@ import type {
|
||||||
AnimationPath,
|
AnimationPath,
|
||||||
ElementAnimations,
|
ElementAnimations,
|
||||||
ElementKeyframe,
|
ElementKeyframe,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import {
|
import {
|
||||||
type AnimationComponentValue,
|
type AnimationComponentValue,
|
||||||
composeAnimationValue,
|
composeAnimationValue,
|
||||||
|
|
@ -13,7 +13,7 @@ import type {
|
||||||
ScalarAnimationKey,
|
ScalarAnimationKey,
|
||||||
ScalarCurveKeyframePatch,
|
ScalarCurveKeyframePatch,
|
||||||
ScalarSegmentType,
|
ScalarSegmentType,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import {
|
import {
|
||||||
cloneAnimationBinding,
|
cloneAnimationBinding,
|
||||||
|
|
@ -2,8 +2,8 @@ import type {
|
||||||
AnimationPropertyGroup,
|
AnimationPropertyGroup,
|
||||||
AnimationPropertyPath,
|
AnimationPropertyPath,
|
||||||
ElementAnimations,
|
ElementAnimations,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import { ANIMATION_PROPERTY_GROUPS } from "@/lib/animation/types";
|
import { ANIMATION_PROPERTY_GROUPS } from "@/animation/types";
|
||||||
import { getKeyframeAtTime } from "./keyframe-query";
|
import { getKeyframeAtTime } from "./keyframe-query";
|
||||||
|
|
||||||
export interface GroupKeyframeRef {
|
export interface GroupKeyframeRef {
|
||||||
|
|
@ -3,20 +3,20 @@ import type {
|
||||||
AnimationInterpolation,
|
AnimationInterpolation,
|
||||||
AnimationPropertyPath,
|
AnimationPropertyPath,
|
||||||
AnimationValue,
|
AnimationValue,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import { parseColorToLinearRgba } from "./binding-values";
|
import { parseColorToLinearRgba } from "./binding-values";
|
||||||
import type { TimelineElement } from "@/lib/timeline";
|
import type { TimelineElement } from "@/timeline";
|
||||||
import { MIN_TRANSFORM_SCALE } from "@/lib/animation/transform";
|
import { MIN_TRANSFORM_SCALE } from "@/animation/transform";
|
||||||
import {
|
import {
|
||||||
CORNER_RADIUS_MAX,
|
CORNER_RADIUS_MAX,
|
||||||
CORNER_RADIUS_MIN,
|
CORNER_RADIUS_MIN,
|
||||||
} from "@/lib/text/background";
|
} from "@/text/background";
|
||||||
import {
|
import {
|
||||||
canElementHaveAudio,
|
canElementHaveAudio,
|
||||||
isVisualElement,
|
isVisualElement,
|
||||||
} from "@/lib/timeline/element-utils";
|
} from "@/timeline/element-utils";
|
||||||
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants";
|
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/timeline/audio-constants";
|
||||||
import { DEFAULTS } from "@/lib/timeline/defaults";
|
import { DEFAULTS } from "@/timeline/defaults";
|
||||||
import { snapToStep } from "@/utils/math";
|
import { snapToStep } from "@/utils/math";
|
||||||
|
|
||||||
export interface NumericSpec {
|
export interface NumericSpec {
|
||||||
|
|
@ -5,8 +5,8 @@ import type {
|
||||||
AnimationPropertyPath,
|
AnimationPropertyPath,
|
||||||
AnimationValueForPath,
|
AnimationValueForPath,
|
||||||
ElementAnimations,
|
ElementAnimations,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import type { Transform } from "@/lib/rendering";
|
import type { Transform } from "@/rendering";
|
||||||
import {
|
import {
|
||||||
type AnimationComponentValue,
|
type AnimationComponentValue,
|
||||||
composeAnimationValue,
|
composeAnimationValue,
|
||||||
|
|
@ -3,20 +3,20 @@ import type {
|
||||||
AnimationInterpolation,
|
AnimationInterpolation,
|
||||||
AnimationPath,
|
AnimationPath,
|
||||||
AnimationValue,
|
AnimationValue,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import {
|
import {
|
||||||
parseEffectParamPath,
|
parseEffectParamPath,
|
||||||
isEffectParamPath,
|
isEffectParamPath,
|
||||||
} from "@/lib/animation/effect-param-channel";
|
} from "@/animation/effect-param-channel";
|
||||||
import {
|
import {
|
||||||
isGraphicParamPath,
|
isGraphicParamPath,
|
||||||
parseGraphicParamPath,
|
parseGraphicParamPath,
|
||||||
} from "@/lib/animation/graphic-param-channel";
|
} from "@/animation/graphic-param-channel";
|
||||||
import type { ParamDefinition } from "@/lib/params";
|
import type { ParamDefinition } from "@/params";
|
||||||
import { effectsRegistry, registerDefaultEffects } from "@/lib/effects";
|
import { effectsRegistry, registerDefaultEffects } from "@/effects";
|
||||||
import { getGraphicDefinition } from "@/lib/graphics";
|
import { getGraphicDefinition } from "@/graphics";
|
||||||
import type { TimelineElement } from "@/lib/timeline";
|
import type { TimelineElement } from "@/timeline";
|
||||||
import { isVisualElement } from "@/lib/timeline/element-utils";
|
import { isVisualElement } from "@/timeline/element-utils";
|
||||||
import { snapToStep } from "@/utils/math";
|
import { snapToStep } from "@/utils/math";
|
||||||
import {
|
import {
|
||||||
coerceAnimationValueForProperty,
|
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 = [
|
export const ANIMATION_PROPERTY_PATHS = [
|
||||||
"transform.positionX",
|
"transform.positionX",
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { auth } from "@/lib/auth/server";
|
import { auth } from "@/auth/server";
|
||||||
import { toNextJsHandler } from "better-auth/next-js";
|
import { toNextJsHandler } from "better-auth/next-js";
|
||||||
|
|
||||||
export const { POST, GET } = toNextJsHandler(auth);
|
export const { POST, GET } = toNextJsHandler(auth);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { type NextRequest, NextResponse } from "next/server";
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { checkRateLimit } from "@/lib/rate-limit";
|
import { checkRateLimit } from "@/auth/rate-limit";
|
||||||
import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/lib/feedback";
|
import { submitFeedback, MAX_MESSAGE_LENGTH } from "@/feedback";
|
||||||
|
|
||||||
const submitSchema = z.object({
|
const submitSchema = z.object({
|
||||||
message: z
|
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 { type NextRequest, NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { checkRateLimit } from "@/lib/rate-limit";
|
import { checkRateLimit } from "@/auth/rate-limit";
|
||||||
|
|
||||||
const searchParamsSchema = z.object({
|
const searchParamsSchema = z.object({
|
||||||
q: z.string().max(500, "Query too long").optional(),
|
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 { BasePage } from "@/app/base-page";
|
||||||
import Prose from "@/components/ui/prose";
|
import Prose from "@/components/ui/prose";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query";
|
import { getPosts, getSinglePost, processHtmlContent } from "@/blog/query";
|
||||||
import type { Author, Post } from "@/lib/blog/types";
|
import type { Author, Post } from "@/blog/types";
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
params: Promise<{ slug: string }>;
|
params: Promise<{ slug: string }>;
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BasePage } from "@/app/base-page";
|
import { BasePage } from "@/app/base-page";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { getPosts } from "@/lib/blog/query";
|
import { getPosts } from "@/blog/query";
|
||||||
import type { Post } from "@/lib/blog/types";
|
import type { Post } from "@/blog/types";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Blog - OpenCut",
|
title: "Blog - OpenCut",
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@ import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { BasePage } from "@/app/base-page";
|
import { BasePage } from "@/app/base-page";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||||
import { getReleaseByVersion, getSortedReleases } from "@/lib/changelog/utils";
|
import { getReleaseByVersion, getSortedReleases } from "@/changelog/utils";
|
||||||
import {
|
import {
|
||||||
ReleaseArticle,
|
ReleaseArticle,
|
||||||
ReleaseMeta,
|
ReleaseMeta,
|
||||||
ReleaseTitle,
|
ReleaseTitle,
|
||||||
ReleaseDescription,
|
ReleaseDescription,
|
||||||
ReleaseChanges,
|
ReleaseChanges,
|
||||||
} from "@/lib/changelog/components/release";
|
} from "@/changelog/components/release";
|
||||||
import { CopyMarkdownButton } from "@/lib/changelog/components/copy-markdown-button";
|
import { CopyMarkdownButton } from "@/changelog/components/copy-markdown-button";
|
||||||
|
|
||||||
type Props = { params: Promise<{ version: string }> };
|
type Props = { params: Promise<{ version: string }> };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ import { Separator } from "@/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
type Release as ReleaseType,
|
type Release as ReleaseType,
|
||||||
getSortedReleases,
|
getSortedReleases,
|
||||||
} from "@/lib/changelog/utils";
|
} from "@/changelog/utils";
|
||||||
import {
|
import {
|
||||||
ReleaseArticle,
|
ReleaseArticle,
|
||||||
ReleaseMeta,
|
ReleaseMeta,
|
||||||
ReleaseTitle,
|
ReleaseTitle,
|
||||||
ReleaseDescription,
|
ReleaseDescription,
|
||||||
ReleaseChanges,
|
ReleaseChanges,
|
||||||
} from "@/lib/changelog/components/release";
|
} from "@/changelog/components/release";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Changelog - OpenCut",
|
title: "Changelog - OpenCut",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Link from "next/link";
|
||||||
import { GitHubContributeSection } from "@/components/gitHub-contribute-section";
|
import { GitHubContributeSection } from "@/components/gitHub-contribute-section";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
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";
|
import { BasePage } from "../base-page";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,32 @@ import {
|
||||||
} from "@/components/ui/resizable";
|
} from "@/components/ui/resizable";
|
||||||
import { AssetsPanel } from "@/components/editor/panels/assets";
|
import { AssetsPanel } from "@/components/editor/panels/assets";
|
||||||
import { PropertiesPanel } from "@/components/editor/panels/properties";
|
import { PropertiesPanel } from "@/components/editor/panels/properties";
|
||||||
import { Timeline } from "@/components/editor/panels/timeline";
|
import { Timeline } from "@/timeline/components";
|
||||||
import { PreviewPanel } from "@/components/editor/panels/preview";
|
import { PreviewPanel } from "@/preview/components";
|
||||||
import { EditorHeader } from "@/components/editor/editor-header";
|
import { EditorHeader } from "@/components/editor/editor-header";
|
||||||
import { EditorProvider } from "@/components/providers/editor-provider";
|
import { EditorProvider } from "@/components/providers/editor-provider";
|
||||||
import { Onboarding } from "@/components/editor/onboarding";
|
import { Onboarding } from "@/components/editor/onboarding";
|
||||||
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
|
import { MigrationDialog } from "@/project/components/migration-dialog";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/editor/panel-store";
|
||||||
import { usePasteMedia } from "@/hooks/use-paste-media";
|
import { usePasteMedia } from "@/media/use-paste-media";
|
||||||
import { MobileGate } from "@/components/editor/mobile-gate";
|
import { MobileGate } from "@/components/editor/mobile-gate";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/editor/use-editor";
|
||||||
import { Cancel01Icon } from "@hugeicons/core-free-icons";
|
import { Cancel01Icon } from "@hugeicons/core-free-icons";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
import { Button } from "@/components/ui/button";
|
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() {
|
export default function Editor() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -70,6 +81,49 @@ function DegradedRendererBanner() {
|
||||||
function EditorLayout() {
|
function EditorLayout() {
|
||||||
usePasteMedia();
|
usePasteMedia();
|
||||||
const { panels, setPanel } = usePanelStore();
|
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 (
|
return (
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
|
|
@ -111,7 +165,11 @@ function EditorLayout() {
|
||||||
minSize={30}
|
minSize={30}
|
||||||
className="min-h-0 min-w-0 flex-1"
|
className="min-h-0 min-w-0 flex-1"
|
||||||
>
|
>
|
||||||
<PreviewPanel />
|
<PreviewPanel
|
||||||
|
overlayControls={overlayControls}
|
||||||
|
overlayInstances={overlaySource.instances}
|
||||||
|
onOverlayVisibilityChange={setOverlayVisibility}
|
||||||
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { ThemeProvider } from "next-themes";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Toaster } from "../components/ui/sonner";
|
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 { TooltipProvider } from "../components/ui/tooltip";
|
||||||
import { baseMetaData } from "./metadata";
|
import { baseMetaData } from "./metadata";
|
||||||
import { BotIdClient } from "botid/client";
|
import { BotIdClient } from "botid/client";
|
||||||
import { webEnv } from "@/lib/env/web";
|
import { webEnv } from "@/env/web";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
|
|
||||||
const siteFont = Inter({ subsets: ["latin"] });
|
const siteFont = Inter({ subsets: ["latin"] });
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Metadata } from "next";
|
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 = {
|
export const baseMetaData: Metadata = {
|
||||||
metadataBase: new URL(SITE_URL),
|
metadataBase: new URL(SITE_URL),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Hero } from "@/components/landing/hero";
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { Footer } from "@/components/footer";
|
import { Footer } from "@/components/footer";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { SITE_URL } from "@/lib/site/brand";
|
import { SITE_URL } from "@/site/brand";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
alternates: {
|
alternates: {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { SOCIAL_LINKS } from "@/lib/site/social";
|
import { SOCIAL_LINKS } from "@/site/social";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Privacy Policy - OpenCut",
|
title: "Privacy Policy - OpenCut",
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,20 @@ import type { KeyboardEvent, MouseEvent } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { EditorCore } from "@/core";
|
import type { EditorCore } from "@/core";
|
||||||
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
|
import { MigrationDialog } from "@/project/components/migration-dialog";
|
||||||
import { StoragePersistenceDialog } from "@/components/editor/dialogs/storage-persistence-dialog";
|
import { StoragePersistenceDialog } from "@/services/storage/components/storage-persistence-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/editor/use-editor";
|
||||||
import { useProjectsStore } from "./store";
|
import { useProjectsStore } from "./store";
|
||||||
import type {
|
import type {
|
||||||
TProjectMetadata,
|
TProjectMetadata,
|
||||||
TProjectSortKey,
|
TProjectSortKey,
|
||||||
TProjectSortOption,
|
TProjectSortOption,
|
||||||
} from "@/lib/project/types";
|
} from "@/project/types";
|
||||||
import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm";
|
import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm";
|
||||||
import { formatDate } from "@/utils/date";
|
import { formatDate } from "@/utils/date";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
|
@ -62,11 +62,11 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { DeleteProjectDialog } from "@/components/editor/dialogs/delete-project-dialog";
|
import { DeleteProjectDialog } from "@/project/components/delete-project-dialog";
|
||||||
import { ProjectInfoDialog } from "@/components/editor/dialogs/project-info-dialog";
|
import { ProjectInfoDialog } from "@/project/components/project-info-dialog";
|
||||||
import { RenameProjectDialog } from "@/components/editor/dialogs/rename-project-dialog";
|
import { RenameProjectDialog } from "@/project/components/rename-project-dialog";
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
|
import { ChangelogNotification } from "@/changelog/components/changelog-notification";
|
||||||
const formatProjectDuration = ({
|
const formatProjectDuration = ({
|
||||||
duration,
|
duration,
|
||||||
}: {
|
}: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import type { TProjectSortKey } from "@/lib/project/types";
|
import type { TProjectSortKey } from "@/project/types";
|
||||||
|
|
||||||
export type ProjectsViewMode = "grid" | "list";
|
export type ProjectsViewMode = "grid" | "list";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
import { SITE_URL } from "@/lib/site/brand";
|
import { SITE_URL } from "@/site/brand";
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Feed } from "feed";
|
import { Feed } from "feed";
|
||||||
import { getPosts } from "@/lib/blog/query";
|
import { getPosts } from "@/blog/query";
|
||||||
import { SITE_INFO, SITE_URL } from "@/lib/site/brand";
|
import { SITE_INFO, SITE_URL } from "@/site/brand";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { SITE_URL } from "@/lib/site/brand";
|
import { SITE_URL } from "@/site/brand";
|
||||||
import { getPosts } from "@/lib/blog/query";
|
import { getPosts } from "@/blog/query";
|
||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BasePage } from "@/app/base-page";
|
import { BasePage } from "@/app/base-page";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
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 { HugeiconsIcon } from "@hugeicons/react";
|
||||||
import { LinkSquare02Icon } from "@hugeicons/core-free-icons";
|
import { LinkSquare02Icon } from "@hugeicons/core-free-icons";
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { SOCIAL_LINKS } from "@/lib/site/social";
|
import { SOCIAL_LINKS } from "@/site/social";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Terms of Service - OpenCut",
|
title: "Terms of Service - OpenCut",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createAuthClient } from "better-auth/react";
|
import { createAuthClient } from "better-auth/react";
|
||||||
import { webEnv } from "@/lib/env/web";
|
import { webEnv } from "@/env/web";
|
||||||
|
|
||||||
export const { signIn, signUp, useSession } = createAuthClient({
|
export const { signIn, signUp, useSession } = createAuthClient({
|
||||||
baseURL: webEnv.NEXT_PUBLIC_SITE_URL,
|
baseURL: webEnv.NEXT_PUBLIC_SITE_URL,
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Ratelimit } from "@upstash/ratelimit";
|
import { Ratelimit } from "@upstash/ratelimit";
|
||||||
import { Redis } from "@upstash/redis";
|
import { Redis } from "@upstash/redis";
|
||||||
import { webEnv } from "@/lib/env/web";
|
import { webEnv } from "@/env/web";
|
||||||
|
|
||||||
const redis = new Redis({
|
const redis = new Redis({
|
||||||
url: webEnv.UPSTASH_REDIS_REST_URL,
|
url: webEnv.UPSTASH_REDIS_REST_URL,
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { betterAuth, type RateLimit } from "better-auth";
|
import { betterAuth, type RateLimit } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { Redis } from "@upstash/redis";
|
import { Redis } from "@upstash/redis";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/db";
|
||||||
import { webEnv } from "@/lib/env/web";
|
import { webEnv } from "@/env/web";
|
||||||
|
|
||||||
const redis = new Redis({
|
const redis = new Redis({
|
||||||
url: webEnv.UPSTASH_REDIS_REST_URL,
|
url: webEnv.UPSTASH_REDIS_REST_URL,
|
||||||
|
|
@ -5,7 +5,7 @@ import type {
|
||||||
MarblePost,
|
MarblePost,
|
||||||
MarblePostList,
|
MarblePostList,
|
||||||
MarbleTagList,
|
MarbleTagList,
|
||||||
} from "@/lib/blog/types";
|
} from "@/blog/types";
|
||||||
import { unified } from "unified";
|
import { unified } from "unified";
|
||||||
import rehypeParse from "rehype-parse";
|
import rehypeParse from "rehype-parse";
|
||||||
import rehypeStringify from "rehype-stringify";
|
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[] = [
|
export const DEFAULT_CANVAS_PRESETS: TCanvasSize[] = [
|
||||||
{ width: 1920, height: 1080 },
|
{ width: 1920, height: 1080 },
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PasteCommand } from "@/lib/commands/timeline";
|
import { PasteCommand } from "@/commands/timeline";
|
||||||
import type { ClipboardHandler } from "../types";
|
import type { ClipboardHandler } from "../types";
|
||||||
|
|
||||||
export const ElementsClipboardHandler = {
|
export const ElementsClipboardHandler = {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { getKeyframeById } from "@/lib/animation";
|
import { getKeyframeById } from "@/animation";
|
||||||
import type { SelectedKeyframeRef } from "@/lib/animation/types";
|
import type { SelectedKeyframeRef } from "@/animation/types";
|
||||||
import type { TimelineElement } from "@/lib/timeline";
|
import type { TimelineElement } from "@/timeline";
|
||||||
import { PasteKeyframesCommand } from "@/lib/commands/timeline";
|
import { PasteKeyframesCommand } from "@/commands/timeline";
|
||||||
import type {
|
import type {
|
||||||
ClipboardHandler,
|
ClipboardHandler,
|
||||||
KeyframeClipboardCurvePatch,
|
KeyframeClipboardCurvePatch,
|
||||||
|
|
@ -5,13 +5,13 @@ import type {
|
||||||
AnimationValue,
|
AnimationValue,
|
||||||
ScalarCurveKeyframePatch,
|
ScalarCurveKeyframePatch,
|
||||||
SelectedKeyframeRef,
|
SelectedKeyframeRef,
|
||||||
} from "@/lib/animation/types";
|
} from "@/animation/types";
|
||||||
import type { Command } from "@/lib/commands/base-command";
|
import type { Command } from "@/commands/base-command";
|
||||||
import type {
|
import type {
|
||||||
CreateTimelineElement,
|
CreateTimelineElement,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
TrackType,
|
TrackType,
|
||||||
} from "@/lib/timeline";
|
} from "@/timeline";
|
||||||
|
|
||||||
export interface ElementClipboardItem {
|
export interface ElementClipboardItem {
|
||||||
trackId: string;
|
trackId: string;
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { EditorSelectionPatch } from "@/lib/selection/editor-selection";
|
import type { EditorSelectionPatch } from "@/selection/editor-selection";
|
||||||
import type { ElementRef } from "@/lib/timeline/types";
|
import type { ElementRef } from "@/timeline/types";
|
||||||
|
|
||||||
export interface CommandResult {
|
export interface CommandResult {
|
||||||
selection?: EditorSelectionPatch;
|
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 { EditorCore } from "@/core";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { MediaAsset } from "@/lib/media/types";
|
import type { MediaAsset } from "@/media/types";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import { storageService } from "@/services/storage/service";
|
import { storageService } from "@/services/storage/service";
|
||||||
import type { FrameRate } from "opencut-wasm";
|
import type { FrameRate } from "opencut-wasm";
|
||||||
import { hasMediaId } from "@/lib/timeline/element-utils";
|
import { hasMediaId } from "@/timeline/element-utils";
|
||||||
import { frameRatesEqual, getHighestImportedVideoFps } from "@/lib/fps/utils";
|
import { frameRatesEqual, getHighestImportedVideoFps } from "@/fps/utils";
|
||||||
import { UpdateProjectSettingsCommand } from "@/lib/commands/project";
|
import { UpdateProjectSettingsCommand } from "@/commands/project";
|
||||||
|
|
||||||
export class AddMediaAssetCommand extends Command {
|
export class AddMediaAssetCommand extends Command {
|
||||||
private assetId: string;
|
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 { EditorCore } from "@/core";
|
||||||
import type { MediaAsset } from "@/lib/media/types";
|
import type { MediaAsset } from "@/media/types";
|
||||||
import { buildWaveformSourceKey } from "@/lib/media/waveform-summary";
|
import { buildWaveformSourceKey } from "@/media/waveform-summary";
|
||||||
import { storageService } from "@/services/storage/service";
|
import { storageService } from "@/services/storage/service";
|
||||||
import { videoCache } from "@/services/video-cache/service";
|
import { videoCache } from "@/services/video-cache/service";
|
||||||
import { waveformCache } from "@/services/waveform-cache/service";
|
import { waveformCache } from "@/services/waveform-cache/service";
|
||||||
import { hasMediaId } from "@/lib/timeline/element-utils";
|
import { hasMediaId } from "@/timeline/element-utils";
|
||||||
import type { SceneTracks } from "@/lib/timeline";
|
import type { SceneTracks } from "@/timeline";
|
||||||
|
|
||||||
export class RemoveMediaAssetCommand extends Command {
|
export class RemoveMediaAssetCommand extends Command {
|
||||||
private savedAssets: MediaAsset[] | null = null;
|
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 { EditorCore } from "@/core";
|
||||||
import type { TProject, TProjectSettings } from "@/lib/project/types";
|
import type { TProject, TProjectSettings } from "@/project/types";
|
||||||
|
|
||||||
export class UpdateProjectSettingsCommand extends Command {
|
export class UpdateProjectSettingsCommand extends Command {
|
||||||
private savedSettings: TProjectSettings | null = null;
|
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 { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/timeline";
|
||||||
import { buildDefaultScene } from "@/lib/scenes";
|
import { buildDefaultScene } from "@/timeline/scenes";
|
||||||
|
|
||||||
export class CreateSceneCommand extends Command {
|
export class CreateSceneCommand extends Command {
|
||||||
private savedScenes: TScene[] | null = null;
|
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 { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/timeline";
|
||||||
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/lib/scenes";
|
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/timeline/scenes";
|
||||||
|
|
||||||
export class DeleteSceneCommand extends Command {
|
export class DeleteSceneCommand extends Command {
|
||||||
private savedScenes: TScene[] | null = null;
|
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 { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/timeline/scenes";
|
||||||
import { getFrameTime, moveBookmarkInArray } from "@/lib/timeline/bookmarks";
|
import { getFrameTime, moveBookmarkInArray } from "@/timeline/bookmarks/index";
|
||||||
|
|
||||||
export class MoveBookmarkCommand extends Command {
|
export class MoveBookmarkCommand extends Command {
|
||||||
private savedScenes: TScene[] | null = null;
|
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 { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/timeline/scenes";
|
||||||
import {
|
import {
|
||||||
getFrameTime,
|
getFrameTime,
|
||||||
removeBookmarkFromArray,
|
removeBookmarkFromArray,
|
||||||
} from "@/lib/timeline/bookmarks";
|
} from "@/timeline/bookmarks/index";
|
||||||
|
|
||||||
export class RemoveBookmarkCommand extends Command {
|
export class RemoveBookmarkCommand extends Command {
|
||||||
private savedScenes: TScene[] | null = null;
|
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 { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/timeline/scenes";
|
||||||
|
|
||||||
export class RenameSceneCommand extends Command {
|
export class RenameSceneCommand extends Command {
|
||||||
private savedScenes: TScene[] | null = null;
|
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 { EditorCore } from "@/core";
|
||||||
import type { TScene } from "@/lib/timeline";
|
import type { TScene } from "@/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/timeline/scenes";
|
||||||
import { getFrameTime, toggleBookmarkInArray } from "@/lib/timeline/bookmarks";
|
import {
|
||||||
|
getFrameTime,
|
||||||
|
toggleBookmarkInArray,
|
||||||
|
} from "@/timeline/bookmarks/index";
|
||||||
|
|
||||||
export class ToggleBookmarkCommand extends Command {
|
export class ToggleBookmarkCommand extends Command {
|
||||||
private savedScenes: TScene[] | null = null;
|
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 { EditorCore } from "@/core";
|
||||||
import type { Bookmark, TScene } from "@/lib/timeline";
|
import type { Bookmark, TScene } from "@/timeline";
|
||||||
import { updateSceneInArray } from "@/lib/scenes";
|
import { updateSceneInArray } from "@/timeline/scenes";
|
||||||
import { getFrameTime, updateBookmarkInArray } from "@/lib/timeline/bookmarks";
|
import {
|
||||||
|
getFrameTime,
|
||||||
|
updateBookmarkInArray,
|
||||||
|
} from "@/timeline/bookmarks/index";
|
||||||
|
|
||||||
export class UpdateBookmarkCommand extends Command {
|
export class UpdateBookmarkCommand extends Command {
|
||||||
private savedScenes: TScene[] | null = null;
|
private savedScenes: TScene[] | null = null;
|
||||||
|
|
@ -4,11 +4,11 @@ import {
|
||||||
resolveAnimationTarget,
|
resolveAnimationTarget,
|
||||||
updateScalarKeyframeCurve,
|
updateScalarKeyframeCurve,
|
||||||
upsertPathKeyframe,
|
upsertPathKeyframe,
|
||||||
} from "@/lib/animation";
|
} from "@/animation";
|
||||||
import { Command, type CommandResult } from "@/lib/commands/base-command";
|
import { Command, type CommandResult } from "@/commands/base-command";
|
||||||
import type { KeyframeClipboardItem } from "@/lib/clipboard";
|
import type { KeyframeClipboardItem } from "@/clipboard";
|
||||||
import type { SceneTracks, TimelineElement } from "@/lib/timeline";
|
import type { SceneTracks, TimelineElement } from "@/timeline";
|
||||||
import { updateElementInSceneTracks } from "@/lib/timeline";
|
import { updateElementInSceneTracks } from "@/timeline";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
|
|
||||||
function pasteKeyframesIntoElement({
|
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 { Command, type CommandResult } from "@/commands/base-command";
|
||||||
import type { SceneTracks } from "@/lib/timeline";
|
import type { SceneTracks } from "@/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import type { TimelineTrack } from "@/lib/timeline";
|
import type { TimelineTrack } from "@/timeline";
|
||||||
|
|
||||||
function removeTrackElements<TTrack extends TimelineTrack>({
|
function removeTrackElements<TTrack extends TimelineTrack>({
|
||||||
track,
|
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 { EditorCore } from "@/core";
|
||||||
import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline";
|
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
|
||||||
import type { SceneTracks, VisualElement } from "@/lib/timeline";
|
import type { SceneTracks, VisualElement } from "@/timeline";
|
||||||
import { buildDefaultEffectInstance } from "@/lib/effects";
|
import { buildDefaultEffectInstance } from "@/effects";
|
||||||
|
|
||||||
function addEffectToElement({
|
function addEffectToElement({
|
||||||
element,
|
element,
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue