Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
70a05d5
feat(recording): add manual zoom markers helper and types
muhammad-a-dev Sep 24, 2026
9366132
feat(recording): add fresh recording manual zoom hook
muhammad-a-dev Sep 24, 2026
b8b7881
feat(recording): add manual-zoom to CursorInteractionType
muhammad-a-dev Sep 24, 2026
d57bf14
feat(recording): add manual zoom pending refs to editor UI state
muhammad-a-dev Sep 24, 2026
373fd82
feat(recording): restore editor UI state with manual zoom refs
muhammad-a-dev Sep 24, 2026
58430d4
test
muhammad-a-dev Sep 24, 2026
663cc59
feat(recording): restore project open actions with manual zoom pendin…
muhammad-a-dev Sep 24, 2026
7d153be
feat(recording): restore project open actions with manual zoom pendin…
muhammad-a-dev Sep 24, 2026
3d00b6f
feat(recording): capture manual zoom markers in cursor telemetry
muhammad-a-dev Sep 24, 2026
e6f6a97
feat(recording): restore cursor telemetry with manual zoom capture
muhammad-a-dev Sep 24, 2026
fcd5302
feat(recording): add manual-zoom to ipc CursorInteractionType
muhammad-a-dev Sep 24, 2026
6afd0bc
feat(recording): add pending manual zoom refs to editor UI state
muhammad-a-dev Sep 24, 2026
a89ed22
feat(recording): set pending manual zoom path when importing media
muhammad-a-dev Sep 24, 2026
852a50f
feat(recording): add CommandOrControl+Alt+Z zoom marker shortcut helper
muhammad-a-dev Sep 24, 2026
1c254d7
chore: size probe (will replace)
muhammad-a-dev Sep 24, 2026
a394e60
feat(recording): register zoom shortcut from IPC recording state changes
muhammad-a-dev Sep 24, 2026
db691b6
feat(recording): add manual zoom region builder and fresh-recording hook
muhammad-a-dev Sep 24, 2026
5b201ef
feat(recording): call manual zoom hook from timeline editing controller
muhammad-a-dev Sep 24, 2026
10740e1
chore: remove size probe file
muhammad-a-dev Sep 24, 2026
98611fa
feat(recording): queue manual zoom path when opening a fresh recording
muhammad-a-dev Sep 24, 2026
643151f
feat(recording): thread manual zoom refs through project controller
muhammad-a-dev Sep 24, 2026
d968fee
feat(recording): wire manual zoom refs in VideoEditor
muhammad-a-dev Sep 24, 2026
96d148f
feat(recording): reset manual zoom refs on project lifecycle events
muhammad-a-dev Sep 24, 2026
3c01089
feat(recording): add manual-zoom to video editor CursorInteractionType
muhammad-a-dev Sep 24, 2026
11cd958
feat(recording): extract CursorTelemetryPoint with manual-zoom type
muhammad-a-dev Sep 24, 2026
e983080
revert: remove split CursorTelemetryPoint file pending electron-env u…
muhammad-a-dev Sep 24, 2026
a0049c8
chore: size probe small first
muhammad-a-dev Sep 24, 2026
8b7974b
chore: remove size probe file
muhammad-a-dev Sep 24, 2026
cbaae96
fix(recording): keep manual-zoom markers and allow close shortcut pre…
muhammad-a-dev Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions electron/ipc/cursor/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export function normalizeCursorTelemetrySamples(rawSamples: unknown): CursorTele
point.interactionType === "right-click" ||
point.interactionType === "middle-click" ||
point.interactionType === "move" ||
point.interactionType === "mouseup"
point.interactionType === "mouseup" ||
point.interactionType === "manual-zoom"
? point.interactionType
: undefined,
cursorType:
Expand Down Expand Up @@ -253,7 +254,12 @@ export function pushCursorSample(
} as CursorTelemetryPoint);

if (activeCursorSamples.length > MAX_CURSOR_SAMPLES) {
activeCursorSamples.shift();
const oldestNonMarkerIndex = activeCursorSamples.findIndex(
(sample) => sample.interactionType !== "manual-zoom",
);
if (oldestNonMarkerIndex >= 0) {
activeCursorSamples.splice(oldestNonMarkerIndex, 1);
}
}
}

Expand All @@ -262,6 +268,21 @@ export function sampleCursorPoint() {
pushCursorSample(point.cx, point.cy, getCursorCaptureElapsedMs(), "move");
}

