stop via tray
This commit is contained in:
+48
-3
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
Vendored
+20
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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 |
@@ -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 };
|
||||
|
||||
Vendored
+2
@@ -36,5 +36,7 @@ interface Window {
|
||||
message?: string
|
||||
error?: string
|
||||
}>
|
||||
setRecordingState: (recording: boolean) => Promise<void>
|
||||
onStopRecordingFromTray: (callback: () => void) => () => void
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user