summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorVlad <vlad.palacio@gmail.com>2025-07-30 10:25:35 -0400
committerVlad <vlad.palacio@gmail.com>2025-07-30 10:25:35 -0400
commit281ec63d7e0dfe70534790deb790c5bbd6701502 (patch)
tree1180cccbb9aeb6dfd03e254f98513674e895bde9 /apps/web/src
parent1c20027d33bcf303d6a9b40970cd8216c489f0d5 (diff)
downloadopencut-281ec63d7e0dfe70534790deb790c5bbd6701502.tar.gz
Fixed BUG Editor cursor does not stop at the end of an audio file
Get duration of content on the timeline to make sure the playhead moves until the end of the timeline content when the content is less than 10 seconds. Originally it would go all the way to the 10 seconds when timeline content was less than that. I checked how clipchamp does it and they have a min 10 sec timeline but video timeline plays at length of content, so we are doing it like the pros. timeline is jumpy though will probably work on that.
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx22
-rw-r--r--apps/web/src/components/editor/timeline/index.tsx30
-rw-r--r--apps/web/src/components/landing/handlebars.tsx4
-rw-r--r--apps/web/src/hooks/use-editor-actions.ts280
-rw-r--r--apps/web/src/stores/playback-store.ts16
5 files changed, 233 insertions, 119 deletions
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
index 639858d0..6cb06061 100644
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -211,7 +211,7 @@ export function PreviewPanel() {
setDragState({
isDragging: true,
elementId: element.id,
- trackId: trackId,
+ trackId,
startX: e.clientX,
startY: e.clientY,
initialElementX: element.x,
@@ -355,8 +355,24 @@ export function PreviewPanel() {
handleTextMouseDown(e, element, elementData.track.id)
}
style={{
- left: `${50 + ((dragState.isDragging && dragState.elementId === element.id ? dragState.currentX : element.x) / canvasSize.width) * 100}%`,
- top: `${50 + ((dragState.isDragging && dragState.elementId === element.id ? dragState.currentY : element.y) / canvasSize.height) * 100}%`,
+ left: `${
+ 50 +
+ (
+ (dragState.isDragging && dragState.elementId === element.id
+ ? dragState.currentX
+ : element.x) / canvasSize.width
+ ) *
+ 100
+ }%`,
+ top: `${
+ 50 +
+ (
+ (dragState.isDragging && dragState.elementId === element.id
+ ? dragState.currentY
+ : element.y) / canvasSize.height
+ ) *
+ 100
+ }%`,
transform: `translate(-50%, -50%) rotate(${element.rotation}deg) scale(${scaleRatio})`,
opacity: element.opacity,
zIndex: 100 + index, // Text elements on top
diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx
index bdb4433c..7943af06 100644
--- a/apps/web/src/components/editor/timeline/index.tsx
+++ b/apps/web/src/components/editor/timeline/index.tsx
@@ -607,7 +607,11 @@ export function Timeline() {
: "border-l border-muted-foreground/20"
}`}
style={{
- left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
+ left: `${
+ time *
+ TIMELINE_CONSTANTS.PIXELS_PER_SECOND *
+ zoomLevel
+ }px`,
}}
>
<span
@@ -624,10 +628,16 @@ export function Timeline() {
const secs = seconds % 60;
if (hours > 0) {
- return `${hours}:${minutes.toString().padStart(2, "0")}:${Math.floor(secs).toString().padStart(2, "0")}`;
+ return `${hours}:${minutes
+ .toString()
+ .padStart(2, "0")}:${Math.floor(secs)
+ .toString()
+ .padStart(2, "0")}`;
}
if (minutes > 0) {
- return `${minutes}:${Math.floor(secs).toString().padStart(2, "0")}`;
+ return `${minutes}:${Math.floor(secs)
+ .toString()
+ .padStart(2, "0")}`;
}
if (interval >= 1) {
return `${Math.floor(secs)}s`;
@@ -710,7 +720,10 @@ export function Timeline() {
<div
className="relative flex-1"
style={{
- height: `${Math.max(200, Math.min(800, getTotalTracksHeight(tracks)))}px`,
+ height: `${Math.max(
+ 200,
+ Math.min(800, getTotalTracksHeight(tracks))
+ )}px`,
width: `${dynamicTimelineWidth}px`,
}}
>
@@ -724,7 +737,10 @@ export function Timeline() {
<div
className="absolute left-0 right-0 border-b border-muted/30 py-[0.05rem]"
style={{
- top: `${getCumulativeHeightBefore(tracks, index)}px`,
+ top: `${getCumulativeHeightBefore(
+ tracks,
+ index
+ )}px`,
height: `${getTrackHeight(track.type)}px`,
}}
onClick={(e) => {
@@ -1092,7 +1108,9 @@ function TimelineToolbar({
<TooltipTrigger asChild>
<Button variant="text" size="icon" onClick={toggleRippleEditing}>
<Link
- className={`h-4 w-4 ${rippleEditingEnabled ? "text-primary" : ""}`}
+ className={`h-4 w-4 ${
+ rippleEditingEnabled ? "text-primary" : ""
+ }`}
/>
</Button>
</TooltipTrigger>
diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx
index 26cf9254..e87d50e9 100644
--- a/apps/web/src/components/landing/handlebars.tsx
+++ b/apps/web/src/components/landing/handlebars.tsx
@@ -62,7 +62,7 @@ export function Handlebars({ children }: HandlebarsProps) {
whileDrag={{ scale: 1.1, cursor: "grabbing" }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
>
- <div className="w-2 h-8 rounded-full bg-yellow-500"></div>
+ <div className="w-2 h-8 rounded-full bg-yellow-500" />
</motion.div>
<motion.div
@@ -81,7 +81,7 @@ export function Handlebars({ children }: HandlebarsProps) {
whileDrag={{ scale: 1.1, cursor: "grabbing" }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
>
- <div className="w-2 h-8 rounded-full bg-yellow-500"></div>
+ <div className="w-2 h-8 rounded-full bg-yellow-500" />
</motion.div>
</div>
diff --git a/apps/web/src/hooks/use-editor-actions.ts b/apps/web/src/hooks/use-editor-actions.ts
index b2fad55f..bfe9a7b9 100644
--- a/apps/web/src/hooks/use-editor-actions.ts
+++ b/apps/web/src/hooks/use-editor-actions.ts
@@ -26,136 +26,204 @@ export function useEditorActions() {
const { activeProject } = useProjectStore();
// Playback actions
- useActionHandler("toggle-play", () => {
- toggle();
- }, undefined);
-
- useActionHandler("stop-playback", () => {
- if (isPlaying) {
+ useActionHandler(
+ "toggle-play",
+ () => {
toggle();
- }
- seek(0);
- }, undefined);
+ },
+ undefined
+ );
+
+ useActionHandler(
+ "stop-playback",
+ () => {
+ if (isPlaying) {
+ toggle();
+ }
+ seek(0);
+ },
+ undefined
+ );
- useActionHandler("seek-forward", (args) => {
- const seconds = args?.seconds ?? 1;
- seek(Math.min(duration, currentTime + seconds));
- }, undefined);
+ useActionHandler(
+ "seek-forward",
+ (args) => {
+ const seconds = args?.seconds ?? 1;
+ seek(Math.min(duration, currentTime + seconds));
+ },
+ undefined
+ );
- useActionHandler("seek-backward", (args) => {
- const seconds = args?.seconds ?? 1;
- seek(Math.max(0, currentTime - seconds));
- }, undefined);
+ useActionHandler(
+ "seek-backward",
+ (args) => {
+ const seconds = args?.seconds ?? 1;
+ seek(Math.max(0, currentTime - seconds));
+ },
+ undefined
+ );
- useActionHandler("frame-step-forward", () => {
- const projectFps = activeProject?.fps || 30;
- seek(Math.min(duration, currentTime + 1 / projectFps));
- }, undefined);
+ useActionHandler(
+ "frame-step-forward",
+ () => {
+ const projectFps = activeProject?.fps || 30;
+ seek(Math.min(duration, currentTime + 1 / projectFps));
+ },
+ undefined
+ );
- useActionHandler("frame-step-backward", () => {
- const projectFps = activeProject?.fps || 30;
- seek(Math.max(0, currentTime - 1 / projectFps));
- }, undefined);
+ useActionHandler(
+ "frame-step-backward",
+ () => {
+ const projectFps = activeProject?.fps || 30;
+ seek(Math.max(0, currentTime - 1 / projectFps));
+ },
+ undefined
+ );
- useActionHandler("jump-forward", (args) => {
- const seconds = args?.seconds ?? 5;
- seek(Math.min(duration, currentTime + seconds));
- }, undefined);
+ useActionHandler(
+ "jump-forward",
+ (args) => {
+ const seconds = args?.seconds ?? 5;
+ seek(Math.min(duration, currentTime + seconds));
+ },
+ undefined
+ );
- useActionHandler("jump-backward", (args) => {
- const seconds = args?.seconds ?? 5;
- seek(Math.max(0, currentTime - seconds));
- }, undefined);
+ useActionHandler(
+ "jump-backward",
+ (args) => {
+ const seconds = args?.seconds ?? 5;
+ seek(Math.max(0, currentTime - seconds));
+ },
+ undefined
+ );
- useActionHandler("goto-start", () => {
- seek(0);
- }, undefined);
+ useActionHandler(
+ "goto-start",
+ () => {
+ seek(0);
+ },
+ undefined
+ );
- useActionHandler("goto-end", () => {
- seek(duration);
- }, undefined);
+ useActionHandler(
+ "goto-end",
+ () => {
+ seek(duration);
+ },
+ undefined
+ );
// Timeline editing actions
- useActionHandler("split-element", () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element to split");
- return;
- }
+ useActionHandler(
+ "split-element",
+ () => {
+ if (selectedElements.length !== 1) {
+ toast.error("Select exactly one element to split");
+ return;
+ }
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t: any) => t.id === trackId);
- const element = track?.elements.find((el: any) => el.id === elementId);
+ const { trackId, elementId } = selectedElements[0];
+ const track = tracks.find((t: any) => t.id === trackId);
+ const element = track?.elements.find((el: any) => el.id === elementId);
- if (element) {
- const effectiveStart = element.startTime;
- const effectiveEnd =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd);
+ if (element) {
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
- if (currentTime > effectiveStart && currentTime < effectiveEnd) {
- splitElement(trackId, elementId, currentTime);
- } else {
- toast.error("Playhead must be within selected element");
+ if (currentTime > effectiveStart && currentTime < effectiveEnd) {
+ splitElement(trackId, elementId, currentTime);
+ } else {
+ toast.error("Playhead must be within selected element");
+ }
}
- }
- }, undefined);
+ },
+ undefined
+ );
- useActionHandler("delete-selected", () => {
- if (selectedElements.length === 0) {
- return;
- }
- selectedElements.forEach(
- ({ trackId, elementId }: { trackId: string; elementId: string }) => {
- removeElementFromTrack(trackId, elementId);
+ useActionHandler(
+ "delete-selected",
+ () => {
+ if (selectedElements.length === 0) {
+ return;
}
- );
- clearSelectedElements();
- }, undefined);
+ selectedElements.forEach(
+ ({ trackId, elementId }: { trackId: string; elementId: string }) => {
+ removeElementFromTrack(trackId, elementId);
+ }
+ );
+ clearSelectedElements();
+ },
+ undefined
+ );
- useActionHandler("select-all", () => {
- const allElements = tracks.flatMap((track: any) =>
- track.elements.map((element: any) => ({
- trackId: track.id,
- elementId: element.id,
- }))
- );
- setSelectedElements(allElements);
- }, undefined);
+ useActionHandler(
+ "select-all",
+ () => {
+ const allElements = tracks.flatMap((track: any) =>
+ track.elements.map((element: any) => ({
+ trackId: track.id,
+ elementId: element.id,
+ }))
+ );
+ setSelectedElements(allElements);
+ },
+ undefined
+ );
- useActionHandler("duplicate-selected", () => {
- if (selectedElements.length !== 1) {
- toast.error("Select exactly one element to duplicate");
- return;
- }
+ useActionHandler(
+ "duplicate-selected",
+ () => {
+ if (selectedElements.length !== 1) {
+ toast.error("Select exactly one element to duplicate");
+ return;
+ }
- const { trackId, elementId } = selectedElements[0];
- const track = tracks.find((t: any) => t.id === trackId);
- const element = track?.elements.find((el: any) => el.id === elementId);
+ const { trackId, elementId } = selectedElements[0];
+ const track = tracks.find((t: any) => t.id === trackId);
+ const element = track?.elements.find((el: any) => el.id === elementId);
- if (element) {
- const newStartTime =
- element.startTime +
- (element.duration - element.trimStart - element.trimEnd) +
- 0.1;
- const { id, ...elementWithoutId } = element;
+ if (element) {
+ const newStartTime =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd) +
+ 0.1;
+ const { id, ...elementWithoutId } = element;
- addElementToTrack(trackId, {
- ...elementWithoutId,
- startTime: newStartTime,
- });
- }
- }, undefined);
+ addElementToTrack(trackId, {
+ ...elementWithoutId,
+ startTime: newStartTime,
+ });
+ }
+ },
+ undefined
+ );
- useActionHandler("toggle-snapping", () => {
- toggleSnapping();
- }, undefined);
+ useActionHandler(
+ "toggle-snapping",
+ () => {
+ toggleSnapping();
+ },
+ undefined
+ );
// History actions
- useActionHandler("undo", () => {
- undo();
- }, undefined);
+ useActionHandler(
+ "undo",
+ () => {
+ undo();
+ },
+ undefined
+ );
- useActionHandler("redo", () => {
- redo();
- }, undefined);
+ useActionHandler(
+ "redo",
+ () => {
+ redo();
+ },
+ undefined
+ );
}
diff --git a/apps/web/src/stores/playback-store.ts b/apps/web/src/stores/playback-store.ts
index 543865f9..70ff490d 100644
--- a/apps/web/src/stores/playback-store.ts
+++ b/apps/web/src/stores/playback-store.ts
@@ -20,8 +20,20 @@ const startTimer = (store: () => PlaybackStore) => {
lastUpdate = now;
const newTime = state.currentTime + delta * state.speed;
- if (newTime >= state.duration) {
- // When video completes, pause and reset playhead to start
+
+ // Get actual content duration from timeline store
+ const { useTimelineStore } = require("@/stores/timeline-store");
+ const actualContentDuration = useTimelineStore
+ .getState()
+ .getTotalDuration();
+
+ // Stop at actual content end, not timeline duration (which has 10s minimum)
+ // It was either this or reducing default min timeline to 1 second
+ const effectiveDuration =
+ actualContentDuration > 0 ? actualContentDuration : state.duration;
+
+ if (newTime >= effectiveDuration) {
+ // When content completes, pause and reset playhead to start
state.pause();
state.setCurrentTime(0);
// Notify video elements to sync with reset