diff --git a/apps/web/src/core/managers/playback-manager.ts b/apps/web/src/core/managers/playback-manager.ts index 9b9d26bca..5585aeb91 100644 --- a/apps/web/src/core/managers/playback-manager.ts +++ b/apps/web/src/core/managers/playback-manager.ts @@ -224,12 +224,26 @@ export class PlaybackManager { const maxTime = this.editor.timeline.getTotalDuration(); if (newTime >= maxTime) { + const shouldLoop = + this.editor.project.getActive()?.settings.loop === true && + maxTime > ZERO_MEDIA_TIME; + + if (shouldLoop) { + this.playbackStartWallTime = performance.now(); + this.playbackStartTime = ZERO_MEDIA_TIME; + this.currentTime = ZERO_MEDIA_TIME; + this.notifySeek(ZERO_MEDIA_TIME); + this.dispatchSeekEvent(ZERO_MEDIA_TIME); + this.playbackTimer = requestAnimationFrame(this.updateTime); + return; + } + this.pause(); this.currentTime = maxTime; this.notify(); - this.notifySeek(maxTime); - this.dispatchSeekEvent(maxTime); - return; + this.notifySeek(maxTime); + this.dispatchSeekEvent(maxTime); + return; } this.currentTime = newTime; diff --git a/apps/web/src/preview/components/toolbar.tsx b/apps/web/src/preview/components/toolbar.tsx index 1a5ce1bc6..86ace8482 100644 --- a/apps/web/src/preview/components/toolbar.tsx +++ b/apps/web/src/preview/components/toolbar.tsx @@ -10,7 +10,10 @@ import { FullScreenIcon, PauseIcon, PlayIcon, + RepeatIcon, + RepeatOffIcon, } from "@hugeicons/core-free-icons"; +import { UpdateProjectSettingsCommand } from "@/commands/project"; import { HugeiconsIcon } from "@hugeicons/react"; import { Separator } from "@/components/ui/separator"; import { @@ -34,7 +37,10 @@ export function PreviewToolbar({ return (
- +
+ + +
@@ -144,3 +150,23 @@ function PlayPauseButton() { ); } + +function LoopToggleButton() { + const loop = useEditor((e) => e.project.getActive()?.settings.loop === true); + + return ( + + ); +} diff --git a/apps/web/src/project/types.ts b/apps/web/src/project/types.ts index dca2d7854..2c201e7a3 100644 --- a/apps/web/src/project/types.ts +++ b/apps/web/src/project/types.ts @@ -33,6 +33,11 @@ export interface TProjectSettings { lastCustomCanvasSize?: TCanvasSize | null; originalCanvasSize?: TCanvasSize | null; background: TBackground; + /** + * When true, preview playback wraps back to the start of the timeline + * instead of pausing once the playhead reaches the end. Defaults to false. + */ + loop?: boolean; } export interface TTimelineViewState {