Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ee42a39
Add global mouse tracking during recording sessions
bakaEC Dec 22, 2025
0293139
Add cursor and mouse move event handlers in preload
bakaEC Dec 22, 2025
ad75a50
Add cursor data storage and retrieval functions
bakaEC Dec 22, 2025
2250eb6
Add global mouse position tracking during recording
bakaEC Dec 22, 2025
fb4bff9
Add cursor data and global mouse move event handling
bakaEC Dec 22, 2025
e99d37e
Add default SVG icon file
bakaEC Dec 22, 2025
fff2b43
Add cursor style controls in SettingsPanel component
bakaEC Dec 22, 2025
056c7cf
Adds cursor track loading and selection management functionality
bakaEC Dec 22, 2025
9232adf
Add cursor trail rendering with SVG arrow support
bakaEC Dec 22, 2025
35f8df1
Add cursor variant and icon to timeline items
bakaEC Dec 22, 2025
a508dc5
Add blue glass style with hover and selected states
bakaEC Dec 22, 2025
a8dafe8
Add cursor track support and rendering logic
bakaEC Dec 22, 2025
ca98dd1
Add cursor-related type definitions and defaults
bakaEC Dec 22, 2025
41e42fc
Add cursor capture with normalization and cleanup logic
bakaEC Dec 22, 2025
3949629
Add cursor data storage and retrieval methods to electronAPI
bakaEC Dec 22, 2025
58b8d53
Improve mouse zoom in/out and trajectory adaptation during zooming.
bakaEC Dec 22, 2025
9ce5af2
feat(cursor): add time-bound spline-based smoothing to cursor playback
bakaEC Dec 22, 2025
b2136f7
Add cursor smoothing settings in video editor
bakaEC Dec 22, 2025
a0d0777
Add cursor settings and end-to-end parameters control
bakaEC Dec 22, 2025
95f406a
Add end2end cursor smoothing for video playback
bakaEC Dec 22, 2025
586063d
Add cursor enable switch to TimelineEditor component
bakaEC Dec 22, 2025
2fa9235
Add End2EndParams and CursorSmoothing types
bakaEC Dec 22, 2025
29fd9d9
feat(cursor): add time and position offsets and UI controls
bakaEC Dec 22, 2025
519b02c
feature: dynamic zoom-follow using smoothed cursor track- Add dynamic…
bakaEC Dec 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 174 additions & 23 deletions dist-electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,16 @@ function createHudOverlayWindow() {
return win;
}
function createEditorWindow() {
const isMac = process.platform === "darwin";
const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 12, y: 12 },
...isMac && {
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 12, y: 12 }
},
transparent: false,
resizable: true,
alwaysOnTop: false,
Expand Down Expand Up @@ -124,6 +127,9 @@ function createSourceSelectorWindow() {
return win;
}
let selectedSource = null;
let globalMouseListenerInterval = null;
let recordingWindow = null;
let lastMousePosition = null;
function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, getMainWindow, getSourceSelectorWindow, onRecordingStateChange) {
ipcMain.handle("get-sources", async (_, opts) => {
const sources = await desktopCapturer.getSources(opts);
Expand Down Expand Up @@ -180,6 +186,26 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g
};
}
});
ipcMain.handle("store-cursor-data", async (_, videoPath, cursorData) => {
try {
const cursorPath = `${videoPath}.cursor.json`;
const payload = JSON.stringify(cursorData);
await fs.writeFile(cursorPath, payload, "utf-8");
return { success: true, path: cursorPath };
} catch (error) {
console.error("Failed to store cursor data:", error);
return { success: false, message: "Failed to store cursor data", error: String(error) };
}
});
ipcMain.handle("load-cursor-data", async (_, videoPath) => {
try {
const cursorPath = `${videoPath}.cursor.json`;
const data = await fs.readFile(cursorPath, "utf-8");
return { success: true, path: cursorPath, data };
} catch (error) {
return { success: false, message: "Cursor data not found", error: String(error) };
}
});
ipcMain.handle("get-recorded-video-path", async () => {
try {
const files = await fs.readdir(RECORDINGS_DIR);
Expand All @@ -200,7 +226,57 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g
if (onRecordingStateChange) {
onRecordingStateChange(recording, source.name);
}
if (recording) {
startGlobalMouseListener(getMainWindow());
} else {
stopGlobalMouseListener();
}
});
function startGlobalMouseListener(window) {
if (globalMouseListenerInterval) {
return;
}
recordingWindow = window;
lastMousePosition = null;
globalMouseListenerInterval = setInterval(() => {
const targetWindow = recordingWindow || getMainWindow();
if (!targetWindow || targetWindow.isDestroyed()) {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length === 0) {
stopGlobalMouseListener();
return;
}
recordingWindow = allWindows[0];
}
try {
const point = screen.getCursorScreenPoint();
const currentPosition = { x: point.x, y: point.y };
if (!lastMousePosition || lastMousePosition.x !== currentPosition.x || lastMousePosition.y !== currentPosition.y) {
const windows = BrowserWindow.getAllWindows();
windows.forEach((win) => {
if (!win.isDestroyed()) {
win.webContents.send("global-mouse-move", {
screenX: currentPosition.x,
screenY: currentPosition.y,
timestamp: Date.now()
});
}
});
lastMousePosition = currentPosition;
}
} catch (error) {
console.error("Error in global mouse listener:", error);
}
}, 1e3 / 60);
}
function stopGlobalMouseListener() {
if (globalMouseListenerInterval) {
clearInterval(globalMouseListenerInterval);
globalMouseListenerInterval = null;
}
recordingWindow = null;
lastMousePosition = null;
}
ipcMain.handle("open-external-url", async (_, url) => {
try {
await shell.openExternal(url);
Expand Down Expand Up @@ -295,6 +371,62 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g
ipcMain.handle("get-platform", () => {
return process.platform;
});
ipcMain.handle("get-source-bounds", async () => {
try {
if (!selectedSource) {
return { success: false, message: "No source selected" };
}
const sourceId = selectedSource.id;
if (sourceId.startsWith("screen:")) {
const displays = screen.getAllDisplays();
const displayId = selectedSource.display_id;
const display = displays.find((d) => String(d.id) === String(displayId)) || screen.getPrimaryDisplay();
return {
success: true,
bounds: {
x: display.bounds.x,
y: display.bounds.y,
width: display.bounds.width,
height: display.bounds.height
},
scaleFactor: display.scaleFactor || 1
};
}
if (sourceId.startsWith("window:")) {
const displays = screen.getAllDisplays();
const displayId = selectedSource.display_id;
const display = displays.find((d) => String(d.id) === String(displayId)) || screen.getPrimaryDisplay();
return {
success: true,
bounds: {
x: display.bounds.x,
y: display.bounds.y,
width: display.bounds.width,
height: display.bounds.height
},
scaleFactor: display.scaleFactor || 1
};
}
const primaryDisplay = screen.getPrimaryDisplay();
return {
success: true,
bounds: {
x: primaryDisplay.bounds.x,
y: primaryDisplay.bounds.y,
width: primaryDisplay.bounds.width,
height: primaryDisplay.bounds.height
},
scaleFactor: primaryDisplay.scaleFactor || 1
};
} catch (error) {
console.error("Failed to get source bounds:", error);
return {
success: false,
message: "Failed to get source bounds",
error: String(error)
};
}
});
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings");
Expand All @@ -316,19 +448,26 @@ let mainWindow = null;
let sourceSelectorWindow = null;
let tray = null;
let selectedSourceName = "";
const defaultTrayIcon = getTrayIcon("openscreen.png");
const recordingTrayIcon = getTrayIcon("rec-button.png");
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();
tray = new Tray(defaultTrayIcon);
}
function getTrayIcon(filename) {
return nativeImage.createFromPath(path.join(process.env.VITE_PUBLIC || RENDERER_DIST, filename)).resize({
width: 24,
height: 24,
quality: "best"
});
}
function updateTrayMenu() {
function updateTrayMenu(recording = false) {
if (!tray) return;
const menuTemplate = [
const trayIcon = recording ? recordingTrayIcon : defaultTrayIcon;
const trayToolTip = recording ? `Recording: ${selectedSourceName}` : "OpenScreen";
const menuTemplate = recording ? [
{
label: "Stop Recording",
click: () => {
Expand All @@ -337,10 +476,27 @@ function updateTrayMenu() {
}
}
}
] : [
{
label: "Open",
click: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.isMinimized() && mainWindow.restore();
} else {
createWindow();
}
}
},
{
label: "Quit",
click: () => {
app.quit();
}
}
];
const contextMenu = Menu.buildFromTemplate(menuTemplate);
tray.setContextMenu(contextMenu);
tray.setToolTip(`Recording: ${selectedSourceName}`);
tray.setImage(trayIcon);
tray.setToolTip(trayToolTip);
tray.setContextMenu(Menu.buildFromTemplate(menuTemplate));
}
function createEditorWindowWrapper() {
if (mainWindow) {
Expand All @@ -366,10 +522,10 @@ app.on("activate", () => {
app.whenReady().then(async () => {
const { ipcMain: ipcMain2 } = await import("electron");
ipcMain2.on("hud-overlay-close", () => {
if (process.platform === "darwin") {
app.quit();
}
app.quit();
});
createTray();
updateTrayMenu();
await ensureRecordingsDir();
registerIpcHandlers(
createEditorWindowWrapper,
Expand All @@ -378,14 +534,9 @@ app.whenReady().then(async () => {
() => sourceSelectorWindow,
(recording, sourceName) => {
selectedSourceName = sourceName;
if (recording) {
if (!tray) createTray();
updateTrayMenu();
} else {
if (tray) {
tray.destroy();
tray = null;
}
if (!tray) createTray();
updateTrayMenu(recording);
if (!recording) {
if (mainWindow) mainWindow.restore();
}
}
Expand Down
14 changes: 14 additions & 0 deletions dist-electron/preload.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ electron.contextBridge.exposeInMainWorld("electronAPI", {
storeRecordedVideo: (videoData, fileName) => {
return electron.ipcRenderer.invoke("store-recorded-video", videoData, fileName);
},
storeCursorData: (videoPath, cursorData) => {
return electron.ipcRenderer.invoke("store-cursor-data", videoPath, cursorData);
},
loadCursorData: (videoPath) => {
return electron.ipcRenderer.invoke("load-cursor-data", videoPath);
},
getRecordedVideoPath: () => {
return electron.ipcRenderer.invoke("get-recorded-video-path");
},
Expand All @@ -39,6 +45,11 @@ electron.contextBridge.exposeInMainWorld("electronAPI", {
electron.ipcRenderer.on("stop-recording-from-tray", listener);
return () => electron.ipcRenderer.removeListener("stop-recording-from-tray", listener);
},
onGlobalMouseMove: (callback) => {
const listener = (_, event) => callback(event);
electron.ipcRenderer.on("global-mouse-move", listener);
return () => electron.ipcRenderer.removeListener("global-mouse-move", listener);
},
openExternalUrl: (url) => {
return electron.ipcRenderer.invoke("open-external-url", url);
},
Expand All @@ -59,5 +70,8 @@ electron.contextBridge.exposeInMainWorld("electronAPI", {
},
getPlatform: () => {
return electron.ipcRenderer.invoke("get-platform");
},
getSourceBounds: () => {
return electron.ipcRenderer.invoke("get-source-bounds");
}
});
6 changes: 4 additions & 2 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ interface Window {
openSourceSelector: () => Promise<void>
selectSource: (source: any) => Promise<any>
getSelectedSource: () => Promise<any>
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }>
getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }>
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }>
storeCursorData: (videoPath: string, cursorData: unknown) => Promise<{ success: boolean; path?: string; message?: string; error?: string }>
loadCursorData: (videoPath: string) => Promise<{ success: boolean; path?: string; data?: string; message?: string; error?: string }>
getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }>
setRecordingState: (recording: boolean) => Promise<void>
onStopRecordingFromTray: (callback: () => void) => () => void
openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>
Expand Down
Loading