diff --git a/dist-electron/main.js b/dist-electron/main.js index 2d1e494..d3dd1ef 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,8 +1,8 @@ import { BrowserWindow, screen, ipcMain, desktopCapturer, app } from "electron"; import { fileURLToPath } from "node:url"; import path from "node:path"; -import { uIOhook } from "uiohook-napi"; import fs from "node:fs/promises"; +import { uIOhook } from "uiohook-napi"; const __dirname$1 = path.dirname(fileURLToPath(import.meta.url)); const APP_ROOT = path.join(__dirname$1, ".."); const VITE_DEV_SERVER_URL$1 = process.env["VITE_DEV_SERVER_URL"]; @@ -54,7 +54,8 @@ function createEditorWindow() { webPreferences: { preload: path.join(__dirname$1, "preload.mjs"), nodeIntegration: false, - contextIsolation: true + contextIsolation: true, + webSecurity: false } }); win.webContents.on("did-finish-load", () => { @@ -214,14 +215,13 @@ let selectedSource = null; function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, getMainWindow, getSourceSelectorWindow) { ipcMain.handle("get-sources", async (_, opts) => { const sources = await desktopCapturer.getSources(opts); - const processedSources = sources.map((source) => ({ + return sources.map((source) => ({ id: source.id, name: source.name, display_id: source.display_id, thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, appIcon: source.appIcon ? source.appIcon.toDataURL() : null })); - return processedSources; }); ipcMain.handle("select-source", (_, source) => { selectedSource = source; @@ -255,31 +255,90 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g ipcMain.handle("stop-mouse-tracking", () => { return stopMouseTracking(); }); - ipcMain.handle("save-mouse-tracking-data", async (_, videoFileName) => { + ipcMain.handle("store-recorded-video", async (_, videoData, fileName) => { + try { + const videoPath = path.join(RECORDINGS_DIR, fileName); + await fs.writeFile(videoPath, Buffer.from(videoData)); + return { + success: true, + path: videoPath, + message: "Video stored successfully" + }; + } catch (error) { + console.error("Failed to store video:", error); + return { + success: false, + message: "Failed to store video", + error: String(error) + }; + } + }); + ipcMain.handle("store-mouse-tracking-data", async (_, fileName) => { try { const data = getTrackingData(); if (data.length === 0) { return { success: false, message: "No tracking data to save" }; } - const jsonFileName = videoFileName.replace(".webm", "_tracking.json"); - const filePath = path.join(process.env.HOME || "", "Downloads", jsonFileName); - await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8"); + const trackingPath = path.join(RECORDINGS_DIR, fileName); + await fs.writeFile(trackingPath, JSON.stringify(data, null, 2), "utf-8"); return { success: true, - message: "Tracking data saved", - filePath, - eventCount: data.length + path: trackingPath, + eventCount: data.length, + message: "Mouse tracking data stored successfully" }; } catch (error) { + console.error("Failed to store mouse tracking data:", error); return { success: false, - message: "Failed to save tracking data", + message: "Failed to store mouse tracking data", error: String(error) }; } }); + ipcMain.handle("get-recorded-video-path", async () => { + try { + const files = await fs.readdir(RECORDINGS_DIR); + const videoFiles = files.filter((file) => file.endsWith(".webm")); + if (videoFiles.length === 0) { + return { success: false, message: "No recorded video found" }; + } + const latestVideo = videoFiles.sort().reverse()[0]; + const videoPath = path.join(RECORDINGS_DIR, latestVideo); + return { success: true, path: videoPath }; + } catch (error) { + console.error("Failed to get video path:", error); + return { success: false, message: "Failed to get video path", error: String(error) }; + } + }); } const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); +async function cleanupOldRecordings() { + try { + const files = await fs.readdir(RECORDINGS_DIR); + const now = Date.now(); + const maxAge = 7 * 24 * 60 * 60 * 1e3; + for (const file of files) { + const filePath = path.join(RECORDINGS_DIR, file); + const stats = await fs.stat(filePath); + if (now - stats.mtimeMs > maxAge) { + await fs.unlink(filePath); + console.log(`Deleted old recording: ${file}`); + } + } + } catch (error) { + console.error("Failed to cleanup old recordings:", error); + } +} +async function ensureRecordingsDir() { + try { + await fs.mkdir(RECORDINGS_DIR, { recursive: true }); + console.log("Recordings directory ready:", RECORDINGS_DIR); + } catch (error) { + console.error("Failed to create recordings directory:", error); + } +} process.env.APP_ROOT = path.join(__dirname, ".."); const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); @@ -316,10 +375,14 @@ app.on("activate", () => { createWindow(); } }); -app.on("will-quit", () => { +app.on("before-quit", async (event) => { + event.preventDefault(); cleanupMouseTracking(); + await cleanupOldRecordings(); + app.exit(0); }); -app.whenReady().then(() => { +app.whenReady().then(async () => { + await ensureRecordingsDir(); registerIpcHandlers( createEditorWindowWrapper, createSourceSelectorWindowWrapper, @@ -330,6 +393,7 @@ app.whenReady().then(() => { }); export { MAIN_DIST, + RECORDINGS_DIR, RENDERER_DIST, VITE_DEV_SERVER_URL }; diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 712f445..1202937 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -22,7 +22,13 @@ electron.contextBridge.exposeInMainWorld("electronAPI", { stopMouseTracking: () => { return electron.ipcRenderer.invoke("stop-mouse-tracking"); }, - saveMouseTrackingData: (videoFileName) => { - return electron.ipcRenderer.invoke("save-mouse-tracking-data", videoFileName); + storeRecordedVideo: (videoData, fileName) => { + return electron.ipcRenderer.invoke("store-recorded-video", videoData, fileName); + }, + storeMouseTrackingData: (fileName) => { + return electron.ipcRenderer.invoke("store-mouse-tracking-data", fileName); + }, + getRecordedVideoPath: () => { + return electron.ipcRenderer.invoke("get-recorded-video-path"); } }); diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 924a82b..24d225f 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -2,8 +2,8 @@ import { ipcMain, desktopCapturer, BrowserWindow } from 'electron' import { startMouseTracking, stopMouseTracking, getTrackingData } from './mouseTracking' import fs from 'node:fs/promises' import path from 'node:path' +import { RECORDINGS_DIR } from '../main' -// Store selected source let selectedSource: any = null export function registerIpcHandlers( @@ -12,21 +12,17 @@ export function registerIpcHandlers( getMainWindow: () => BrowserWindow | null, getSourceSelectorWindow: () => BrowserWindow | null ) { - // Get available desktop capturer sources ipcMain.handle('get-sources', async (_, opts) => { const sources = await desktopCapturer.getSources(opts) - const processedSources = sources.map(source => ({ + return sources.map(source => ({ id: source.id, name: source.name, display_id: source.display_id, thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, appIcon: source.appIcon ? source.appIcon.toDataURL() : null })) - - return processedSources }) - // Select a source for recording ipcMain.handle('select-source', (_, source) => { selectedSource = source const sourceSelectorWin = getSourceSelectorWindow() @@ -36,12 +32,10 @@ export function registerIpcHandlers( return selectedSource }) - // Get the currently selected source ipcMain.handle('get-selected-source', () => { return selectedSource }) - // Open the source selector window ipcMain.handle('open-source-selector', () => { const sourceSelectorWin = getSourceSelectorWindow() if (sourceSelectorWin) { @@ -51,7 +45,6 @@ export function registerIpcHandlers( createSourceSelectorWindow() }) - // Switch from HUD overlay to editor window ipcMain.handle('switch-to-editor', () => { const mainWin = getMainWindow() if (mainWin) { @@ -60,18 +53,35 @@ export function registerIpcHandlers( createEditorWindow() }) - // Start mouse tracking ipcMain.handle('start-mouse-tracking', () => { return startMouseTracking() }) - // Stop mouse tracking ipcMain.handle('stop-mouse-tracking', () => { return stopMouseTracking() }) - // Save mouse tracking data to file - ipcMain.handle('save-mouse-tracking-data', async (_, videoFileName: string) => { + ipcMain.handle('store-recorded-video', async (_, videoData: ArrayBuffer, fileName: string) => { + try { + const videoPath = path.join(RECORDINGS_DIR, fileName) + await fs.writeFile(videoPath, Buffer.from(videoData)) + + return { + success: true, + path: videoPath, + message: 'Video stored successfully' + } + } catch (error) { + console.error('Failed to store video:', error) + return { + success: false, + message: 'Failed to store video', + error: String(error) + } + } + }) + + ipcMain.handle('store-mouse-tracking-data', async (_, fileName: string) => { try { const data = getTrackingData() @@ -79,24 +89,41 @@ export function registerIpcHandlers( return { success: false, message: 'No tracking data to save' } } - // Save to the same directory as the video, with .json extension - const jsonFileName = videoFileName.replace('.webm', '_tracking.json') - const filePath = path.join(process.env.HOME || '', 'Downloads', jsonFileName) + const trackingPath = path.join(RECORDINGS_DIR, fileName) + await fs.writeFile(trackingPath, JSON.stringify(data, null, 2), 'utf-8') - await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8') - - return { - success: true, - message: 'Tracking data saved', - filePath, - eventCount: data.length + return { + success: true, + path: trackingPath, + eventCount: data.length, + message: 'Mouse tracking data stored successfully' } } catch (error) { - return { - success: false, - message: 'Failed to save tracking data', + console.error('Failed to store mouse tracking data:', error) + return { + success: false, + message: 'Failed to store mouse tracking data', error: String(error) } } }) + + ipcMain.handle('get-recorded-video-path', async () => { + try { + const files = await fs.readdir(RECORDINGS_DIR) + const videoFiles = files.filter(file => file.endsWith('.webm')) + + if (videoFiles.length === 0) { + return { success: false, message: 'No recorded video found' } + } + + const latestVideo = videoFiles.sort().reverse()[0] + const videoPath = path.join(RECORDINGS_DIR, latestVideo) + + return { success: true, path: videoPath } + } catch (error) { + console.error('Failed to get video path:', error) + return { success: false, message: 'Failed to get video path', error: String(error) } + } + }) } diff --git a/electron/main.ts b/electron/main.ts index 3908322..98bb337 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,12 +1,45 @@ import { app, BrowserWindow } from 'electron' import { fileURLToPath } from 'node:url' import path from 'node:path' +import fs from 'node:fs/promises' import { createHudOverlayWindow, createEditorWindow, createSourceSelectorWindow } from './windows' import { registerIpcHandlers } from './ipc/handlers' import { cleanupMouseTracking } from './ipc/mouseTracking' const __dirname = path.dirname(fileURLToPath(import.meta.url)) +export const RECORDINGS_DIR = path.join(app.getPath('userData'), 'recordings') + +// Cleanup old recordings (older than 1 day) +async function cleanupOldRecordings() { + try { + const files = await fs.readdir(RECORDINGS_DIR) + const now = Date.now() + const maxAge = 1 * 24 * 60 * 60 * 1000 + + for (const file of files) { + const filePath = path.join(RECORDINGS_DIR, file) + const stats = await fs.stat(filePath) + + if (now - stats.mtimeMs > maxAge) { + await fs.unlink(filePath) + console.log(`Deleted old recording: ${file}`) + } + } + } catch (error) { + console.error('Failed to cleanup old recordings:', error) + } +} + +async function ensureRecordingsDir() { + try { + await fs.mkdir(RECORDINGS_DIR, { recursive: true }) + console.log('Recordings directory ready:', RECORDINGS_DIR) + } catch (error) { + console.error('Failed to create recordings directory:', error) + } +} + // The built directory structure // // ├─┬─┬ dist @@ -68,12 +101,19 @@ app.on('activate', () => { } }) -app.on('will-quit', () => { +// Cleanup old recordings on quit (both macOS and other platforms) +app.on('before-quit', async (event) => { + event.preventDefault() cleanupMouseTracking() + await cleanupOldRecordings() + app.exit(0) }) // Register all IPC handlers when app is ready -app.whenReady().then(() => { +app.whenReady().then(async () => { + // Ensure recordings directory exists + await ensureRecordingsDir() + registerIpcHandlers( createEditorWindowWrapper, createSourceSelectorWindowWrapper, diff --git a/electron/preload.ts b/electron/preload.ts index 132260a..f355cdf 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -22,7 +22,13 @@ contextBridge.exposeInMainWorld('electronAPI', { stopMouseTracking: () => { return ipcRenderer.invoke('stop-mouse-tracking') }, - saveMouseTrackingData: (videoFileName: string) => { - return ipcRenderer.invoke('save-mouse-tracking-data', videoFileName) + storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => { + return ipcRenderer.invoke('store-recorded-video', videoData, fileName) + }, + storeMouseTrackingData: (fileName: string) => { + return ipcRenderer.invoke('store-mouse-tracking-data', fileName) + }, + getRecordedVideoPath: () => { + return ipcRenderer.invoke('get-recorded-video-path') } }) \ No newline at end of file diff --git a/electron/windows.ts b/electron/windows.ts index 2125b65..7c3211b 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -61,6 +61,7 @@ export function createEditorWindow(): BrowserWindow { preload: path.join(__dirname, 'preload.mjs'), nodeIntegration: false, contextIsolation: true, + webSecurity: false, }, }) diff --git a/src/App.tsx b/src/App.tsx index 217006a..caa4436 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { LaunchWindow } from "./components/LaunchWindow"; import { SourceSelector } from "./components/SourceSelector"; +import { VideoEditor } from "./components/VideoEditor"; import { useEffect, useState } from "react"; export default function App() { @@ -28,12 +29,7 @@ export default function App() { } if (windowType === 'editor') { - return ( -
Recording stopped. Video editor interface coming soon...
-