stop via tray

This commit is contained in:
Siddharth
2025-10-17 20:05:17 -07:00
parent ec37cd7f11
commit c3eb97116a
9 changed files with 164 additions and 19 deletions
+48 -3
View File
@@ -1,4 +1,4 @@
import { BrowserWindow, screen, ipcMain, desktopCapturer, app } from "electron";
import { BrowserWindow, screen, ipcMain, desktopCapturer, app, nativeImage, Tray, Menu } from "electron";
import { fileURLToPath } from "node:url";
import path from "node:path";
import fs from "node:fs/promises";
@@ -213,7 +213,7 @@ function cleanupMouseTracking() {
}
}
let selectedSource = null;
function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, getMainWindow, getSourceSelectorWindow) {
function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, getMainWindow, getSourceSelectorWindow, onRecordingStateChange) {
ipcMain.handle("get-sources", async (_, opts) => {
const sources = await desktopCapturer.getSources(opts);
return sources.map((source) => ({
@@ -312,6 +312,12 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g
return { success: false, message: "Failed to get video path", error: String(error) };
}
});
ipcMain.handle("set-recording-state", (_, recording) => {
const source = selectedSource || { name: "Screen" };
if (onRecordingStateChange) {
onRecordingStateChange(recording, source.name);
}
});
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings");
@@ -347,9 +353,34 @@ const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist");
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, "public") : RENDERER_DIST;
let mainWindow = null;
let sourceSelectorWindow = null;
let tray = null;
let selectedSourceName = "";
function createWindow() {
mainWindow = createHudOverlayWindow();
}
function createTray() {
const iconPath = path.join(process.env.VITE_PUBLIC || RENDERER_DIST, "rec-button.png");
let icon = nativeImage.createFromPath(iconPath);
icon = icon.resize({ width: 24, height: 24, quality: "best" });
tray = new Tray(icon);
updateTrayMenu();
}
function updateTrayMenu() {
if (!tray) return;
const menuTemplate = [
{
label: "Stop Recording",
click: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("stop-recording-from-tray");
}
}
}
];
const contextMenu = Menu.buildFromTemplate(menuTemplate);
tray.setContextMenu(contextMenu);
tray.setToolTip(`Recording: ${selectedSourceName}`);
}
function createEditorWindowWrapper() {
if (mainWindow) {
mainWindow.close();
@@ -388,7 +419,21 @@ app.whenReady().then(async () => {
createEditorWindowWrapper,
createSourceSelectorWindowWrapper,
() => mainWindow,
() => sourceSelectorWindow
() => sourceSelectorWindow,
(recording, sourceName) => {
selectedSourceName = sourceName;
if (recording) {
if (!tray) createTray();
updateTrayMenu();
if (mainWindow) mainWindow.minimize();
} else {
if (tray) {
tray.destroy();
tray = null;
}
if (mainWindow) mainWindow.restore();
}
}
);
createWindow();
});
+8
View File
@@ -30,5 +30,13 @@ electron.contextBridge.exposeInMainWorld("electronAPI", {
},
getRecordedVideoPath: () => {
return electron.ipcRenderer.invoke("get-recorded-video-path");
},
setRecordingState: (recording) => {
return electron.ipcRenderer.invoke("set-recording-state", recording);
},
onStopRecordingFromTray: (callback) => {
const listener = () => callback();
electron.ipcRenderer.on("stop-recording-from-tray", listener);
return () => electron.ipcRenderer.removeListener("stop-recording-from-tray", listener);
}
});
+20 -1
View File
@@ -24,6 +24,25 @@ declare namespace NodeJS {
// Used in Renderer process, expose in `preload.ts`
interface Window {
electronAPI: {
getSources: (opts: Electron.SourcesOptions) => Promise<Electron.DesktopCapturerSource[]>
getSources: (opts: Electron.SourcesOptions) => Promise<ProcessedDesktopSource[]>
switchToEditor: () => Promise<void>
openSourceSelector: () => Promise<void>
selectSource: (source: any) => Promise<any>
getSelectedSource: () => Promise<any>
startMouseTracking: () => Promise<void>
stopMouseTracking: () => Promise<void>
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }>
storeMouseTrackingData: (fileName: string) => Promise<{ success: boolean; path?: string; eventCount?: number; message?: string }>
getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }>
setRecordingState: (recording: boolean) => Promise<void>
onStopRecordingFromTray: (callback: () => void) => () => void
}
}
interface ProcessedDesktopSource {
id: string
name: string
display_id: string
thumbnail: string | null
appIcon: string | null
}
+9 -1
View File
@@ -10,7 +10,8 @@ export function registerIpcHandlers(
createEditorWindow: () => void,
createSourceSelectorWindow: () => BrowserWindow,
getMainWindow: () => BrowserWindow | null,
getSourceSelectorWindow: () => BrowserWindow | null
getSourceSelectorWindow: () => BrowserWindow | null,
onRecordingStateChange?: (recording: boolean, sourceName: string) => void
) {
ipcMain.handle('get-sources', async (_, opts) => {
const sources = await desktopCapturer.getSources(opts)
@@ -126,4 +127,11 @@ export function registerIpcHandlers(
return { success: false, message: 'Failed to get video path', error: String(error) }
}
})
ipcMain.handle('set-recording-state', (_, recording: boolean) => {
const source = selectedSource || { name: 'Screen' }
if (onRecordingStateChange) {
onRecordingStateChange(recording, source.name)
}
})
}
+45 -2
View File
@@ -1,4 +1,4 @@
import { app, BrowserWindow } from 'electron'
import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import fs from 'node:fs/promises'
@@ -61,11 +61,39 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT,
// Window references
let mainWindow: BrowserWindow | null = null
let sourceSelectorWindow: BrowserWindow | null = null
let tray: Tray | null = null
let isRecording = false
let selectedSourceName = ''
function createWindow() {
mainWindow = createHudOverlayWindow()
}
function createTray() {
const iconPath = path.join(process.env.VITE_PUBLIC || RENDERER_DIST, 'rec-button.png');
let icon = nativeImage.createFromPath(iconPath);
icon = icon.resize({ width: 24, height: 24, quality: 'best' });
tray = new Tray(icon);
updateTrayMenu();
}
function updateTrayMenu() {
if (!tray) return;
const menuTemplate = [
{
label: 'Stop Recording',
click: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('stop-recording-from-tray');
}
}
}
];
const contextMenu = Menu.buildFromTemplate(menuTemplate);
tray.setContextMenu(contextMenu);
tray.setToolTip(`Recording: ${selectedSourceName}`);
}
function createEditorWindowWrapper() {
if (mainWindow) {
mainWindow.close()
@@ -118,7 +146,22 @@ app.whenReady().then(async () => {
createEditorWindowWrapper,
createSourceSelectorWindowWrapper,
() => mainWindow,
() => sourceSelectorWindow
() => sourceSelectorWindow,
(recording: boolean, sourceName: string) => {
isRecording = recording
selectedSourceName = sourceName
if (recording) {
if (!tray) createTray();
updateTrayMenu();
if (mainWindow) mainWindow.minimize();
} else {
if (tray) {
tray.destroy();
tray = null;
}
if (mainWindow) mainWindow.restore();
}
}
)
createWindow()
})
+8
View File
@@ -30,5 +30,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
getRecordedVideoPath: () => {
return ipcRenderer.invoke('get-recorded-video-path')
},
setRecordingState: (recording: boolean) => {
return ipcRenderer.invoke('set-recording-state', recording)
},
onStopRecordingFromTray: (callback: () => void) => {
const listener = () => callback()
ipcRenderer.on('stop-recording-from-tray', listener)
return () => ipcRenderer.removeListener('stop-recording-from-tray', listener)
}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

+24 -12
View File
@@ -13,8 +13,30 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const chunks = useRef<Blob[]>([]);
const startTime = useRef<number>(0);
const stopRecording = useRef(() => {
if (mediaRecorder.current?.state === "recording") {
if (stream.current) {
stream.current.getTracks().forEach(track => track.stop());
}
mediaRecorder.current.stop();
setRecording(false);
window.electronAPI.stopMouseTracking();
window.electronAPI?.setRecordingState(false);
}
});
useEffect(() => {
let cleanup: (() => void) | undefined;
if (window.electronAPI?.onStopRecordingFromTray) {
cleanup = window.electronAPI.onStopRecordingFromTray(() => {
stopRecording.current();
});
}
return () => {
if (cleanup) cleanup();
if (mediaRecorder.current?.state === "recording") {
mediaRecorder.current.stop();
}
@@ -91,6 +113,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
recorder.start(1000);
startTime.current = Date.now();
setRecording(true);
window.electronAPI?.setRecordingState(true);
} catch (error) {
console.error('Failed to start recording:', error);
setRecording(false);
@@ -101,19 +124,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
};
const stopRecording = () => {
if (mediaRecorder.current?.state === "recording") {
if (stream.current) {
stream.current.getTracks().forEach(track => track.stop());
}
mediaRecorder.current.stop();
setRecording(false);
window.electronAPI.stopMouseTracking();
}
};
const toggleRecording = () => {
recording ? stopRecording() : startRecording();
recording ? stopRecording.current() : startRecording();
};
return { recording, toggleRecording };
+2
View File
@@ -36,5 +36,7 @@ interface Window {
message?: string
error?: string
}>
setRecordingState: (recording: boolean) => Promise<void>
onStopRecordingFromTray: (callback: () => void) => () => void
}
}