-
Notifications
You must be signed in to change notification settings - Fork 2.4k
feat: implement pause and resume recording #509
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { | |
| app, | ||
| BrowserWindow, | ||
| dialog, | ||
| globalShortcut, | ||
| ipcMain, | ||
| Menu, | ||
| nativeImage, | ||
|
|
@@ -217,35 +218,59 @@ function getTrayIcon(filename: string, size: number) { | |
| }); | ||
| } | ||
|
|
||
| function updateTrayMenu(recording: boolean = false) { | ||
| function updateTrayMenu(state: "recording" | "paused" | "stopped" = "stopped") { | ||
| if (!tray) return; | ||
| const trayIcon = recording ? recordingTrayIcon : defaultTrayIcon; | ||
| const trayToolTip = recording ? `Recording: ${selectedSourceName}` : "OpenScreen"; | ||
| const menuTemplate = recording | ||
| ? [ | ||
| { | ||
| label: mainT("common", "actions.stopRecording") || "Stop Recording", | ||
| click: () => { | ||
| if (mainWindow && !mainWindow.isDestroyed()) { | ||
| mainWindow.webContents.send("stop-recording-from-tray"); | ||
| } | ||
| }, | ||
|
|
||
| const isRecording = state === "recording"; | ||
| const isPaused = state === "paused"; | ||
| // TODO: Create an actual paused icon later, or use default/recording for now. Let's just use default or an indicator? | ||
| // Using default tray icon for paused visually differentiates it from the red recording dot | ||
| const trayIcon = isRecording ? recordingTrayIcon : defaultTrayIcon; | ||
|
|
||
| let trayToolTip = "OpenScreen"; | ||
| if (isRecording) trayToolTip = `Recording: ${selectedSourceName}`; | ||
| if (isPaused) trayToolTip = `Paused: ${selectedSourceName}`; | ||
|
|
||
| let menuTemplate: Parameters<typeof Menu.buildFromTemplate>[0] = []; | ||
|
|
||
| if (isRecording || isPaused) { | ||
| menuTemplate = [ | ||
| { | ||
| label: isPaused | ||
| ? mainT("common", "actions.resumeRecording") || "Resume Recording" | ||
| : mainT("common", "actions.pauseRecording") || "Pause Recording", | ||
| click: () => { | ||
| if (mainWindow && !mainWindow.isDestroyed()) { | ||
| mainWindow.webContents.send("toggle-pause-recording-from-tray"); | ||
| } | ||
| }, | ||
| ] | ||
| : [ | ||
| { | ||
| label: mainT("common", "actions.open") || "Open", | ||
| click: () => { | ||
| showMainWindow(); | ||
| }, | ||
| }, | ||
| { | ||
| label: mainT("common", "actions.stopRecording") || "Stop Recording", | ||
| click: () => { | ||
| if (mainWindow && !mainWindow.isDestroyed()) { | ||
| mainWindow.webContents.send("stop-recording-from-tray"); | ||
| } | ||
| }, | ||
| { | ||
| label: mainT("common", "actions.quit") || "Quit", | ||
| click: () => { | ||
| app.quit(); | ||
| }, | ||
| }, | ||
| ]; | ||
| } else { | ||
| menuTemplate = [ | ||
| { | ||
| label: mainT("common", "actions.open") || "Open", | ||
| click: () => { | ||
| showMainWindow(); | ||
| }, | ||
| }, | ||
| { | ||
| label: mainT("common", "actions.quit") || "Quit", | ||
| click: () => { | ||
| app.quit(); | ||
| }, | ||
| ]; | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| tray.setImage(trayIcon); | ||
| tray.setToolTip(trayToolTip); | ||
| tray.setContextMenu(Menu.buildFromTemplate(menuTemplate)); | ||
|
|
@@ -410,18 +435,64 @@ app.whenReady().then(async () => { | |
| showMainWindow(); | ||
| } | ||
|
|
||
| async function updateGlobalShortcuts() { | ||
| globalShortcut.unregisterAll(); | ||
| let config: any = {}; | ||
| try { | ||
| const data = await fs.readFile(path.join(app.getPath("userData"), "shortcuts.json"), "utf-8"); | ||
| config = JSON.parse(data); | ||
| } catch {} | ||
|
|
||
| const binding = config.togglePauseRecording || { key: "p", ctrl: true, alt: true }; | ||
| const parts = []; | ||
| if (binding.ctrl) parts.push("CommandOrControl"); | ||
| if (binding.alt) parts.push("Alt"); | ||
| if (binding.shift) parts.push("Shift"); | ||
| const key = binding.key && binding.key.length === 1 ? binding.key.toUpperCase() : binding.key; | ||
| if (key) { | ||
| parts.push(key); | ||
| const accelerator = parts.join("+"); | ||
| try { | ||
| globalShortcut.register(accelerator, () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| if (mainWindow && !mainWindow.isDestroyed()) { | ||
| mainWindow.webContents.send("toggle-pause-recording-from-tray"); | ||
| } | ||
| }); | ||
| } catch (e) { | ||
| console.error("Failed to register global shortcut:", accelerator, e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| await updateGlobalShortcuts(); | ||
|
|
||
| ipcMain.handle("save-shortcuts", async (_, shortcuts: unknown) => { | ||
| try { | ||
| await fs.writeFile( | ||
| path.join(app.getPath("userData"), "shortcuts.json"), | ||
| JSON.stringify(shortcuts, null, 2), | ||
| "utf-8", | ||
| ); | ||
| await updateGlobalShortcuts(); | ||
| return { success: true }; | ||
| } catch (error) { | ||
| console.error("Failed to save shortcuts:", error); | ||
| return { success: false, error: String(error) }; | ||
| } | ||
| }); | ||
|
|
||
| registerIpcHandlers( | ||
| createEditorWindowWrapper, | ||
| createSourceSelectorWindowWrapper, | ||
| createCountdownOverlayWindowWrapper, | ||
| () => mainWindow, | ||
| () => sourceSelectorWindow, | ||
| () => countdownOverlayWindow, | ||
| (recording: boolean, sourceName: string) => { | ||
| (state: "recording" | "paused" | "stopped", sourceName: string) => { | ||
| selectedSourceName = sourceName; | ||
| if (!tray) createTray(); | ||
| updateTrayMenu(recording); | ||
| if (!recording) { | ||
| updateTrayMenu(state); | ||
| if (state === "stopped") { | ||
| showMainWindow(); | ||
| } | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -148,6 +148,9 @@ export function createCursorTelemetryBuffer( | |
|
|
||
| return { | ||
| startSession(recordingId) { | ||
| if (activeRecordingId === recordingId) { | ||
| return; | ||
| } | ||
|
Comment on lines
+151
to
+153
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 151 makes same-id calls a no-op (nice), but the nit: cleaner doc patch- * Begin a new recording session under the given `recordingId`. Clears
- * any in-progress active samples (without touching already-completed
- * pending batches). Safe to call repeatedly — e.g. a rapid Stop →
- * Record sequence — and the most recent id wins.
+ * Begin a new recording session under the given `recordingId`.
+ * If called with the same `recordingId` as the current active session,
+ * this is a no-op (used by pause/resume flows). If called with a different
+ * id, any in-progress active samples are cleared (without touching
+ * already-completed pending batches).🤖 Prompt for AI Agents |
||
| active = []; | ||
| activeRecordingId = recordingId; | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid calling
startSession()on every transition to"recording": this path now runs both at initial start and after pause, andstartSession()clears the active sample buffer. In a pause→resume flow, all cursor telemetry captured before the pause is dropped, so the final overlay only contains post-resume movement. This breaks cursor/video sync for any recording that is paused at least once.Useful? React with 👍 / 👎.