summaryrefslogtreecommitdiff
path: root/apps/web/src/core/managers
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/core/managers')
-rw-r--r--apps/web/src/core/managers/commands.ts33
-rw-r--r--apps/web/src/core/managers/timeline-manager.ts203
2 files changed, 146 insertions, 90 deletions
diff --git a/apps/web/src/core/managers/commands.ts b/apps/web/src/core/managers/commands.ts
index 918fc33e..a3665852 100644
--- a/apps/web/src/core/managers/commands.ts
+++ b/apps/web/src/core/managers/commands.ts
@@ -1,6 +1,7 @@
import type { EditorCore } from "@/core";
import type { Command, CommandResult } from "@/lib/commands";
-import type { ElementRef } from "@/lib/timeline/types";
+import { applyRippleAdjustments, computeRippleAdjustments } from "@/lib/ripple";
+import type { TimelineTrack, ElementRef } from "@/lib/timeline/types";
interface CommandHistoryEntry {
command: Command;
@@ -9,6 +10,7 @@ interface CommandHistoryEntry {
}
export class CommandManager {
+ public isRippleEnabled = false;
private history: CommandHistoryEntry[] = [];
private redoStack: CommandHistoryEntry[] = [];
private reactors: Array<() => void> = [];
@@ -16,8 +18,10 @@ export class CommandManager {
constructor(private editor: EditorCore) {}
execute({ command }: { command: Command }): Command {
+ const beforeTracks = this.isRippleEnabled ? this.editor.timeline.getTracks() : null;
const previousSelection = this.getSelectionSnapshot();
const result = command.execute();
+ this.applyRippleIfEnabled({ beforeTracks });
const selectionOverride = this.applySelectionOverride(result);
this.runReactors();
this.history.push({
@@ -67,8 +71,10 @@ export class CommandManager {
return;
}
+ const beforeTracks = this.isRippleEnabled ? this.editor.timeline.getTracks() : null;
const previousSelection = this.getSelectionSnapshot();
const result = entry.command.redo();
+ this.applyRippleIfEnabled({ beforeTracks });
const selectionOverride = this.applySelectionOverride(result);
this.runReactors();
@@ -113,4 +119,29 @@ export class CommandManager {
reactor();
}
}
+
+ private applyRippleIfEnabled({
+ beforeTracks,
+ }: {
+ beforeTracks: TimelineTrack[] | null;
+ }): void {
+ if (!this.isRippleEnabled || !beforeTracks) {
+ return;
+ }
+
+ const afterTracks = this.editor.timeline.getTracks();
+ const adjustments = computeRippleAdjustments({
+ beforeTracks,
+ afterTracks,
+ });
+ if (adjustments.length === 0) {
+ return;
+ }
+
+ const tracksWithRipple = applyRippleAdjustments({
+ tracks: afterTracks,
+ adjustments,
+ });
+ this.editor.timeline.updateTracks(tracksWithRipple);
+ }
}
diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts
index 6f262e57..a6f2462a 100644
--- a/apps/web/src/core/managers/timeline-manager.ts
+++ b/apps/web/src/core/managers/timeline-manager.ts
@@ -7,32 +7,31 @@ import type {
ClipboardItem,
RetimeConfig,
} from "@/lib/timeline";
+import { calculateTotalDuration } from "@/lib/timeline";
+import {
+ canElementBeHidden,
+ canElementHaveAudio,
+} from "@/lib/timeline/element-utils";
import type {
AnimationPath,
AnimationInterpolation,
AnimationValue,
} from "@/lib/animation/types";
-import { calculateTotalDuration } from "@/lib/timeline";
import { getLastFrameTime } from "opencut-wasm";
+import { BatchCommand } from "@/lib/commands";
import {
AddTrackCommand,
RemoveTrackCommand,
ToggleTrackMuteCommand,
ToggleTrackVisibilityCommand,
InsertElementCommand,
- UpdateElementTrimCommand,
- UpdateElementDurationCommand,
DeleteElementsCommand,
DuplicateElementsCommand,
- ToggleElementsVisibilityCommand,
- ToggleElementsMutedCommand,
- UpdateElementCommand,
+ UpdateElementsCommand,
SplitElementsCommand,
PasteCommand,
- UpdateElementStartTimeCommand,
MoveElementCommand,
TracksSnapshotCommand,
- UpdateElementRetimeCommand,
UpsertKeyframeCommand,
RemoveKeyframeCommand,
RetimeKeyframeCommand,
@@ -47,7 +46,6 @@ import {
RemoveEffectParamKeyframeCommand,
ToggleSourceAudioSeparationCommand,
} from "@/lib/commands/timeline";
-import { BatchCommand } from "@/lib/commands";
import type { InsertElementParams } from "@/lib/commands/timeline/element/insert-element";
export class TimelineManager {
@@ -80,7 +78,6 @@ export class TimelineManager {
startTime,
duration,
pushHistory = true,
- rippleEnabled = false,
}: {
elementId: string;
trimStart: number;
@@ -88,44 +85,33 @@ export class TimelineManager {
startTime?: number;
duration?: number;
pushHistory?: boolean;
- rippleEnabled?: boolean;
}): void {
- const command = new UpdateElementTrimCommand({
- elementId,
+ const trackId = this.findTrackIdForElement({ elementId });
+ if (!trackId) {
+ return;
+ }
+
+ const nextUpdates: Partial<TimelineElement> = {
trimStart,
trimEnd,
- startTime,
- duration,
- rippleEnabled,
- });
- if (pushHistory) {
- this.editor.command.execute({ command });
- } else {
- command.execute();
+ };
+ if (startTime !== undefined) {
+ nextUpdates.startTime = startTime;
+ }
+ if (duration !== undefined) {
+ nextUpdates.duration = duration;
}
- }
- updateElementDuration({
- trackId,
- elementId,
- duration,
- pushHistory = true,
- }: {
- trackId: string;
- elementId: string;
- duration: number;
- pushHistory?: boolean;
- }): void {
- const command = new UpdateElementDurationCommand({
- trackId,
- elementId,
- duration,
+ this.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId,
+ patch: nextUpdates,
+ },
+ ],
+ pushHistory,
});
- if (pushHistory) {
- this.editor.command.execute({ command });
- } else {
- command.execute();
- }
}
updateElementRetime({
@@ -139,30 +125,18 @@ export class TimelineManager {
retime?: RetimeConfig;
pushHistory?: boolean;
}): void {
- const command = new UpdateElementRetimeCommand({
- trackId,
- elementId,
- retime,
- });
- if (pushHistory) {
- this.editor.command.execute({ command });
- } else {
- command.execute();
- }
- }
-
- updateElementStartTime({
- elements,
- startTime,
- }: {
- elements: { trackId: string; elementId: string }[];
- startTime: number;
- }): void {
- const command = new UpdateElementStartTimeCommand({
- elements,
- startTime,
+ this.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId,
+ patch: {
+ retime,
+ },
+ },
+ ],
+ pushHistory,
});
- this.editor.command.execute({ command });
}
moveElement({
@@ -171,14 +145,12 @@ export class TimelineManager {
elementId,
newStartTime,
createTrack,
- rippleEnabled = false,
}: {
sourceTrackId: string;
targetTrackId: string;
elementId: string;
newStartTime: number;
createTrack?: { type: TrackType; index: number };
- rippleEnabled?: boolean;
}): void {
const command = new MoveElementCommand({
sourceTrackId,
@@ -186,7 +158,6 @@ export class TimelineManager {
elementId,
newStartTime,
createTrack,
- rippleEnabled,
});
this.editor.command.execute({ command });
}
@@ -205,18 +176,15 @@ export class TimelineManager {
elements,
splitTime,
retainSide = "both",
- rippleEnabled = false,
}: {
elements: { trackId: string; elementId: string }[];
splitTime: number;
retainSide?: "both" | "left" | "right";
- rippleEnabled?: boolean;
}): { trackId: string; elementId: string }[] {
const command = new SplitElementsCommand({
elements,
splitTime,
retainSide,
- rippleEnabled,
});
this.editor.command.execute({ command });
return command.getRightSideElements();
@@ -273,12 +241,10 @@ export class TimelineManager {
deleteElements({
elements,
- rippleEnabled = false,
}: {
elements: { trackId: string; elementId: string }[];
- rippleEnabled?: boolean;
}): void {
- const command = new DeleteElementsCommand({ elements, rippleEnabled });
+ const command = new DeleteElementsCommand({ elements });
this.editor.command.execute({ command });
}
@@ -303,20 +269,17 @@ export class TimelineManager {
updates: Array<{
trackId: string;
elementId: string;
- updates: Partial<TimelineElement>;
+ patch: Partial<TimelineElement>;
}>;
pushHistory?: boolean;
}): void {
- const commands = updates.map(
- ({ trackId, elementId, updates: elementUpdates }) =>
- new UpdateElementCommand({
- trackId,
- elementId,
- updates: elementUpdates,
- }),
- );
- const command =
- commands.length === 1 ? commands[0] : new BatchCommand(commands);
+ if (updates.length === 0) {
+ return;
+ }
+
+ const command = new UpdateElementsCommand({
+ updates,
+ });
if (pushHistory) {
this.editor.command.execute({ command });
} else {
@@ -679,8 +642,27 @@ export class TimelineManager {
}: {
elements: { trackId: string; elementId: string }[];
}): void {
- const command = new ToggleElementsVisibilityCommand(elements);
- this.editor.command.execute({ command });
+ const shouldHide = elements.some(({ trackId, elementId }) => {
+ const element = this.getElementByRef({ trackId, elementId });
+ return element && canElementBeHidden(element) && !element.hidden;
+ });
+
+ const nextUpdates = elements.flatMap(({ trackId, elementId }) => {
+ const element = this.getElementByRef({ trackId, elementId });
+ if (!element || !canElementBeHidden(element)) {
+ return [];
+ }
+
+ return [
+ {
+ trackId,
+ elementId,
+ patch: { hidden: shouldHide },
+ },
+ ];
+ });
+
+ this.updateElements({ updates: nextUpdates });
}
toggleElementsMuted({
@@ -688,8 +670,27 @@ export class TimelineManager {
}: {
elements: { trackId: string; elementId: string }[];
}): void {
- const command = new ToggleElementsMutedCommand(elements);
- this.editor.command.execute({ command });
+ const shouldMute = elements.some(({ trackId, elementId }) => {
+ const element = this.getElementByRef({ trackId, elementId });
+ return element && canElementHaveAudio(element) && !element.muted;
+ });
+
+ const nextUpdates = elements.flatMap(({ trackId, elementId }) => {
+ const element = this.getElementByRef({ trackId, elementId });
+ if (!element || !canElementHaveAudio(element)) {
+ return [];
+ }
+
+ return [
+ {
+ trackId,
+ elementId,
+ patch: { muted: shouldMute },
+ },
+ ];
+ });
+
+ this.updateElements({ updates: nextUpdates });
}
getTracks(): TimelineTrack[] {
@@ -712,6 +713,30 @@ export class TimelineManager {
});
}
+ private getElementByRef({
+ trackId,
+ elementId,
+ }: {
+ trackId: string;
+ elementId: string;
+ }): TimelineElement | undefined {
+ return this.getTrackById({ trackId })?.elements.find(
+ (element) => element.id === elementId,
+ );
+ }
+
+ private findTrackIdForElement({
+ elementId,
+ }: {
+ elementId: string;
+ }): string | null {
+ return (
+ this.getTracks().find((track) =>
+ track.elements.some((element) => element.id === elementId),
+ )?.id ?? null
+ );
+ }
+
updateTracks(newTracks: TimelineTrack[]): void {
this.previewOverlay.clear();
this.previewTracks = null;