export function captureManualZoomMarker() {
if (!isCursorCaptureActive) {
return false;
}

const point = getNormalizedCursorPoint();
pushCursorSample(
point.cx,
point.cy,
getCursorCaptureElapsedMs(),
"manual-zoom",
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return true;
}

export async function persistPendingCursorTelemetry(videoPath: string) {
const telemetryPath = getTelemetryPathForVideo(videoPath);
if (pendingCursorSamples.length > 0) {
Expand Down
13 changes: 12 additions & 1 deletion electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { BrowserWindow } from "electron";
import {
registerRecordingZoomShortcut,
unregisterRecordingZoomShortcut,
} from "../recordingZoomShortcut";
import { registerAnnouncementHandlers } from "./register/announcements";
import { registerAssetHandlers } from "./register/assets";
import { registerCaptionHandlers } from "./register/captions";
Expand Down Expand Up @@ -64,7 +68,14 @@ export function registerIpcHandlers(
createSourceSelectorWindow,
getSourceSelectorWindow,
});
registerRecordingHandlers(onRecordingStateChange);
registerRecordingHandlers((recording, sourceName) => {
if (recording) {
registerRecordingZoomShortcut();
} else {
unregisterRecordingZoomShortcut();
}
onRecordingStateChange?.(recording, sourceName);
});
registerPermissionHandlers();
registerAnnouncementHandlers();
registerAssetHandlers();
Expand Down
1 change: 1 addition & 0 deletions electron/ipc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export type CursorInteractionType =
| "double-click"
| "right-click"
| "middle-click"
| "manual-zoom"
| "mouseup";

export interface CursorTelemetryPoint {
Expand Down
59 changes: 59 additions & 0 deletions electron/recordingZoomShortcut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { app, globalShortcut } from "electron";
import { captureManualZoomMarker } from "./ipc/cursor/telemetry";

const RECORDING_ZOOM_SHORTCUT = "CommandOrControl+Alt+Z";
const RECORDING_ZOOM_SHORTCUT_THROTTLE_MS = 500;

let recordingZoomShortcutRegistered = false;
let lastRecordingZoomShortcutAtMs = 0;
let beforeQuitHookInstalled = false;

function addRecordingZoomMarkerFromShortcut() {
const now = Date.now();
if (now - lastRecordingZoomShortcutAtMs < RECORDING_ZOOM_SHORTCUT_THROTTLE_MS) {
return;
}

lastRecordingZoomShortcutAtMs = now;
if (captureManualZoomMarker()) {
console.log(`[recording-zoom] Added zoom marker via ${RECORDING_ZOOM_SHORTCUT}`);
}
}

function ensureBeforeQuitHook() {
if (beforeQuitHookInstalled) {
return;
}
beforeQuitHookInstalled = true;
app.on("before-quit", () => {
unregisterRecordingZoomShortcut();
});
}

export function registerRecordingZoomShortcut() {
ensureBeforeQuitHook();
if (recordingZoomShortcutRegistered) {
return;
}

recordingZoomShortcutRegistered = globalShortcut.register(
RECORDING_ZOOM_SHORTCUT,
addRecordingZoomMarkerFromShortcut,
);

if (!recordingZoomShortcutRegistered) {
console.warn(
`[recording-zoom] Failed to register ${RECORDING_ZOOM_SHORTCUT}; another app may already be using it.`,
);
}
}

export function unregisterRecordingZoomShortcut() {
if (!recordingZoomShortcutRegistered) {
return;
}

globalShortcut.unregister(RECORDING_ZOOM_SHORTCUT);
recordingZoomShortcutRegistered = false;
lastRecordingZoomShortcutAtMs = 0;
}
7 changes: 7 additions & 0 deletions src/components/video-editor/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export default function VideoEditor() {
nextAnnotationZIndexRef,
autoSuggestedVideoPathRef,
pendingFreshRecordingAutoZoomPathRef,
pendingFreshRecordingManualZoomPathRef,
manualRecordingZoomsAppliedVideoPathRef,
pendingFreshRecordingAutoSuggestTimeoutRef,
pendingFreshRecordingAutoSuggestTelemetryCountRef,
timelineRef,
Expand Down Expand Up @@ -151,6 +153,7 @@ export default function VideoEditor() {

useEffect(() => {
autoSuggestedVideoPathRef.current = null;
manualRecordingZoomsAppliedVideoPathRef.current = null;
pendingFreshRecordingAutoSuggestTelemetryCountRef.current = 0;
if (pendingFreshRecordingAutoSuggestTimeoutRef.current !== null) {
window.clearTimeout(pendingFreshRecordingAutoSuggestTimeoutRef.current);
Expand Down Expand Up @@ -260,6 +263,8 @@ export default function VideoEditor() {
autoFullTrackClipIdRef,
autoFullTrackClipEndMsRef,
pendingFreshRecordingAutoZoomPathRef,
pendingFreshRecordingManualZoomPathRef,
manualRecordingZoomsAppliedVideoPathRef,
pendingFreshRecordingAutoSuggestTelemetryCountRef,
autoSuggestedVideoPathRef,
applySessionPresentation,
Expand Down Expand Up @@ -304,6 +309,8 @@ export default function VideoEditor() {
autoFullTrackClipEndMsRef,
autoSuggestedVideoPathRef,
pendingFreshRecordingAutoZoomPathRef,
pendingFreshRecordingManualZoomPathRef,
manualRecordingZoomsAppliedVideoPathRef,
pendingFreshRecordingAutoSuggestTimeoutRef,
pendingFreshRecordingAutoSuggestTelemetryCountRef,
handleUndo,
Expand Down
99 changes: 99 additions & 0 deletions src/components/video-editor/hooks/useFreshRecordingManualZoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
type Dispatch,
type MutableRefObject,
type SetStateAction,
useEffect,
} from "react";
import { buildManualRecordingZoomRegions } from "../timeline/recordingZoomMarkers";
import {
clampFocusToDepth,
DEFAULT_ZOOM_DEPTH,
type CursorTelemetryPoint,
type ZoomRegion,
} from "../types";

interface UseFreshRecordingManualZoomParams {
videoPath: string | null;
loading: boolean;
duration: number;
normalizedCursorTelemetry: CursorTelemetryPoint[];
zoomRegions: ZoomRegion[];
setZoomRegions: Dispatch<SetStateAction<ZoomRegion[]>>;
nextZoomIdRef: MutableRefObject<number>;
autoSuggestedVideoPathRef: MutableRefObject<string | null>;
pendingFreshRecordingAutoZoomPathRef: MutableRefObject<string | null>;
pendingFreshRecordingManualZoomPathRef: MutableRefObject<string | null>;
manualRecordingZoomsAppliedVideoPathRef: MutableRefObject<string | null>;
}

export function useFreshRecordingManualZoom({
videoPath,
loading,
duration,
normalizedCursorTelemetry,
zoomRegions,
setZoomRegions,
nextZoomIdRef,
autoSuggestedVideoPathRef,
pendingFreshRecordingAutoZoomPathRef,
pendingFreshRecordingManualZoomPathRef,
manualRecordingZoomsAppliedVideoPathRef,
}: UseFreshRecordingManualZoomParams) {
useEffect(() => {
if (!videoPath || loading || duration <= 0 || normalizedCursorTelemetry.length === 0) {
return;
}

if (pendingFreshRecordingManualZoomPathRef.current !== videoPath) {
return;
}

if (manualRecordingZoomsAppliedVideoPathRef.current === videoPath) {
return;
}

const totalMs = Math.round(duration * 1000);
const manualRegions = buildManualRecordingZoomRegions({
cursorTelemetry: normalizedCursorTelemetry,
totalMs,
defaultDurationMs: Math.min(1000, totalMs),
reservedSpans: zoomRegions.map((region) => ({
start: region.startMs,
end: region.endMs,
})),
});

manualRecordingZoomsAppliedVideoPathRef.current = videoPath;
pendingFreshRecordingManualZoomPathRef.current = null;

if (manualRegions.length === 0) {
return;
}

setZoomRegions((previous) => [
...previous,
...manualRegions.map((region) => ({
id: `zoom-${nextZoomIdRef.current++}`,
startMs: region.start,
endMs: region.end,
depth: DEFAULT_ZOOM_DEPTH,
focus: clampFocusToDepth(region.focus, DEFAULT_ZOOM_DEPTH),
mode: "manual" as const,
})),
]);
autoSuggestedVideoPathRef.current = videoPath;
pendingFreshRecordingAutoZoomPathRef.current = null;
}, [
videoPath,
loading,
duration,
normalizedCursorTelemetry,
zoomRegions,
setZoomRegions,
nextZoomIdRef,
autoSuggestedVideoPathRef,
pendingFreshRecordingAutoZoomPathRef,
pendingFreshRecordingManualZoomPathRef,
manualRecordingZoomsAppliedVideoPathRef,
]);
}
16 changes: 16 additions & 0 deletions src/components/video-editor/hooks/useTimelineEditingController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useCursorTelemetry } from "./useCursorTelemetry";
import { useEditorGlobalInteractions } from "./useEditorGlobalInteractions";
import { useEditorPlaybackControls } from "./useEditorPlaybackControls";
import { useFreshRecordingAutoZoom } from "./useFreshRecordingAutoZoom";
import { useFreshRecordingManualZoom } from "./useFreshRecordingManualZoom";
import { useTimelineProjection } from "./useTimelineProjection";
import { useZoomRegionCommands } from "./useZoomRegionCommands";

Expand Down Expand Up @@ -51,6 +52,8 @@ type Input = {
autoFullTrackClipEndMsRef: MutableRefObject<number | null>;
autoSuggestedVideoPathRef: MutableRefObject<string | null>;
pendingFreshRecordingAutoZoomPathRef: MutableRefObject<string | null>;
pendingFreshRecordingManualZoomPathRef: MutableRefObject<string | null>;
manualRecordingZoomsAppliedVideoPathRef: MutableRefObject<string | null>;
pendingFreshRecordingAutoSuggestTimeoutRef: MutableRefObject<number | null>;
pendingFreshRecordingAutoSuggestTelemetryCountRef: MutableRefObject<number>;
handleUndo: () => void;
Expand Down Expand Up @@ -155,6 +158,19 @@ export function useTimelineEditingController(input: Input) {
timeline.setSelectedCaptionId,
],
);
useFreshRecordingManualZoom({
videoPath: input.videoPath,
loading: input.loading,
duration: input.duration,
normalizedCursorTelemetry: cursor.normalizedCursorTelemetry,
zoomRegions: timeline.zoomRegions,
setZoomRegions: timeline.setZoomRegions,
nextZoomIdRef: input.nextZoomIdRef,
autoSuggestedVideoPathRef: input.autoSuggestedVideoPathRef,
pendingFreshRecordingAutoZoomPathRef: input.pendingFreshRecordingAutoZoomPathRef,
pendingFreshRecordingManualZoomPathRef: input.pendingFreshRecordingManualZoomPathRef,
manualRecordingZoomsAppliedVideoPathRef: input.manualRecordingZoomsAppliedVideoPathRef,
});
const freshZoom = useFreshRecordingAutoZoom({
appPlatform: input.appPlatform,
videoPath: input.videoPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ type Input = {
autoFullTrackClipIdRef: MutableRefObject<string | null>;
autoFullTrackClipEndMsRef: MutableRefObject<number | null>;
pendingFreshRecordingAutoZoomPathRef: MutableRefObject<string | null>;
pendingFreshRecordingManualZoomPathRef: MutableRefObject<string | null>;
manualRecordingZoomsAppliedVideoPathRef: MutableRefObject<string | null>;
pendingFreshRecordingAutoSuggestTelemetryCountRef: MutableRefObject<number>;
autoSuggestedVideoPathRef: MutableRefObject<string | null>;
applySessionPresentation: (
Expand Down Expand Up @@ -126,6 +128,8 @@ export function useEditorProjectController(input: Input) {
autoFullTrackClipIdRef: input.autoFullTrackClipIdRef,
autoFullTrackClipEndMsRef: input.autoFullTrackClipEndMsRef,
pendingFreshRecordingAutoZoomPathRef: input.pendingFreshRecordingAutoZoomPathRef,
pendingFreshRecordingManualZoomPathRef: input.pendingFreshRecordingManualZoomPathRef,
manualRecordingZoomsAppliedVideoPathRef: input.manualRecordingZoomsAppliedVideoPathRef,
pendingFreshRecordingAutoSuggestTelemetryCountRef:
input.pendingFreshRecordingAutoSuggestTelemetryCountRef,
autoSuggestedVideoPathRef: input.autoSuggestedVideoPathRef,
Expand All @@ -148,6 +152,7 @@ export function useEditorProjectController(input: Input) {
devConfig: input.devConfig,
videoSourcePath: input.videoSourcePath,
pendingFreshRecordingAutoZoomPathRef: input.pendingFreshRecordingAutoZoomPathRef,
pendingFreshRecordingManualZoomPathRef: input.pendingFreshRecordingManualZoomPathRef,
applyLoadedProject: lifecycle.applyLoadedProject,
resetSourceScopedEditorState: lifecycle.resetSourceScopedEditorState,
applySessionPresentation: input.applySessionPresentation,
Expand Down Expand Up @@ -202,6 +207,7 @@ export function useEditorProjectController(input: Input) {
appearance: input.appearance,
videoPlaybackRef: input.videoPlaybackRef,
pendingFreshRecordingAutoZoomPathRef: input.pendingFreshRecordingAutoZoomPathRef,
pendingFreshRecordingManualZoomPathRef: input.pendingFreshRecordingManualZoomPathRef,
hasUnsavedChanges,
setIsPlaying: input.setIsPlaying,
setCurrentTime: input.setCurrentTime,
Expand Down
Loading