-
-
-
{
- 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)}
+
+ );
}