uiohook refactoring
This commit is contained in:
+82
-23
@@ -2,6 +2,7 @@ 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";
|
||||
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"];
|
||||
@@ -98,77 +99,112 @@ function createSourceSelectorWindow() {
|
||||
}
|
||||
let isMouseTrackingActive = false;
|
||||
let isHookStarted = false;
|
||||
let recordingStartTime = 0;
|
||||
let mouseEventData = [];
|
||||
function startMouseTracking() {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log("⚠️ Mouse tracking already active");
|
||||
return { success: false, message: "Already tracking" };
|
||||
}
|
||||
console.log("🎯 Starting mouse tracking...");
|
||||
isMouseTrackingActive = true;
|
||||
recordingStartTime = performance.now();
|
||||
mouseEventData = [];
|
||||
if (!isHookStarted) {
|
||||
setupMouseEventListeners();
|
||||
try {
|
||||
uIOhook.start();
|
||||
isHookStarted = true;
|
||||
console.log("✅ Mouse tracking started successfully");
|
||||
console.log('💡 If you see "Accessibility API is disabled" error:');
|
||||
console.log(" Go to System Settings → Privacy & Security → Accessibility");
|
||||
console.log(" Enable permissions for Electron/Terminal/VS Code");
|
||||
return { success: true, message: "Mouse tracking started" };
|
||||
return { success: true, message: "Mouse tracking started", startTime: recordingStartTime };
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to start mouse tracking:", error);
|
||||
isMouseTrackingActive = false;
|
||||
return { success: false, message: "Failed to start hook", error };
|
||||
}
|
||||
} else {
|
||||
console.log("✅ Mouse tracking resumed");
|
||||
return { success: true, message: "Mouse tracking resumed" };
|
||||
return { success: true, message: "Mouse tracking resumed", startTime: recordingStartTime };
|
||||
}
|
||||
}
|
||||
function stopMouseTracking() {
|
||||
if (!isMouseTrackingActive) {
|
||||
console.log("⚠️ Mouse tracking not active");
|
||||
return { success: false, message: "Not currently tracking" };
|
||||
}
|
||||
console.log("🛑 Stopping mouse tracking...");
|
||||
isMouseTrackingActive = false;
|
||||
console.log("✅ Mouse tracking stopped (events will still be captured but not logged)");
|
||||
return { success: true, message: "Mouse tracking stopped" };
|
||||
const duration = performance.now() - recordingStartTime;
|
||||
const session = {
|
||||
startTime: recordingStartTime,
|
||||
events: mouseEventData,
|
||||
duration
|
||||
};
|
||||
return {
|
||||
success: true,
|
||||
message: "Mouse tracking stopped",
|
||||
data: session
|
||||
};
|
||||
}
|
||||
function setupMouseEventListeners() {
|
||||
uIOhook.on("mousemove", (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[MOUSE MOVE] x: ${e.x}, y: ${e.y}`);
|
||||
const timestamp = performance.now() - recordingStartTime;
|
||||
const event = {
|
||||
type: "move",
|
||||
timestamp,
|
||||
x: e.x,
|
||||
y: e.y
|
||||
};
|
||||
mouseEventData.push(event);
|
||||
}
|
||||
});
|
||||
uIOhook.on("mousedown", (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[MOUSE DOWN] x: ${e.x}, y: ${e.y}, button: ${e.button}, clicks: ${e.clicks}`);
|
||||
const timestamp = performance.now() - recordingStartTime;
|
||||
const event = {
|
||||
type: "down",
|
||||
timestamp,
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
button: e.button,
|
||||
clicks: e.clicks
|
||||
};
|
||||
mouseEventData.push(event);
|
||||
}
|
||||
});
|
||||
uIOhook.on("mouseup", (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[MOUSE UP] x: ${e.x}, y: ${e.y}, button: ${e.button}`);
|
||||
const timestamp = performance.now() - recordingStartTime;
|
||||
const event = {
|
||||
type: "up",
|
||||
timestamp,
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
button: e.button
|
||||
};
|
||||
mouseEventData.push(event);
|
||||
}
|
||||
});
|
||||
uIOhook.on("click", (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[CLICK] x: ${e.x}, y: ${e.y}, button: ${e.button}, clicks: ${e.clicks}`);
|
||||
}
|
||||
});
|
||||
uIOhook.on("wheel", (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[WHEEL] x: ${e.x}, y: ${e.y}, amount: ${e.amount}, direction: ${e.direction}, rotation: ${e.rotation}`);
|
||||
const timestamp = performance.now() - recordingStartTime;
|
||||
const event = {
|
||||
type: "click",
|
||||
timestamp,
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
button: e.button,
|
||||
clicks: e.clicks
|
||||
};
|
||||
mouseEventData.push(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
function getTrackingData() {
|
||||
return [...mouseEventData];
|
||||
}
|
||||
function cleanupMouseTracking() {
|
||||
if (isHookStarted) {
|
||||
try {
|
||||
uIOhook.stop();
|
||||
isHookStarted = false;
|
||||
isMouseTrackingActive = false;
|
||||
console.log("🧹 Mouse tracking cleaned up");
|
||||
mouseEventData = [];
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up mouse tracking:", error);
|
||||
}
|
||||
@@ -219,6 +255,29 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g
|
||||
ipcMain.handle("stop-mouse-tracking", () => {
|
||||
return stopMouseTracking();
|
||||
});
|
||||
ipcMain.handle("save-mouse-tracking-data", async (_, videoFileName) => {
|
||||
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");
|
||||
return {
|
||||
success: true,
|
||||
message: "Tracking data saved",
|
||||
filePath,
|
||||
eventCount: data.length
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to save tracking data",
|
||||
error: String(error)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
process.env.APP_ROOT = path.join(__dirname, "..");
|
||||
|
||||
@@ -21,5 +21,8 @@ electron.contextBridge.exposeInMainWorld("electronAPI", {
|
||||
},
|
||||
stopMouseTracking: () => {
|
||||
return electron.ipcRenderer.invoke("stop-mouse-tracking");
|
||||
},
|
||||
saveMouseTrackingData: (videoFileName) => {
|
||||
return electron.ipcRenderer.invoke("save-mouse-tracking-data", videoFileName);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ipcMain, desktopCapturer, BrowserWindow } from 'electron'
|
||||
import { startMouseTracking, stopMouseTracking } from './mouseTracking'
|
||||
import { startMouseTracking, stopMouseTracking, getTrackingData } from './mouseTracking'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
// Store selected source
|
||||
let selectedSource: any = null
|
||||
@@ -67,4 +69,34 @@ export function registerIpcHandlers(
|
||||
ipcMain.handle('stop-mouse-tracking', () => {
|
||||
return stopMouseTracking()
|
||||
})
|
||||
|
||||
// Save mouse tracking data to file
|
||||
ipcMain.handle('save-mouse-tracking-data', async (_, videoFileName: string) => {
|
||||
try {
|
||||
const data = getTrackingData()
|
||||
|
||||
if (data.length === 0) {
|
||||
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)
|
||||
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Tracking data saved',
|
||||
filePath,
|
||||
eventCount: data.length
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to save tracking data',
|
||||
error: String(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,15 +2,34 @@ import { uIOhook } from 'uiohook-napi'
|
||||
|
||||
let isMouseTrackingActive = false
|
||||
let isHookStarted = false
|
||||
let recordingStartTime: number = 0
|
||||
let mouseEventData: MouseEvent[] = []
|
||||
|
||||
export interface MouseEvent {
|
||||
type: 'move' | 'down' | 'up' | 'click'
|
||||
timestamp: number // milliseconds since recording started
|
||||
x: number
|
||||
y: number
|
||||
button?: unknown
|
||||
clicks?: number
|
||||
}
|
||||
|
||||
export interface MouseTrackingSession {
|
||||
startTime: number
|
||||
events: MouseEvent[]
|
||||
duration: number
|
||||
}
|
||||
|
||||
export function startMouseTracking() {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log('⚠️ Mouse tracking already active')
|
||||
return { success: false, message: 'Already tracking' }
|
||||
}
|
||||
|
||||
console.log('🎯 Starting mouse tracking...')
|
||||
isMouseTrackingActive = true
|
||||
|
||||
// Reset data for new recording session
|
||||
recordingStartTime = performance.now()
|
||||
mouseEventData = []
|
||||
|
||||
// Only start the hook once
|
||||
if (!isHookStarted) {
|
||||
@@ -19,70 +38,104 @@ export function startMouseTracking() {
|
||||
try {
|
||||
uIOhook.start()
|
||||
isHookStarted = true
|
||||
console.log('✅ Mouse tracking started successfully')
|
||||
console.log('💡 If you see "Accessibility API is disabled" error:')
|
||||
console.log(' Go to System Settings → Privacy & Security → Accessibility')
|
||||
console.log(' Enable permissions for Electron/Terminal/VS Code')
|
||||
return { success: true, message: 'Mouse tracking started' }
|
||||
return { success: true, message: 'Mouse tracking started', startTime: recordingStartTime }
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start mouse tracking:', error)
|
||||
isMouseTrackingActive = false
|
||||
return { success: false, message: 'Failed to start hook', error }
|
||||
}
|
||||
} else {
|
||||
console.log('✅ Mouse tracking resumed')
|
||||
return { success: true, message: 'Mouse tracking resumed' }
|
||||
return { success: true, message: 'Mouse tracking resumed', startTime: recordingStartTime }
|
||||
}
|
||||
}
|
||||
|
||||
export function stopMouseTracking() {
|
||||
export function stopMouseTracking(): { success: boolean; message: string; data?: MouseTrackingSession } {
|
||||
if (!isMouseTrackingActive) {
|
||||
console.log('⚠️ Mouse tracking not active')
|
||||
return { success: false, message: 'Not currently tracking' }
|
||||
}
|
||||
|
||||
console.log('🛑 Stopping mouse tracking...')
|
||||
isMouseTrackingActive = false
|
||||
console.log('✅ Mouse tracking stopped (events will still be captured but not logged)')
|
||||
return { success: true, message: 'Mouse tracking stopped' }
|
||||
|
||||
const duration = performance.now() - recordingStartTime
|
||||
|
||||
const session: MouseTrackingSession = {
|
||||
startTime: recordingStartTime,
|
||||
events: mouseEventData,
|
||||
duration: duration
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Mouse tracking stopped',
|
||||
data: session
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setupMouseEventListeners() {
|
||||
// Track mouse movement
|
||||
uIOhook.on('mousemove', (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[MOUSE MOVE] x: ${e.x}, y: ${e.y}`)
|
||||
const timestamp = performance.now() - recordingStartTime
|
||||
const event: MouseEvent = {
|
||||
type: 'move',
|
||||
timestamp,
|
||||
x: e.x,
|
||||
y: e.y
|
||||
}
|
||||
mouseEventData.push(event)
|
||||
}
|
||||
})
|
||||
|
||||
// Track mouse button press
|
||||
uIOhook.on('mousedown', (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[MOUSE DOWN] x: ${e.x}, y: ${e.y}, button: ${e.button}, clicks: ${e.clicks}`)
|
||||
const timestamp = performance.now() - recordingStartTime
|
||||
const event: MouseEvent = {
|
||||
type: 'down',
|
||||
timestamp,
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
button: e.button,
|
||||
clicks: e.clicks
|
||||
}
|
||||
mouseEventData.push(event)
|
||||
}
|
||||
})
|
||||
|
||||
// Track mouse button release
|
||||
uIOhook.on('mouseup', (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[MOUSE UP] x: ${e.x}, y: ${e.y}, button: ${e.button}`)
|
||||
const timestamp = performance.now() - recordingStartTime
|
||||
const event: MouseEvent = {
|
||||
type: 'up',
|
||||
timestamp,
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
button: e.button
|
||||
}
|
||||
mouseEventData.push(event)
|
||||
}
|
||||
})
|
||||
|
||||
// Track complete click events
|
||||
uIOhook.on('click', (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[CLICK] x: ${e.x}, y: ${e.y}, button: ${e.button}, clicks: ${e.clicks}`)
|
||||
const timestamp = performance.now() - recordingStartTime
|
||||
const event: MouseEvent = {
|
||||
type: 'click',
|
||||
timestamp,
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
button: e.button,
|
||||
clicks: e.clicks
|
||||
}
|
||||
mouseEventData.push(event)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Track mouse wheel scrolling
|
||||
uIOhook.on('wheel', (e) => {
|
||||
if (isMouseTrackingActive) {
|
||||
console.log(`[WHEEL] x: ${e.x}, y: ${e.y}, amount: ${e.amount}, direction: ${e.direction}, rotation: ${e.rotation}`)
|
||||
}
|
||||
})
|
||||
export function getTrackingData(): MouseEvent[] {
|
||||
return [...mouseEventData]
|
||||
}
|
||||
|
||||
export function cleanupMouseTracking() {
|
||||
@@ -91,7 +144,7 @@ export function cleanupMouseTracking() {
|
||||
uIOhook.stop()
|
||||
isHookStarted = false
|
||||
isMouseTrackingActive = false
|
||||
console.log('🧹 Mouse tracking cleaned up')
|
||||
mouseEventData = []
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up mouse tracking:', error)
|
||||
}
|
||||
|
||||
@@ -21,5 +21,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
},
|
||||
stopMouseTracking: () => {
|
||||
return ipcRenderer.invoke('stop-mouse-tracking')
|
||||
},
|
||||
saveMouseTrackingData: (videoFileName: string) => {
|
||||
return ipcRenderer.invoke('save-mouse-tracking-data', videoFileName)
|
||||
}
|
||||
})
|
||||
@@ -37,7 +37,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
}
|
||||
|
||||
// Start mouse tracking
|
||||
console.log('Starting mouse tracking from renderer...')
|
||||
await window.electronAPI.startMouseTracking();
|
||||
|
||||
// Use the selected source
|
||||
@@ -68,21 +67,36 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
chunksRef.current.push(e.data);
|
||||
}
|
||||
};
|
||||
recorder.onstop = () => {
|
||||
recorder.onstop = async () => {
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop());
|
||||
streamRef.current = null;
|
||||
}
|
||||
if (chunksRef.current.length === 0) return;
|
||||
|
||||
const blob = new Blob(chunksRef.current, { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `recording-${Date.now()}.webm`;
|
||||
|
||||
// Generate filename with timestamp
|
||||
const videoFileName = `recording-${Date.now()}.webm`;
|
||||
a.download = videoFileName;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
// Save mouse tracking data alongside the video
|
||||
try {
|
||||
const result = await window.electronAPI.saveMouseTrackingData(videoFileName);
|
||||
if (!result.success) {
|
||||
console.warn('Failed to save tracking data:', result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving tracking data:', error);
|
||||
}
|
||||
};
|
||||
recorder.onerror = () => {
|
||||
setRecording(false);
|
||||
@@ -107,7 +121,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
setRecording(false);
|
||||
|
||||
// Stop mouse tracking
|
||||
console.log('Stopping mouse tracking from renderer...')
|
||||
window.electronAPI.stopMouseTracking();
|
||||
}
|
||||
};
|
||||
|
||||
Vendored
+8
-2
@@ -15,7 +15,13 @@ interface Window {
|
||||
openSourceSelector: () => Promise<void>
|
||||
selectSource: (source: any) => Promise<any>
|
||||
getSelectedSource: () => Promise<any>
|
||||
startMouseTracking: () => Promise<{ success: boolean }>
|
||||
stopMouseTracking: () => Promise<{ success: boolean }>
|
||||
startMouseTracking: () => Promise<{ success: boolean; startTime?: number }>
|
||||
stopMouseTracking: () => Promise<{ success: boolean; data?: any }>
|
||||
saveMouseTrackingData: (videoFileName: string) => Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
filePath?: string
|
||||
eventCount?: number
|
||||
}>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user