From 3cec3ac21915c6bbeed97e5554e1de95c437c9e0 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:25:09 +0300 Subject: [PATCH 1/3] fixes --- apps/desktop/src/routes/editor/Player.tsx | 8 +- .../src/routes/editor/Timeline/ClipTrack.tsx | 135 ++++++++++-------- 2 files changed, 79 insertions(+), 64 deletions(-) diff --git a/apps/desktop/src/routes/editor/Player.tsx b/apps/desktop/src/routes/editor/Player.tsx index f090484f77..001cf75744 100644 --- a/apps/desktop/src/routes/editor/Player.tsx +++ b/apps/desktop/src/routes/editor/Player.tsx @@ -147,7 +147,13 @@ export function Player() { // Register keyboard shortcuts in one place useEditorShortcuts( - () => document.activeElement === document.body, + () => { + const el = document.activeElement; + if (!el) return true; + const tagName = el.tagName.toLowerCase(); + const isContentEditable = el.getAttribute("contenteditable") === "true"; + return !(tagName === "input" || tagName === "textarea" || isContentEditable); + }, [ { combo: "S", diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index b75b36a68e..6e7c324236 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -309,30 +309,34 @@ export function ClipTrack( if (m.type === "single") return m.value; })()} > - {(marker) => ( -
- { - const m = marker(); - return m.type === "time" ? m.time : 0; - })()} - onClick={() => { - setProject( - "timeline", - "segments", - produce((s) => { - if (marker().type === "reset") { - s[i() - 1].end = s[i()].end; - s.splice(i(), 1); - } else { - s[i() - 1].end = s[i()].start; - } - }), - ); - }} - /> -
- )} + {(markerValue) => { + const value = createMemo(() => { + const m = markerValue(); + return m.type === "time" ? m.time : 0; + }); + + return ( +
+ { + setProject( + "timeline", + "segments", + produce((s) => { + if (markerValue().type === "reset") { + s[i() - 1].end = s[i()].end; + s.splice(i(), 1); + } else { + s[i() - 1].end = s[i()].start; + } + }), + ); + }} + /> +
+ ); + }} { @@ -345,16 +349,16 @@ export function ClipTrack( return m.right; })()} > - {(marker) => { - const markerValue = marker(); + {(markerValue) => { + const value = createMemo(() => { + const m = markerValue(); + return m.type === "time" ? m.time : 0; + }); + return (
{ setProject( @@ -684,34 +688,38 @@ export function ClipTrack( return m.left; })()} > - {(marker) => ( -
-
-
- { - const m = marker(); - return m.type === "time" ? m.time : 0; - })()} - class="-right-px absolute rounded-l-full !pr-1.5 rounded-tr-full" - onClick={() => { - setProject( - "timeline", - "segments", - i(), - "end", - segmentRecording().display.duration, - ); - }} - /> + {(markerValue) => { + const value = createMemo(() => { + const m = markerValue(); + return m.type === "time" ? m.time : 0; + }); + + return ( +
+
+
+ { + setProject( + "timeline", + "segments", + i(), + "end", + segmentRecording().display.duration, + ); + }} + /> +
-
- )} + ); + }} ); @@ -770,11 +778,12 @@ function CutOffsetButton(props: { )} onClick={() => props.onClick?.()} > - {props.value === 0 ? ( - - ) : ( - formatTime(props.value) - )} + } + > + {formatTime(props.value)} + ); } From 2724a712433c7d88a93d6036c6a082b39b06e865 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:28:29 +0300 Subject: [PATCH 2/3] formatting --- apps/desktop/src/routes/editor/Player.tsx | 821 +++++++++++----------- 1 file changed, 411 insertions(+), 410 deletions(-) diff --git a/apps/desktop/src/routes/editor/Player.tsx b/apps/desktop/src/routes/editor/Player.tsx index 001cf75744..c97e23c6a8 100644 --- a/apps/desktop/src/routes/editor/Player.tsx +++ b/apps/desktop/src/routes/editor/Player.tsx @@ -13,424 +13,425 @@ import { useEditorShortcuts } from "./useEditorShortcuts"; import { formatTime } from "./utils"; export function Player() { - const { - project, - editorInstance, - setDialog, - totalDuration, - editorState, - setEditorState, - zoomOutLimit, - setProject, - } = useEditorContext(); - - // Load captions on mount - onMount(async () => { - if (editorInstance && editorInstance.path) { - // Still load captions into the store since they will be used by the GPU renderer - await captionsStore.loadCaptions(editorInstance.path); - - // Synchronize captions settings with project configuration - // This ensures the GPU renderer will receive the caption settings - if (editorInstance && project) { - const updatedProject = { ...project }; - - // Add captions data to project configuration if it doesn't exist - if ( - !updatedProject.captions && - captionsStore.state.segments.length > 0 - ) { - updatedProject.captions = { - segments: captionsStore.state.segments.map((segment) => ({ - id: segment.id, - start: segment.start, - end: segment.end, - text: segment.text, - })), - settings: { - enabled: captionsStore.state.settings.enabled, - font: captionsStore.state.settings.font, - size: captionsStore.state.settings.size, - color: captionsStore.state.settings.color, - backgroundColor: captionsStore.state.settings.backgroundColor, - backgroundOpacity: captionsStore.state.settings.backgroundOpacity, - position: captionsStore.state.settings.position, - bold: captionsStore.state.settings.bold, - italic: captionsStore.state.settings.italic, - outline: captionsStore.state.settings.outline, - outlineColor: captionsStore.state.settings.outlineColor, - exportWithSubtitles: - captionsStore.state.settings.exportWithSubtitles, - }, - }; - - // Update the project with captions data - setProject(updatedProject); - - // Save the updated project configuration - await commands.setProjectConfig(updatedProject); - } - } - } - }); - - // Continue to update current caption when playback time changes - // This is still needed for CaptionsTab to highlight the current caption - createEffect(() => { - const time = editorState.playbackTime; - // Only update captions if we have a valid time and segments exist - if ( - time !== undefined && - time >= 0 && - captionsStore.state.segments.length > 0 - ) { - captionsStore.updateCurrentCaption(time); - } - }); - - const [canvasContainerRef, setCanvasContainerRef] = - createSignal(); - const containerBounds = createElementBounds(canvasContainerRef); - - const isAtEnd = () => { - const total = totalDuration(); - return total > 0 && total - editorState.playbackTime <= 0.1; - }; - - const cropDialogHandler = () => { - const display = editorInstance.recordings.segments[0].display; - setDialog({ - open: true, - type: "crop", - position: { - ...(project.background.crop?.position ?? { x: 0, y: 0 }), - }, - size: { - ...(project.background.crop?.size ?? { - x: display.width, - y: display.height, - }), - }, - }); - }; - - createEffect(() => { - if (isAtEnd() && editorState.playing) { - commands.stopPlayback(); - setEditorState("playing", false); - } - }); - - const handlePlayPauseClick = async () => { - try { - if (isAtEnd()) { - await commands.stopPlayback(); - setEditorState("playbackTime", 0); - await commands.seekTo(0); - await commands.startPlayback(FPS, OUTPUT_SIZE); - setEditorState("playing", true); - } else if (editorState.playing) { - await commands.stopPlayback(); - setEditorState("playing", false); - } else { - // Ensure we seek to the current playback time before starting playback - await commands.seekTo(Math.floor(editorState.playbackTime * FPS)); - await commands.startPlayback(FPS, OUTPUT_SIZE); - setEditorState("playing", true); - } - if (editorState.playing) setEditorState("previewTime", null); - } catch (error) { - console.error("Error handling play/pause:", error); - setEditorState("playing", false); - } - }; - - // Register keyboard shortcuts in one place - useEditorShortcuts( - () => { - const el = document.activeElement; - if (!el) return true; - const tagName = el.tagName.toLowerCase(); - const isContentEditable = el.getAttribute("contenteditable") === "true"; - return !(tagName === "input" || tagName === "textarea" || isContentEditable); - }, - [ - { - combo: "S", - handler: () => - setEditorState( - "timeline", - "interactMode", - editorState.timeline.interactMode === "split" ? "seek" : "split", - ), - }, - { - combo: "Mod+=", - handler: () => - editorState.timeline.transform.updateZoom( - editorState.timeline.transform.zoom / 1.1, - editorState.playbackTime, - ), - }, - { - combo: "Mod+-", - handler: () => - editorState.timeline.transform.updateZoom( - editorState.timeline.transform.zoom * 1.1, - editorState.playbackTime, - ), - }, - { - combo: "Space", - handler: async () => { - const prevTime = editorState.previewTime; - - if (!editorState.playing) { - if (prevTime !== null) setEditorState("playbackTime", prevTime); - - await commands.seekTo(Math.floor(editorState.playbackTime * FPS)); - } - - await handlePlayPauseClick(); - }, - }, - ], - ); - - return ( -
-
- - cropDialogHandler()} - leftIcon={} - > - Crop - -
- -
-
-
-
- - - - - -
-
-
- - tooltipText="Toggle Split" - kbd={["S"]} - pressed={editorState.timeline.interactMode === "split"} - onChange={(v: boolean) => - setEditorState("timeline", "interactMode", v ? "split" : "seek") - } - as={KToggleButton} - variant="danger" - leftIcon={ - - } - /> -
- - { - editorState.timeline.transform.updateZoom( - editorState.timeline.transform.zoom * 1.1, - editorState.playbackTime, - ); - }} - class="text-gray-12 size-5 will-change-[opacity] transition-opacity hover:opacity-70" - /> - - - { - editorState.timeline.transform.updateZoom( - editorState.timeline.transform.zoom / 1.1, - editorState.playbackTime, - ); - }} - class="text-gray-12 size-5 will-change-[opacity] transition-opacity hover:opacity-70" - /> - - { - editorState.timeline.transform.updateZoom( - (1 - v) * zoomOutLimit(), - editorState.playbackTime, - ); - }} - formatTooltip={() => - `${editorState.timeline.transform.zoom.toFixed( - 0, - )} seconds visible` - } - /> -
-
-
- ); + const { + project, + editorInstance, + setDialog, + totalDuration, + editorState, + setEditorState, + zoomOutLimit, + setProject, + } = useEditorContext(); + + // Load captions on mount + onMount(async () => { + if (editorInstance && editorInstance.path) { + // Still load captions into the store since they will be used by the GPU renderer + await captionsStore.loadCaptions(editorInstance.path); + + // Synchronize captions settings with project configuration + // This ensures the GPU renderer will receive the caption settings + if (editorInstance && project) { + const updatedProject = { ...project }; + + // Add captions data to project configuration if it doesn't exist + if ( + !updatedProject.captions && + captionsStore.state.segments.length > 0 + ) { + updatedProject.captions = { + segments: captionsStore.state.segments.map((segment) => ({ + id: segment.id, + start: segment.start, + end: segment.end, + text: segment.text, + })), + settings: { + enabled: captionsStore.state.settings.enabled, + font: captionsStore.state.settings.font, + size: captionsStore.state.settings.size, + color: captionsStore.state.settings.color, + backgroundColor: captionsStore.state.settings.backgroundColor, + backgroundOpacity: captionsStore.state.settings.backgroundOpacity, + position: captionsStore.state.settings.position, + bold: captionsStore.state.settings.bold, + italic: captionsStore.state.settings.italic, + outline: captionsStore.state.settings.outline, + outlineColor: captionsStore.state.settings.outlineColor, + exportWithSubtitles: + captionsStore.state.settings.exportWithSubtitles, + }, + }; + + // Update the project with captions data + setProject(updatedProject); + + // Save the updated project configuration + await commands.setProjectConfig(updatedProject); + } + } + } + }); + + // Continue to update current caption when playback time changes + // This is still needed for CaptionsTab to highlight the current caption + createEffect(() => { + const time = editorState.playbackTime; + // Only update captions if we have a valid time and segments exist + if ( + time !== undefined && + time >= 0 && + captionsStore.state.segments.length > 0 + ) { + captionsStore.updateCurrentCaption(time); + } + }); + + const [canvasContainerRef, setCanvasContainerRef] = + createSignal(); + const containerBounds = createElementBounds(canvasContainerRef); + + const isAtEnd = () => { + const total = totalDuration(); + return total > 0 && total - editorState.playbackTime <= 0.1; + }; + + const cropDialogHandler = () => { + const display = editorInstance.recordings.segments[0].display; + setDialog({ + open: true, + type: "crop", + position: { + ...(project.background.crop?.position ?? { x: 0, y: 0 }), + }, + size: { + ...(project.background.crop?.size ?? { + x: display.width, + y: display.height, + }), + }, + }); + }; + + createEffect(() => { + if (isAtEnd() && editorState.playing) { + commands.stopPlayback(); + setEditorState("playing", false); + } + }); + + const handlePlayPauseClick = async () => { + try { + if (isAtEnd()) { + await commands.stopPlayback(); + setEditorState("playbackTime", 0); + await commands.seekTo(0); + await commands.startPlayback(FPS, OUTPUT_SIZE); + setEditorState("playing", true); + } else if (editorState.playing) { + await commands.stopPlayback(); + setEditorState("playing", false); + } else { + // Ensure we seek to the current playback time before starting playback + await commands.seekTo(Math.floor(editorState.playbackTime * FPS)); + await commands.startPlayback(FPS, OUTPUT_SIZE); + setEditorState("playing", true); + } + if (editorState.playing) setEditorState("previewTime", null); + } catch (error) { + console.error("Error handling play/pause:", error); + setEditorState("playing", false); + } + }; + + // Register keyboard shortcuts in one place + useEditorShortcuts(() => { + const el = document.activeElement; + if (!el) return true; + const tagName = el.tagName.toLowerCase(); + const isContentEditable = el.getAttribute("contenteditable") === "true"; + return !( + tagName === "input" || + tagName === "textarea" || + isContentEditable + ); + }, [ + { + combo: "S", + handler: () => + setEditorState( + "timeline", + "interactMode", + editorState.timeline.interactMode === "split" ? "seek" : "split" + ), + }, + { + combo: "Mod+=", + handler: () => + editorState.timeline.transform.updateZoom( + editorState.timeline.transform.zoom / 1.1, + editorState.playbackTime + ), + }, + { + combo: "Mod+-", + handler: () => + editorState.timeline.transform.updateZoom( + editorState.timeline.transform.zoom * 1.1, + editorState.playbackTime + ), + }, + { + combo: "Space", + handler: async () => { + const prevTime = editorState.previewTime; + + if (!editorState.playing) { + if (prevTime !== null) setEditorState("playbackTime", prevTime); + + await commands.seekTo(Math.floor(editorState.playbackTime * FPS)); + } + + await handlePlayPauseClick(); + }, + }, + ]); + + return ( +
+
+ + cropDialogHandler()} + leftIcon={} + > + Crop + +
+ +
+
+
+
+ + + + + +
+
+
+ + tooltipText="Toggle Split" + kbd={["S"]} + pressed={editorState.timeline.interactMode === "split"} + onChange={(v: boolean) => + setEditorState("timeline", "interactMode", v ? "split" : "seek") + } + as={KToggleButton} + variant="danger" + leftIcon={ + + } + /> +
+ + { + editorState.timeline.transform.updateZoom( + editorState.timeline.transform.zoom * 1.1, + editorState.playbackTime + ); + }} + class="text-gray-12 size-5 will-change-[opacity] transition-opacity hover:opacity-70" + /> + + + { + editorState.timeline.transform.updateZoom( + editorState.timeline.transform.zoom / 1.1, + editorState.playbackTime + ); + }} + class="text-gray-12 size-5 will-change-[opacity] transition-opacity hover:opacity-70" + /> + + { + editorState.timeline.transform.updateZoom( + (1 - v) * zoomOutLimit(), + editorState.playbackTime + ); + }} + formatTooltip={() => + `${editorState.timeline.transform.zoom.toFixed( + 0 + )} seconds visible` + } + /> +
+
+
+ ); } // CSS for checkerboard grid (adaptive to light/dark mode) const gridStyle = { - "background-image": - "linear-gradient(45deg, rgba(128,128,128,0.12) 25%, transparent 25%), " + - "linear-gradient(-45deg, rgba(128,128,128,0.12) 25%, transparent 25%), " + - "linear-gradient(45deg, transparent 75%, rgba(128,128,128,0.12) 75%), " + - "linear-gradient(-45deg, transparent 75%, rgba(128,128,128,0.12) 75%)", - "background-size": "40px 40px", - "background-position": "0 0, 0 20px, 20px -20px, -20px 0px", - "background-color": "rgba(200,200,200,0.08)", + "background-image": + "linear-gradient(45deg, rgba(128,128,128,0.12) 25%, transparent 25%), " + + "linear-gradient(-45deg, rgba(128,128,128,0.12) 25%, transparent 25%), " + + "linear-gradient(45deg, transparent 75%, rgba(128,128,128,0.12) 75%), " + + "linear-gradient(-45deg, transparent 75%, rgba(128,128,128,0.12) 75%)", + "background-size": "40px 40px", + "background-position": "0 0, 0 20px, 20px -20px, -20px 0px", + "background-color": "rgba(200,200,200,0.08)", }; function PreviewCanvas() { - const { latestFrame } = useEditorContext(); - - let canvasRef: HTMLCanvasElement | undefined; - - const [canvasContainerRef, setCanvasContainerRef] = - createSignal(); - const containerBounds = createElementBounds(canvasContainerRef); - - createEffect(() => { - const frame = latestFrame(); - if (!frame) return; - if (!canvasRef) return; - const ctx = canvasRef.getContext("2d"); - ctx?.putImageData(frame.data, 0, 0); - }); - - return ( -
- - {(currentFrame) => { - const padding = 4; - - const containerAspect = () => { - if (containerBounds.width && containerBounds.height) { - return ( - (containerBounds.width - padding * 2) / - (containerBounds.height - padding * 2) - ); - } - - return 1; - }; - - const frameAspect = () => - currentFrame().width / currentFrame().data.height; - - const size = () => { - if (frameAspect() < containerAspect()) { - const height = (containerBounds.height ?? 0) - padding * 1; - - return { - width: height * frameAspect(), - height, - }; - } - - const width = (containerBounds.width ?? 0) - padding * 2; - - return { - width, - height: width / frameAspect(), - }; - }; - - return ( -
- -
- ); - }} -
-
- ); + const { latestFrame } = useEditorContext(); + + let canvasRef: HTMLCanvasElement | undefined; + + const [canvasContainerRef, setCanvasContainerRef] = + createSignal(); + const containerBounds = createElementBounds(canvasContainerRef); + + createEffect(() => { + const frame = latestFrame(); + if (!frame) return; + if (!canvasRef) return; + const ctx = canvasRef.getContext("2d"); + ctx?.putImageData(frame.data, 0, 0); + }); + + return ( +
+ + {(currentFrame) => { + const padding = 4; + + const containerAspect = () => { + if (containerBounds.width && containerBounds.height) { + return ( + (containerBounds.width - padding * 2) / + (containerBounds.height - padding * 2) + ); + } + + return 1; + }; + + const frameAspect = () => + currentFrame().width / currentFrame().data.height; + + const size = () => { + if (frameAspect() < containerAspect()) { + const height = (containerBounds.height ?? 0) - padding * 1; + + return { + width: height * frameAspect(), + height, + }; + } + + const width = (containerBounds.width ?? 0) - padding * 2; + + return { + width, + height: width / frameAspect(), + }; + }; + + return ( +
+ +
+ ); + }} +
+
+ ); } function Time(props: { seconds: number; fps?: number; class?: string }) { - return ( - - {formatTime(props.seconds, props.fps ?? FPS)} - - ); + return ( + + {formatTime(props.seconds, props.fps ?? FPS)} + + ); } From 9ea2c9137ccb6f04214f847bf8fbc1993d983b2c Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:31:54 +0300 Subject: [PATCH 3/3] Update Player.tsx --- apps/desktop/src/routes/editor/Player.tsx | 822 +++++++++++----------- 1 file changed, 411 insertions(+), 411 deletions(-) diff --git a/apps/desktop/src/routes/editor/Player.tsx b/apps/desktop/src/routes/editor/Player.tsx index c97e23c6a8..79f8820ae2 100644 --- a/apps/desktop/src/routes/editor/Player.tsx +++ b/apps/desktop/src/routes/editor/Player.tsx @@ -13,425 +13,425 @@ import { useEditorShortcuts } from "./useEditorShortcuts"; import { formatTime } from "./utils"; export function Player() { - const { - project, - editorInstance, - setDialog, - totalDuration, - editorState, - setEditorState, - zoomOutLimit, - setProject, - } = useEditorContext(); - - // Load captions on mount - onMount(async () => { - if (editorInstance && editorInstance.path) { - // Still load captions into the store since they will be used by the GPU renderer - await captionsStore.loadCaptions(editorInstance.path); - - // Synchronize captions settings with project configuration - // This ensures the GPU renderer will receive the caption settings - if (editorInstance && project) { - const updatedProject = { ...project }; - - // Add captions data to project configuration if it doesn't exist - if ( - !updatedProject.captions && - captionsStore.state.segments.length > 0 - ) { - updatedProject.captions = { - segments: captionsStore.state.segments.map((segment) => ({ - id: segment.id, - start: segment.start, - end: segment.end, - text: segment.text, - })), - settings: { - enabled: captionsStore.state.settings.enabled, - font: captionsStore.state.settings.font, - size: captionsStore.state.settings.size, - color: captionsStore.state.settings.color, - backgroundColor: captionsStore.state.settings.backgroundColor, - backgroundOpacity: captionsStore.state.settings.backgroundOpacity, - position: captionsStore.state.settings.position, - bold: captionsStore.state.settings.bold, - italic: captionsStore.state.settings.italic, - outline: captionsStore.state.settings.outline, - outlineColor: captionsStore.state.settings.outlineColor, - exportWithSubtitles: - captionsStore.state.settings.exportWithSubtitles, - }, - }; - - // Update the project with captions data - setProject(updatedProject); - - // Save the updated project configuration - await commands.setProjectConfig(updatedProject); - } - } - } - }); - - // Continue to update current caption when playback time changes - // This is still needed for CaptionsTab to highlight the current caption - createEffect(() => { - const time = editorState.playbackTime; - // Only update captions if we have a valid time and segments exist - if ( - time !== undefined && - time >= 0 && - captionsStore.state.segments.length > 0 - ) { - captionsStore.updateCurrentCaption(time); - } - }); - - const [canvasContainerRef, setCanvasContainerRef] = - createSignal(); - const containerBounds = createElementBounds(canvasContainerRef); - - const isAtEnd = () => { - const total = totalDuration(); - return total > 0 && total - editorState.playbackTime <= 0.1; - }; - - const cropDialogHandler = () => { - const display = editorInstance.recordings.segments[0].display; - setDialog({ - open: true, - type: "crop", - position: { - ...(project.background.crop?.position ?? { x: 0, y: 0 }), - }, - size: { - ...(project.background.crop?.size ?? { - x: display.width, - y: display.height, - }), - }, - }); - }; - - createEffect(() => { - if (isAtEnd() && editorState.playing) { - commands.stopPlayback(); - setEditorState("playing", false); - } - }); - - const handlePlayPauseClick = async () => { - try { - if (isAtEnd()) { - await commands.stopPlayback(); - setEditorState("playbackTime", 0); - await commands.seekTo(0); - await commands.startPlayback(FPS, OUTPUT_SIZE); - setEditorState("playing", true); - } else if (editorState.playing) { - await commands.stopPlayback(); - setEditorState("playing", false); - } else { - // Ensure we seek to the current playback time before starting playback - await commands.seekTo(Math.floor(editorState.playbackTime * FPS)); - await commands.startPlayback(FPS, OUTPUT_SIZE); - setEditorState("playing", true); - } - if (editorState.playing) setEditorState("previewTime", null); - } catch (error) { - console.error("Error handling play/pause:", error); - setEditorState("playing", false); - } - }; - - // Register keyboard shortcuts in one place - useEditorShortcuts(() => { - const el = document.activeElement; - if (!el) return true; - const tagName = el.tagName.toLowerCase(); - const isContentEditable = el.getAttribute("contenteditable") === "true"; - return !( - tagName === "input" || - tagName === "textarea" || - isContentEditable - ); - }, [ - { - combo: "S", - handler: () => - setEditorState( - "timeline", - "interactMode", - editorState.timeline.interactMode === "split" ? "seek" : "split" - ), - }, - { - combo: "Mod+=", - handler: () => - editorState.timeline.transform.updateZoom( - editorState.timeline.transform.zoom / 1.1, - editorState.playbackTime - ), - }, - { - combo: "Mod+-", - handler: () => - editorState.timeline.transform.updateZoom( - editorState.timeline.transform.zoom * 1.1, - editorState.playbackTime - ), - }, - { - combo: "Space", - handler: async () => { - const prevTime = editorState.previewTime; - - if (!editorState.playing) { - if (prevTime !== null) setEditorState("playbackTime", prevTime); - - await commands.seekTo(Math.floor(editorState.playbackTime * FPS)); - } - - await handlePlayPauseClick(); - }, - }, - ]); - - return ( -
-
- - cropDialogHandler()} - leftIcon={} - > - Crop - -
- -
-
-
-
- - - - - -
-
-
- - tooltipText="Toggle Split" - kbd={["S"]} - pressed={editorState.timeline.interactMode === "split"} - onChange={(v: boolean) => - setEditorState("timeline", "interactMode", v ? "split" : "seek") - } - as={KToggleButton} - variant="danger" - leftIcon={ - - } - /> -
- - { - editorState.timeline.transform.updateZoom( - editorState.timeline.transform.zoom * 1.1, - editorState.playbackTime - ); - }} - class="text-gray-12 size-5 will-change-[opacity] transition-opacity hover:opacity-70" - /> - - - { - editorState.timeline.transform.updateZoom( - editorState.timeline.transform.zoom / 1.1, - editorState.playbackTime - ); - }} - class="text-gray-12 size-5 will-change-[opacity] transition-opacity hover:opacity-70" - /> - - { - editorState.timeline.transform.updateZoom( - (1 - v) * zoomOutLimit(), - editorState.playbackTime - ); - }} - formatTooltip={() => - `${editorState.timeline.transform.zoom.toFixed( - 0 - )} seconds visible` - } - /> -
-
-
- ); + const { + project, + editorInstance, + setDialog, + totalDuration, + editorState, + setEditorState, + zoomOutLimit, + setProject, + } = useEditorContext(); + + // Load captions on mount + onMount(async () => { + if (editorInstance && editorInstance.path) { + // Still load captions into the store since they will be used by the GPU renderer + await captionsStore.loadCaptions(editorInstance.path); + + // Synchronize captions settings with project configuration + // This ensures the GPU renderer will receive the caption settings + if (editorInstance && project) { + const updatedProject = { ...project }; + + // Add captions data to project configuration if it doesn't exist + if ( + !updatedProject.captions && + captionsStore.state.segments.length > 0 + ) { + updatedProject.captions = { + segments: captionsStore.state.segments.map((segment) => ({ + id: segment.id, + start: segment.start, + end: segment.end, + text: segment.text, + })), + settings: { + enabled: captionsStore.state.settings.enabled, + font: captionsStore.state.settings.font, + size: captionsStore.state.settings.size, + color: captionsStore.state.settings.color, + backgroundColor: captionsStore.state.settings.backgroundColor, + backgroundOpacity: captionsStore.state.settings.backgroundOpacity, + position: captionsStore.state.settings.position, + bold: captionsStore.state.settings.bold, + italic: captionsStore.state.settings.italic, + outline: captionsStore.state.settings.outline, + outlineColor: captionsStore.state.settings.outlineColor, + exportWithSubtitles: + captionsStore.state.settings.exportWithSubtitles, + }, + }; + + // Update the project with captions data + setProject(updatedProject); + + // Save the updated project configuration + await commands.setProjectConfig(updatedProject); + } + } + } + }); + + // Continue to update current caption when playback time changes + // This is still needed for CaptionsTab to highlight the current caption + createEffect(() => { + const time = editorState.playbackTime; + // Only update captions if we have a valid time and segments exist + if ( + time !== undefined && + time >= 0 && + captionsStore.state.segments.length > 0 + ) { + captionsStore.updateCurrentCaption(time); + } + }); + + const [canvasContainerRef, setCanvasContainerRef] = + createSignal(); + const containerBounds = createElementBounds(canvasContainerRef); + + const isAtEnd = () => { + const total = totalDuration(); + return total > 0 && total - editorState.playbackTime <= 0.1; + }; + + const cropDialogHandler = () => { + const display = editorInstance.recordings.segments[0].display; + setDialog({ + open: true, + type: "crop", + position: { + ...(project.background.crop?.position ?? { x: 0, y: 0 }), + }, + size: { + ...(project.background.crop?.size ?? { + x: display.width, + y: display.height, + }), + }, + }); + }; + + createEffect(() => { + if (isAtEnd() && editorState.playing) { + commands.stopPlayback(); + setEditorState("playing", false); + } + }); + + const handlePlayPauseClick = async () => { + try { + if (isAtEnd()) { + await commands.stopPlayback(); + setEditorState("playbackTime", 0); + await commands.seekTo(0); + await commands.startPlayback(FPS, OUTPUT_SIZE); + setEditorState("playing", true); + } else if (editorState.playing) { + await commands.stopPlayback(); + setEditorState("playing", false); + } else { + // Ensure we seek to the current playback time before starting playback + await commands.seekTo(Math.floor(editorState.playbackTime * FPS)); + await commands.startPlayback(FPS, OUTPUT_SIZE); + setEditorState("playing", true); + } + if (editorState.playing) setEditorState("previewTime", null); + } catch (error) { + console.error("Error handling play/pause:", error); + setEditorState("playing", false); + } + }; + + // Register keyboard shortcuts in one place + useEditorShortcuts(() => { + const el = document.activeElement; + if (!el) return true; + const tagName = el.tagName.toLowerCase(); + const isContentEditable = el.getAttribute("contenteditable") === "true"; + return !( + tagName === "input" || + tagName === "textarea" || + isContentEditable + ); + }, [ + { + combo: "S", + handler: () => + setEditorState( + "timeline", + "interactMode", + editorState.timeline.interactMode === "split" ? "seek" : "split", + ), + }, + { + combo: "Mod+=", + handler: () => + editorState.timeline.transform.updateZoom( + editorState.timeline.transform.zoom / 1.1, + editorState.playbackTime, + ), + }, + { + combo: "Mod+-", + handler: () => + editorState.timeline.transform.updateZoom( + editorState.timeline.transform.zoom * 1.1, + editorState.playbackTime, + ), + }, + { + combo: "Space", + handler: async () => { + const prevTime = editorState.previewTime; + + if (!editorState.playing) { + if (prevTime !== null) setEditorState("playbackTime", prevTime); + + await commands.seekTo(Math.floor(editorState.playbackTime * FPS)); + } + + await handlePlayPauseClick(); + }, + }, + ]); + + return ( +
+
+ + cropDialogHandler()} + leftIcon={} + > + Crop + +
+ +
+
+
+
+ + + + + +
+
+
+ + tooltipText="Toggle Split" + kbd={["S"]} + pressed={editorState.timeline.interactMode === "split"} + onChange={(v: boolean) => + setEditorState("timeline", "interactMode", v ? "split" : "seek") + } + as={KToggleButton} + variant="danger" + leftIcon={ + + } + /> +
+ + { + editorState.timeline.transform.updateZoom( + editorState.timeline.transform.zoom * 1.1, + editorState.playbackTime, + ); + }} + class="text-gray-12 size-5 will-change-[opacity] transition-opacity hover:opacity-70" + /> + + + { + editorState.timeline.transform.updateZoom( + editorState.timeline.transform.zoom / 1.1, + editorState.playbackTime, + ); + }} + class="text-gray-12 size-5 will-change-[opacity] transition-opacity hover:opacity-70" + /> + + { + editorState.timeline.transform.updateZoom( + (1 - v) * zoomOutLimit(), + editorState.playbackTime, + ); + }} + formatTooltip={() => + `${editorState.timeline.transform.zoom.toFixed( + 0, + )} seconds visible` + } + /> +
+
+
+ ); } // CSS for checkerboard grid (adaptive to light/dark mode) const gridStyle = { - "background-image": - "linear-gradient(45deg, rgba(128,128,128,0.12) 25%, transparent 25%), " + - "linear-gradient(-45deg, rgba(128,128,128,0.12) 25%, transparent 25%), " + - "linear-gradient(45deg, transparent 75%, rgba(128,128,128,0.12) 75%), " + - "linear-gradient(-45deg, transparent 75%, rgba(128,128,128,0.12) 75%)", - "background-size": "40px 40px", - "background-position": "0 0, 0 20px, 20px -20px, -20px 0px", - "background-color": "rgba(200,200,200,0.08)", + "background-image": + "linear-gradient(45deg, rgba(128,128,128,0.12) 25%, transparent 25%), " + + "linear-gradient(-45deg, rgba(128,128,128,0.12) 25%, transparent 25%), " + + "linear-gradient(45deg, transparent 75%, rgba(128,128,128,0.12) 75%), " + + "linear-gradient(-45deg, transparent 75%, rgba(128,128,128,0.12) 75%)", + "background-size": "40px 40px", + "background-position": "0 0, 0 20px, 20px -20px, -20px 0px", + "background-color": "rgba(200,200,200,0.08)", }; function PreviewCanvas() { - const { latestFrame } = useEditorContext(); - - let canvasRef: HTMLCanvasElement | undefined; - - const [canvasContainerRef, setCanvasContainerRef] = - createSignal(); - const containerBounds = createElementBounds(canvasContainerRef); - - createEffect(() => { - const frame = latestFrame(); - if (!frame) return; - if (!canvasRef) return; - const ctx = canvasRef.getContext("2d"); - ctx?.putImageData(frame.data, 0, 0); - }); - - return ( -
- - {(currentFrame) => { - const padding = 4; - - const containerAspect = () => { - if (containerBounds.width && containerBounds.height) { - return ( - (containerBounds.width - padding * 2) / - (containerBounds.height - padding * 2) - ); - } - - return 1; - }; - - const frameAspect = () => - currentFrame().width / currentFrame().data.height; - - const size = () => { - if (frameAspect() < containerAspect()) { - const height = (containerBounds.height ?? 0) - padding * 1; - - return { - width: height * frameAspect(), - height, - }; - } - - const width = (containerBounds.width ?? 0) - padding * 2; - - return { - width, - height: width / frameAspect(), - }; - }; - - return ( -
- -
- ); - }} -
-
- ); + const { latestFrame } = useEditorContext(); + + let canvasRef: HTMLCanvasElement | undefined; + + const [canvasContainerRef, setCanvasContainerRef] = + createSignal(); + const containerBounds = createElementBounds(canvasContainerRef); + + createEffect(() => { + const frame = latestFrame(); + if (!frame) return; + if (!canvasRef) return; + const ctx = canvasRef.getContext("2d"); + ctx?.putImageData(frame.data, 0, 0); + }); + + return ( +
+ + {(currentFrame) => { + const padding = 4; + + const containerAspect = () => { + if (containerBounds.width && containerBounds.height) { + return ( + (containerBounds.width - padding * 2) / + (containerBounds.height - padding * 2) + ); + } + + return 1; + }; + + const frameAspect = () => + currentFrame().width / currentFrame().data.height; + + const size = () => { + if (frameAspect() < containerAspect()) { + const height = (containerBounds.height ?? 0) - padding * 1; + + return { + width: height * frameAspect(), + height, + }; + } + + const width = (containerBounds.width ?? 0) - padding * 2; + + return { + width, + height: width / frameAspect(), + }; + }; + + return ( +
+ +
+ ); + }} +
+
+ ); } function Time(props: { seconds: number; fps?: number; class?: string }) { - return ( - - {formatTime(props.seconds, props.fps ?? FPS)} - - ); + return ( + + {formatTime(props.seconds, props.fps ?? FPS)} + + ); }