summaryrefslogtreecommitdiff
path: root/apps/web/src/core/managers
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-26 17:31:28 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-26 17:31:28 +0200
commit56ca09692a308b22455420d11376ff8d41244c47 (patch)
tree4eca5ae032451c5db954801008aa9f164b2e2b1d /apps/web/src/core/managers
parente6c6193638e4247ff1e6f2df40de2a057112c773 (diff)
downloadopencut-56ca09692a308b22455420d11376ff8d41244c47.tar.gz
refactor: thinner hooks
Diffstat (limited to 'apps/web/src/core/managers')
-rw-r--r--apps/web/src/core/managers/audio-manager.ts23
-rw-r--r--apps/web/src/core/managers/playback-manager.ts33
-rw-r--r--apps/web/src/core/managers/timeline-manager.ts11
3 files changed, 42 insertions, 25 deletions
diff --git a/apps/web/src/core/managers/audio-manager.ts b/apps/web/src/core/managers/audio-manager.ts
index 545565be..d0716aad 100644
--- a/apps/web/src/core/managers/audio-manager.ts
+++ b/apps/web/src/core/managers/audio-manager.ts
@@ -1,9 +1,6 @@
import type { EditorCore } from "@/core";
import { TICKS_PER_SECOND } from "@/wasm";
-import {
- clampRetimeRate,
- shouldMaintainPitch,
-} from "@/retime/rate";
+import { clampRetimeRate, shouldMaintainPitch } from "@/retime/rate";
import type { AudioClipSource } from "@/media/audio";
import { createAudioContext, collectAudioClips } from "@/media/audio";
import {
@@ -56,10 +53,8 @@ export class AudioManager {
this.editor.playback.subscribe(this.handlePlaybackChange),
this.editor.timeline.subscribe(this.handleTimelineChange),
this.editor.media.subscribe(this.handleTimelineChange),
+ this.editor.playback.onSeek(this.handleSeek),
);
- if (typeof window !== "undefined") {
- window.addEventListener("playback-seek", this.handleSeek);
- }
}
dispose(): void {
@@ -68,9 +63,6 @@ export class AudioManager {
unsub();
}
this.unsubscribers = [];
- if (typeof window !== "undefined") {
- window.removeEventListener("playback-seek", this.handleSeek);
- }
this.disposeSinks();
this.preparedClipBuffers.clear();
this.decodedBuffers.clear();
@@ -102,17 +94,14 @@ export class AudioManager {
}
};
- private handleSeek = (event: Event): void => {
- const detail = (event as CustomEvent<{ time: number }>).detail;
- if (!detail) return;
-
+ private handleSeek = (time: number): void => {
if (this.editor.playback.getIsScrubbing()) {
this.stopPlayback();
return;
}
if (this.editor.playback.getIsPlaying()) {
- void this.startPlayback({ time: detail.time / TICKS_PER_SECOND });
+ void this.startPlayback({ time: time / TICKS_PER_SECOND });
return;
}
@@ -126,7 +115,9 @@ export class AudioManager {
if (!this.editor.playback.getIsPlaying()) return;
- void this.startPlayback({ time: this.editor.playback.getCurrentTime() / TICKS_PER_SECOND });
+ void this.startPlayback({
+ time: this.editor.playback.getCurrentTime() / TICKS_PER_SECOND,
+ });
};
private ensureAudioContext(): AudioContext | null {
diff --git a/apps/web/src/core/managers/playback-manager.ts b/apps/web/src/core/managers/playback-manager.ts
index 6748fd42..8275344f 100644
--- a/apps/web/src/core/managers/playback-manager.ts
+++ b/apps/web/src/core/managers/playback-manager.ts
@@ -4,9 +4,9 @@ import {
clampMediaTime,
type MediaTime,
mediaTimeFromSeconds,
+ roundFrameTime,
ZERO_MEDIA_TIME,
} from "@/wasm";
-import { roundToFrame } from "opencut-wasm";
export class PlaybackManager {
private isPlaying = false;
@@ -16,6 +16,8 @@ export class PlaybackManager {
private previousVolume = 1;
private isScrubbing = false;
private listeners = new Set<() => void>();
+ private updateListeners = new Set<(time: MediaTime) => void>();
+ private seekListeners = new Set<(time: MediaTime) => void>();
private playbackTimer: number | null = null;
private playbackStartWallTime = 0;
private playbackStartTime: MediaTime = ZERO_MEDIA_TIME;
@@ -139,6 +141,16 @@ export class PlaybackManager {
return () => this.listeners.delete(listener);
}
+ onUpdate(listener: (time: MediaTime) => void): () => void {
+ this.updateListeners.add(listener);
+ return () => this.updateListeners.delete(listener);
+ }
+
+ onSeek(listener: (time: MediaTime) => void): () => void {
+ this.seekListeners.add(listener);
+ return () => this.seekListeners.delete(listener);
+ }
+
private reconcileTimelineScope(): void {
const maxTime = this.editor.timeline.getTotalDuration();
const nextTime = this.clampTimeToTimeline(this.currentTime);
@@ -158,6 +170,7 @@ export class PlaybackManager {
this.notify();
if (timeChanged) {
+ this.notifySeek(this.currentTime);
this.dispatchSeekEvent(this.currentTime);
}
}
@@ -168,6 +181,18 @@ export class PlaybackManager {
});
}
+ private notifyUpdate(time: MediaTime): void {
+ this.updateListeners.forEach((fn) => {
+ fn(time);
+ });
+ }
+
+ private notifySeek(time: MediaTime): void {
+ this.seekListeners.forEach((fn) => {
+ fn(time);
+ });
+ }
+
private startTimer(): void {
if (this.playbackTimer) {
cancelAnimationFrame(this.playbackTimer);
@@ -195,20 +220,20 @@ export class PlaybackManager {
a: this.playbackStartTime,
b: mediaTimeFromSeconds({ seconds: elapsedSeconds }),
});
- const newTime = (fps
- ? (roundToFrame({ time: rawTime, rate: fps }) ?? rawTime)
- : rawTime) as MediaTime;
+ const newTime = fps ? roundFrameTime({ time: rawTime, fps }) : rawTime;
const maxTime = this.editor.timeline.getTotalDuration();
if (newTime >= maxTime) {
this.pause();
this.currentTime = maxTime;
this.notify();
+ this.notifySeek(maxTime);
this.dispatchSeekEvent(maxTime);
return;
}
this.currentTime = newTime;
+ this.notifyUpdate(newTime);
this.dispatchUpdateEvent(newTime);
this.playbackTimer = requestAnimationFrame(this.updateTime);
};
diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts
index a1ead6ca..3c6b7599 100644
--- a/apps/web/src/core/managers/timeline-manager.ts
+++ b/apps/web/src/core/managers/timeline-manager.ts
@@ -9,8 +9,9 @@ import type {
RetimeConfig,
} from "@/timeline";
import { calculateTotalDuration } from "@/timeline";
+import { TimelineDragSource } from "@/timeline/drag-source";
import { findTrackInSceneTracks } from "@/timeline/track-element-update";
-import { type MediaTime, ZERO_MEDIA_TIME } from "@/wasm";
+import { lastFrameMediaTime, type MediaTime, ZERO_MEDIA_TIME } from "@/wasm";
import {
canElementBeHidden,
canElementHaveAudio,
@@ -27,7 +28,6 @@ import {
resolveAnimationTarget,
resolveAnimationPathValueAtTime,
} from "@/animation";
-import { lastFrameTime } from "opencut-wasm";
import { BatchCommand } from "@/commands";
import {
AddTrackCommand,
@@ -68,6 +68,7 @@ export class TimelineManager {
private listeners = new Set<() => void>();
private previewOverlay = new Map<string, Partial<TimelineElement>>();
private previewTracks: SceneTracks | null = null;
+ public readonly dragSource = new TimelineDragSource();
constructor(private editor: EditorCore) {}
@@ -214,7 +215,7 @@ export class TimelineManager {
const duration = this.getTotalDuration();
const fps = this.editor.project.getActive()?.settings.fps;
if (!fps || duration <= 0) return duration;
- return (lastFrameTime({ duration, rate: fps }) ?? duration) as MediaTime;
+ return lastFrameMediaTime({ duration, fps });
}
getTrackById({ trackId }: { trackId: string }): TimelineTrack | null {
@@ -707,11 +708,11 @@ export class TimelineManager {
previewElements({
updates,
}: {
- updates: Array<{
+ updates: readonly {
trackId: string;
elementId: string;
updates: Partial<TimelineElement>;
- }>;
+ }[];
}): void {
let changedOverlayCount = 0;
for (const { elementId, updates: elementUpdates } of updates) {