diff --git a/apps/desktop/src/routes/editor/ConfigSidebar.tsx b/apps/desktop/src/routes/editor/ConfigSidebar.tsx index 08906742b..37d64b25d 100644 --- a/apps/desktop/src/routes/editor/ConfigSidebar.tsx +++ b/apps/desktop/src/routes/editor/ConfigSidebar.tsx @@ -263,7 +263,7 @@ export function ConfigSidebar() { class={cx( "flex justify-center relative border-transparent border z-10 items-center rounded-md size-9 transition will-change-transform", state.selectedTab !== item.id && - "group-hover:border-gray-300 group-disabled:border-none" + "group-hover:border-gray-300 group-disabled:border-none" )} > @@ -940,17 +940,13 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { ref={setBackgroundRef} class="flex overflow-x-auto overscroll-contain relative z-40 flex-row gap-2 items-center mb-3 text-xs hide-scroll" style={{ - "-webkit-mask-image": `linear-gradient(to right, transparent, black ${ - scrollX() > 0 ? "24px" : "0" - }, black calc(100% - ${ - reachedEndOfScroll() ? "0px" : "24px" - }), transparent)`, - - "mask-image": `linear-gradient(to right, transparent, black ${ - scrollX() > 0 ? "24px" : "0" - }, black calc(100% - ${ - reachedEndOfScroll() ? "0px" : "24px" - }), transparent);`, + "-webkit-mask-image": `linear-gradient(to right, transparent, black ${scrollX() > 0 ? "24px" : "0" + }, black calc(100% - ${reachedEndOfScroll() ? "0px" : "24px" + }), transparent)`, + + "mask-image": `linear-gradient(to right, transparent, black ${scrollX() > 0 ? "24px" : "0" + }, black calc(100% - ${reachedEndOfScroll() ? "0px" : "24px" + }), transparent);`, }} > @@ -977,10 +973,10 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { value={ project.background.source.type === "wallpaper" ? wallpapers()?.find((w) => - ( - project.background.source as { path?: string } - ).path?.includes(w.id) - )?.url ?? undefined + ( + project.background.source as { path?: string } + ).path?.includes(w.id) + )?.url ?? undefined : undefined } onChange={(photoUrl) => { @@ -1271,7 +1267,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { const rawNewAngle = Math.round( start + - (downEvent.clientY - moveEvent.clientY) + (downEvent.clientY - moveEvent.clientY) ) % max; const newAngle = moveEvent.shiftKey ? rawNewAngle @@ -1281,7 +1277,7 @@ function BackgroundConfig(props: { scrollRef: HTMLDivElement }) { !moveEvent.shiftKey && hapticsEnabled() && project.background.source.type === - "gradient" && + "gradient" && project.background.source.angle !== newAngle ) { commands.performHapticFeedback( @@ -1494,8 +1490,8 @@ function CameraConfig(props: { scrollRef: HTMLDivElement }) { item.x === "left" ? "left-2" : item.x === "right" - ? "right-2" - : "left-1/2 transform -translate-x-1/2", + ? "right-2" + : "left-1/2 transform -translate-x-1/2", item.y === "top" ? "top-2" : "bottom-2" )} onClick={() => setProject("camera", "position", item)} @@ -1752,8 +1748,7 @@ function ZoomSegmentConfig(props: { createEffect(() => { video.src = convertFileSrc( // TODO: this shouldn't be so hardcoded - `${ - editorInstance.path + `${editorInstance.path }/content/segments/segment-${segmentIndex()}/display.mp4` ); }); @@ -1850,7 +1845,7 @@ function ZoomSegmentConfig(props: { x: Math.max( Math.min( (moveEvent.clientX - bounds.left) / - bounds.width, + bounds.width, 1 ), 0 @@ -1858,7 +1853,7 @@ function ZoomSegmentConfig(props: { y: Math.max( Math.min( (moveEvent.clientY - bounds.top) / - bounds.height, + bounds.height, 1 ), 0 @@ -1873,12 +1868,10 @@ function ZoomSegmentConfig(props: {
@@ -1907,6 +1900,14 @@ function ClipSegmentConfig(props: { }) { const { setProject, setEditorState, project } = useEditorContext(); + // Local state for the input field + const [inputValue, setInputValue] = createSignal(props.segment.timescale.toString()); + + // Update input value when timescale changes externally (e.g., from slider or buttons) + createEffect(() => { + setInputValue(props.segment.timescale.toString()); + }); + return ( <>
@@ -1945,6 +1946,149 @@ function ClipSegmentConfig(props: { Delete
+ }> +
+
+ + setProject( + "timeline", + "segments", + props.segmentIndex, + "timescale", + v[0] + ) + } + minValue={0.1} + maxValue={10} + step={0.01} + formatTooltip={(v) => `${v.toFixed(2)}x`} + class="flex-1" + /> +
+ { + const value = e.currentTarget.value; + setInputValue(value); + + const numValue = parseFloat(value); + if (!isNaN(numValue) && numValue > 0 && numValue <= 10) { + setProject( + "timeline", + "segments", + props.segmentIndex, + "timescale", + numValue + ); + } + }} + onFocus={(e) => { + // Select all text on focus for easy replacement + e.currentTarget.select(); + }} + onBlur={(e) => { + const value = parseFloat(e.currentTarget.value); + if (isNaN(value) || value <= 0) { + setInputValue(props.segment.timescale.toFixed(2)); + } else if (value > 10) { + setProject( + "timeline", + "segments", + props.segmentIndex, + "timescale", + 10 + ); + setInputValue("10.00"); + } else { + // Format to 2 decimal places on blur + setInputValue(value.toFixed(2)); + } + }} + class="w-16 px-2 py-1 text-xs text-center border rounded-md bg-gray-2 text-gray-12" + min="0.01" + max="10" + step="0.01" + /> + x +
+
+
+ + + + + +
+
+
} /> diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index 2f3ea8128..329ad7df0 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -71,7 +71,7 @@ export function ClipTrack( const relativeSegment = mergeProps(segment, () => ({ start: prevDuration(), - end: segment.end - segment.start + prevDuration(), + end: prevDuration() + (segment.end - segment.start) / segment.timescale, })); const segmentX = useSegmentTranslateX(() => relativeSegment); @@ -79,7 +79,7 @@ export function ClipTrack( const segmentRecording = (s = i()) => editorInstance.recordings.segments[ - segments()[s].recordingSegment ?? 0 + segments()[s].recordingSegment ?? 0 ]; const marker = useSectionMarker(() => ({ @@ -112,9 +112,8 @@ export function ClipTrack(
@@ -277,7 +276,7 @@ export function ClipTrack( segmentI === i() ? acc : acc + - (segment.end - segment.start) / segment.timescale, + (segment.end - segment.start) / segment.timescale, 0 ); @@ -290,7 +289,7 @@ export function ClipTrack( const prevSegmentIsSameClip = prevSegment?.recordingSegment !== undefined ? prevSegment.recordingSegment === - segment.recordingSegment + segment.recordingSegment : false; function update(event: MouseEvent) { @@ -344,6 +343,12 @@ export function ClipTrack( {" "} {(segment.end - segment.start).toFixed(1)}s
+ +
+ + {segment.timescale}x +
+
); @@ -367,7 +372,7 @@ export function ClipTrack( segmentI === i() ? acc : acc + - (segment.end - segment.start) / segment.timescale, + (segment.end - segment.start) / segment.timescale, 0 ); @@ -375,7 +380,7 @@ export function ClipTrack( const nextSegmentIsSameClip = nextSegment?.recordingSegment !== undefined ? nextSegment.recordingSegment === - segment.recordingSegment + segment.recordingSegment : false; function update(event: MouseEvent) { @@ -482,9 +487,8 @@ function Markings(props: { segment: TimelineSegment; prevDuration: number }) { {(marking) => (
@@ -528,10 +532,10 @@ function useSectionMarker( } ): Accessor< | ({ type: "dual" } & ( - | { left: SectionMarker; right: null } - | { left: null; right: SectionMarker } - | { left: SectionMarker; right: SectionMarker } - )) + | { left: SectionMarker; right: null } + | { left: null; right: SectionMarker } + | { left: SectionMarker; right: SectionMarker } + )) | { type: "single"; value: SectionMarker } | null > { @@ -544,10 +548,10 @@ function useSectionMarker( return segments[0].start === 0 ? null : { - type: "dual", - right: { type: "time", time: segments[0].start }, - left: null, - }; + type: "dual", + right: { type: "time", time: segments[0].start }, + left: null, + }; } if (i === segments.length - 1 && position === "right") { diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index 09ff520c5..8556b768c 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -82,6 +82,7 @@ declare global { const IconLucideDatabase: typeof import('~icons/lucide/database.jsx')['default'] const IconLucideEdit: typeof import('~icons/lucide/edit.jsx')['default'] const IconLucideEye: typeof import('~icons/lucide/eye.jsx')['default'] + const IconLucideFastForward: typeof import('~icons/lucide/fast-forward.jsx')['default'] const IconLucideFolder: typeof import('~icons/lucide/folder.jsx')['default'] const IconLucideGift: typeof import('~icons/lucide/gift.jsx')['default'] const IconLucideHardDrive: typeof import('~icons/lucide/hard-drive.jsx')['default']