summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-07-08 00:42:46 +0200
committerMaze Winther <mazewinther@gmail.com>2025-07-08 00:42:46 +0200
commitea59cc395075c1961fa5d7984f7becde3b949e1f (patch)
tree0d74820a1c83d90e55d47d2e9abfe582cf48f978 /apps
parentc0cc4c009eecc3865ec93201bdaff4a7f1249577 (diff)
downloadopencut-ea59cc395075c1961fa5d7984f7becde3b949e1f.tar.gz
fix: ensure timeline clips can be extended to longer than 5s
Diffstat (limited to 'apps')
-rw-r--r--apps/web/src/components/editor/timeline-element.tsx2
-rw-r--r--apps/web/src/hooks/use-timeline-element-resize.ts71
-rw-r--r--apps/web/src/stores/timeline-store.ts25
3 files changed, 92 insertions, 6 deletions
diff --git a/apps/web/src/components/editor/timeline-element.tsx b/apps/web/src/components/editor/timeline-element.tsx
index 65126379..4a909ce4 100644
--- a/apps/web/src/components/editor/timeline-element.tsx
+++ b/apps/web/src/components/editor/timeline-element.tsx
@@ -45,6 +45,7 @@ export function TimelineElement({
const { mediaItems } = useMediaStore();
const {
updateElementTrim,
+ updateElementDuration,
removeElementFromTrack,
dragState,
splitElement,
@@ -67,6 +68,7 @@ export function TimelineElement({
track,
zoomLevel,
onUpdateTrim: updateElementTrim,
+ onUpdateDuration: updateElementDuration,
});
const effectiveDuration =
diff --git a/apps/web/src/hooks/use-timeline-element-resize.ts b/apps/web/src/hooks/use-timeline-element-resize.ts
index 3882a8f8..0e409977 100644
--- a/apps/web/src/hooks/use-timeline-element-resize.ts
+++ b/apps/web/src/hooks/use-timeline-element-resize.ts
@@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { ResizeState, TimelineElement, TimelineTrack } from "@/types/timeline";
+import { useMediaStore } from "@/stores/media-store";
interface UseTimelineElementResizeProps {
element: TimelineElement;
@@ -11,6 +12,11 @@ interface UseTimelineElementResizeProps {
trimStart: number,
trimEnd: number
) => void;
+ onUpdateDuration: (
+ trackId: string,
+ elementId: string,
+ duration: number
+ ) => void;
}
export function useTimelineElementResize({
@@ -18,8 +24,10 @@ export function useTimelineElementResize({
track,
zoomLevel,
onUpdateTrim,
+ onUpdateDuration,
}: UseTimelineElementResizeProps) {
const [resizing, setResizing] = useState<ResizeState | null>(null);
+ const { mediaItems } = useMediaStore();
// Set up document-level mouse listeners during resize (like proper drag behavior)
useEffect(() => {
@@ -60,6 +68,30 @@ export function useTimelineElementResize({
});
};
+ const canExtendElementDuration = () => {
+ // Text elements can always be extended
+ if (element.type === "text") {
+ return true;
+ }
+
+ // Media elements - check the media type
+ if (element.type === "media") {
+ const mediaItem = mediaItems.find((item) => item.id === element.mediaId);
+ if (!mediaItem) return false;
+
+ // Images can be extended (static content)
+ if (mediaItem.type === "image") {
+ return true;
+ }
+
+ // Videos and audio cannot be extended beyond their natural duration
+ // (no additional content exists)
+ return false;
+ }
+
+ return false;
+ };
+
const updateTrimFromMouseMove = (e: { clientX: number }) => {
if (!resizing) return;
@@ -68,19 +100,48 @@ export function useTimelineElementResize({
const deltaTime = deltaX / (50 * zoomLevel);
if (resizing.side === "left") {
+ // Left resize - only trim within original duration
const maxAllowed = element.duration - resizing.initialTrimEnd - 0.1;
const calculated = resizing.initialTrimStart + deltaTime;
const newTrimStart = Math.max(0, Math.min(maxAllowed, calculated));
onUpdateTrim(track.id, element.id, newTrimStart, resizing.initialTrimEnd);
} else {
- // For right resize (expanding element), allow trimEnd to go to 0 but cap at element duration
+ // Right resize - can extend duration for supported element types
const calculated = resizing.initialTrimEnd - deltaTime;
- // Prevent negative trim AND prevent trimEnd from exceeding element duration
- const maxTrimEnd = element.duration - resizing.initialTrimStart - 0.1; // Leave at least 0.1s visible
- const newTrimEnd = Math.max(0, Math.min(maxTrimEnd, calculated));
- onUpdateTrim(track.id, element.id, resizing.initialTrimStart, newTrimEnd);
+ if (calculated < 0) {
+ // We're trying to extend beyond original duration
+ if (canExtendElementDuration()) {
+ // Extend the duration instead of reducing trimEnd further
+ const extensionNeeded = Math.abs(calculated);
+ const newDuration = element.duration + extensionNeeded;
+ const newTrimEnd = 0; // Reset trimEnd to 0 since we're extending
+
+ // Update duration first, then trim
+ onUpdateDuration(track.id, element.id, newDuration);
+ onUpdateTrim(
+ track.id,
+ element.id,
+ resizing.initialTrimStart,
+ newTrimEnd
+ );
+ } else {
+ // Can't extend - just set trimEnd to 0 (maximum possible extension)
+ onUpdateTrim(track.id, element.id, resizing.initialTrimStart, 0);
+ }
+ } else {
+ // Normal trimming within original duration
+ const maxTrimEnd = element.duration - resizing.initialTrimStart - 0.1; // Leave at least 0.1s visible
+ const newTrimEnd = Math.max(0, Math.min(maxTrimEnd, calculated));
+
+ onUpdateTrim(
+ track.id,
+ element.id,
+ resizing.initialTrimStart,
+ newTrimEnd
+ );
+ }
}
};
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts
index 6d1a0bd1..6de24c84 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/stores/timeline-store.ts
@@ -87,6 +87,11 @@ interface TimelineStore {
trimStart: number,
trimEnd: number
) => void;
+ updateElementDuration: (
+ trackId: string,
+ elementId: string,
+ duration: number
+ ) => void;
updateElementStartTime: (
trackId: string,
elementId: string,
@@ -284,7 +289,9 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
removeTrack: (trackId) => {
get().pushHistory();
- updateTracksAndSave(get()._tracks.filter((track) => track.id !== trackId));
+ updateTracksAndSave(
+ get()._tracks.filter((track) => track.id !== trackId)
+ );
},
addElementToTrack: (trackId, elementData) => {
@@ -439,6 +446,22 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
);
},
+ updateElementDuration: (trackId, elementId, duration) => {
+ get().pushHistory();
+ updateTracksAndSave(
+ get()._tracks.map((track) =>
+ track.id === trackId
+ ? {
+ ...track,
+ elements: track.elements.map((element) =>
+ element.id === elementId ? { ...element, duration } : element
+ ),
+ }
+ : track
+ )
+ );
+ },
+
updateElementStartTime: (trackId, elementId, startTime) => {
get().pushHistory();
updateTracksAndSave(