Skip to content

Commit 0a80830

Browse files
committed
refactor
1 parent 2995a72 commit 0a80830

File tree

16 files changed

+1570
-1414
lines changed

16 files changed

+1570
-1414
lines changed

src/cursor/assets.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* Cursor asset path resolution for Node.js environment
3+
* Used by the processing pipeline (Sharp-based cursor rendering)
4+
*/
5+
6+
import { existsSync } from 'fs';
7+
import { join } from 'path';
8+
import { CURSOR_SHAPE_MAP } from './constants';
9+
10+
// Create a simple logger function to avoid circular dependencies
11+
function logDebug(message: string, ...args: unknown[]): void {
12+
if (process.env.DEBUG_CURSOR) {
13+
console.log(`[CursorAssets] ${message}`, ...args);
14+
}
15+
}
16+
17+
function logWarn(message: string, ...args: unknown[]): void {
18+
console.warn(`[CursorAssets] ${message}`, ...args);
19+
}
20+
21+
/**
22+
* Get the assets directory path based on environment
23+
* Internal function - not exported from module
24+
*/
25+
function getAssetsDir(): string {
26+
// Try to get app from electron, but handle case where it's not available
27+
let app: { isPackaged: boolean; getPath: (name: string) => string } | undefined;
28+
try {
29+
app = require('electron').app;
30+
} catch {
31+
// Electron not available, assume development
32+
}
33+
34+
const isDev = process.env.NODE_ENV === 'development' || !app?.isPackaged;
35+
logDebug('Getting assets directory, isDev:', isDev, '__dirname:', __dirname, 'cwd:', process.cwd());
36+
37+
if (isDev) {
38+
// In development, try multiple possible paths
39+
// __dirname in compiled code will be dist/main/cursor or dist/cursor
40+
const possiblePaths = [
41+
join(__dirname, '../../src/assets'), // From dist/main/cursor
42+
join(__dirname, '../../../src/assets'), // From dist/cursor
43+
join(process.cwd(), 'src/assets'), // From project root
44+
];
45+
46+
logDebug('Trying possible asset paths:', possiblePaths);
47+
48+
for (const path of possiblePaths) {
49+
logDebug('Checking path:', path, 'exists:', existsSync(path));
50+
if (existsSync(path)) {
51+
logDebug('Found assets directory at:', path);
52+
return path;
53+
}
54+
}
55+
56+
// Fallback to project root
57+
const fallbackPath = join(process.cwd(), 'src/assets');
58+
logDebug('Using fallback path:', fallbackPath);
59+
return fallbackPath;
60+
}
61+
62+
// In production, use app resources
63+
const possibleProdPaths: string[] = [];
64+
65+
if (app) {
66+
try {
67+
const resourcesPath = app.getPath('resources');
68+
possibleProdPaths.push(join(resourcesPath, 'assets'));
69+
logDebug('Added path from app.getPath("resources"):', join(resourcesPath, 'assets'));
70+
} catch (error) {
71+
logDebug('Could not get resources path:', error);
72+
}
73+
74+
try {
75+
const exePath = app.getPath('exe');
76+
possibleProdPaths.push(join(exePath, '../../Resources/assets'));
77+
logDebug('Added path from exe calculation:', join(exePath, '../../Resources/assets'));
78+
} catch (error) {
79+
logDebug('Could not get exe path:', error);
80+
}
81+
}
82+
83+
// Calculate from __dirname
84+
const fromDistPath = join(__dirname, '../../../../assets');
85+
possibleProdPaths.push(fromDistPath);
86+
logDebug('Added path from __dirname (4 levels up):', fromDistPath);
87+
88+
const fromDistPathAlt = join(__dirname, '../../../../../assets');
89+
possibleProdPaths.push(fromDistPathAlt);
90+
logDebug('Added alternative __dirname path (5 levels up):', fromDistPathAlt);
91+
92+
// Check each path and return the first one that exists
93+
for (const path of possibleProdPaths) {
94+
logDebug('Checking production path:', path, 'exists:', existsSync(path));
95+
if (existsSync(path)) {
96+
logDebug('Found production assets directory at:', path);
97+
return path;
98+
}
99+
}
100+
101+
// If none found, return the first calculated path (will be checked by caller)
102+
const fallbackPath = possibleProdPaths[0] || join(process.cwd(), 'resources/assets');
103+
logWarn('No production assets directory found, using fallback:', fallbackPath);
104+
return fallbackPath;
105+
}
106+
107+
/**
108+
* Get cursor asset file path for a given shape
109+
* Internal function - used by getCursorAssetFilePath
110+
* @returns Full path to the asset file, or null if not found
111+
*/
112+
function getCursorAssetPath(shape: string): string | null {
113+
const assetFileName = CURSOR_SHAPE_MAP[shape] || CURSOR_SHAPE_MAP.arrow;
114+
const assetsDir = getAssetsDir();
115+
const assetPath = join(assetsDir, assetFileName);
116+
117+
logDebug('Getting cursor asset for shape:', shape, 'file:', assetFileName, 'path:', assetPath);
118+
119+
if (existsSync(assetPath)) {
120+
logDebug('Cursor asset found at:', assetPath);
121+
return assetPath;
122+
}
123+
124+
logDebug('Cursor asset not found at:', assetPath);
125+
return null;
126+
}
127+
128+
/**
129+
* Get cursor asset file path with fallback verification
130+
* Tries multiple possible locations
131+
*/
132+
export function getCursorAssetFilePath(shape: string): string | null {
133+
logDebug('getCursorAssetFilePath called for shape:', shape);
134+
const path = getCursorAssetPath(shape);
135+
136+
if (path) {
137+
// Verify the file actually exists
138+
if (existsSync(path)) {
139+
logDebug('Cursor asset file verified at:', path);
140+
return path;
141+
} else {
142+
logWarn('Cursor asset path resolved but file does not exist:', path);
143+
// Try to find it in common locations
144+
const assetFileName = CURSOR_SHAPE_MAP[shape] || CURSOR_SHAPE_MAP.arrow;
145+
const fallbackPaths = [
146+
join(process.cwd(), 'src/assets', assetFileName),
147+
join(__dirname, '../../src/assets', assetFileName),
148+
join(__dirname, '../../../src/assets', assetFileName),
149+
];
150+
151+
logDebug('Trying fallback paths:', fallbackPaths);
152+
153+
for (const fallbackPath of fallbackPaths) {
154+
logDebug('Checking fallback path:', fallbackPath, 'exists:', existsSync(fallbackPath));
155+
if (existsSync(fallbackPath)) {
156+
logDebug('Found cursor asset at fallback path:', fallbackPath);
157+
return fallbackPath;
158+
}
159+
}
160+
161+
logWarn('Could not find cursor asset in any fallback path');
162+
}
163+
} else {
164+
logWarn('getCursorAssetPath returned null for shape:', shape);
165+
}
166+
167+
return null;
168+
}

src/cursor/constants.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Shared cursor constants
3+
* Used by both the preview renderer (browser) and processing renderer (Node.js)
4+
*/
5+
6+
/**
7+
* Map cursor shape names to asset file names
8+
* Includes all system cursor types detected by cursor-type binary
9+
*/
10+
export const CURSOR_SHAPE_MAP: Record<string, string> = {
11+
// Standard cursors
12+
arrow: 'default.svg',
13+
pointer: 'handpointing.svg',
14+
hand: 'handopen.svg',
15+
openhand: 'handopen.svg',
16+
closedhand: 'handgrabbing.svg',
17+
crosshair: 'cross.svg',
18+
ibeam: 'textcursor.svg',
19+
ibeamvertical: 'textcursorvertical.svg',
20+
21+
// Resize cursors
22+
move: 'move.svg',
23+
resizeleft: 'resizeleftright.svg',
24+
resizeright: 'resizeleftright.svg',
25+
resizeleftright: 'resizeleftright.svg',
26+
resizeup: 'resizeupdown.svg',
27+
resizedown: 'resizeupdown.svg',
28+
resizeupdown: 'resizeupdown.svg',
29+
resize: 'resizenortheastsouthwest.svg',
30+
resizenortheast: 'resizenortheastsouthwest.svg',
31+
resizesouthwest: 'resizenortheastsouthwest.svg',
32+
resizenorthwest: 'resizenorthwestsoutheast.svg',
33+
resizesoutheast: 'resizenorthwestsoutheast.svg',
34+
35+
// Action cursors
36+
copy: 'copy.svg',
37+
dragcopy: 'copy.svg',
38+
draglink: 'default.svg',
39+
help: 'help.svg',
40+
notallowed: 'notallowed.svg',
41+
contextmenu: 'contextualmenu.svg',
42+
poof: 'poof.svg',
43+
44+
// Screenshot/zoom cursors
45+
screenshot: 'screenshotselection.svg',
46+
zoomin: 'zoomin.svg',
47+
zoomout: 'zoomout.svg',
48+
};
49+
50+
/**
51+
* Cursor hotspot offsets (x, y) within the 32x32 viewBox
52+
* These represent the click point within the cursor image
53+
*/
54+
export const CURSOR_HOTSPOT_MAP: Record<string, { x: number; y: number }> = {
55+
// Standard cursors
56+
arrow: { x: 10, y: 7 },
57+
pointer: { x: 9, y: 8 },
58+
hand: { x: 10, y: 10 },
59+
openhand: { x: 10, y: 10 },
60+
closedhand: { x: 10, y: 10 },
61+
crosshair: { x: 16, y: 16 },
62+
ibeam: { x: 13, y: 8 },
63+
ibeamvertical: { x: 8, y: 16 },
64+
65+
// Resize cursors - centered
66+
move: { x: 16, y: 16 },
67+
resizeleft: { x: 16, y: 16 },
68+
resizeright: { x: 16, y: 16 },
69+
resizeleftright: { x: 16, y: 16 },
70+
resizeup: { x: 16, y: 16 },
71+
resizedown: { x: 16, y: 16 },
72+
resizeupdown: { x: 16, y: 16 },
73+
resize: { x: 16, y: 16 },
74+
resizenortheast: { x: 16, y: 16 },
75+
resizesouthwest: { x: 16, y: 16 },
76+
resizenorthwest: { x: 16, y: 16 },
77+
resizesoutheast: { x: 16, y: 16 },
78+
79+
// Action cursors
80+
copy: { x: 10, y: 7 },
81+
dragcopy: { x: 10, y: 7 },
82+
draglink: { x: 10, y: 7 },
83+
help: { x: 10, y: 7 },
84+
notallowed: { x: 16, y: 16 },
85+
contextmenu: { x: 10, y: 7 },
86+
poof: { x: 16, y: 16 },
87+
88+
// Zoom/screenshot cursors
89+
zoomin: { x: 10, y: 10 },
90+
zoomout: { x: 10, y: 10 },
91+
screenshot: { x: 16, y: 16 },
92+
};
93+
94+
/**
95+
* Get cursor hotspot offset for a given shape
96+
*/
97+
export function getCursorHotspot(shape: string): { x: number; y: number } {
98+
return CURSOR_HOTSPOT_MAP[shape] || CURSOR_HOTSPOT_MAP.arrow || { x: 10, y: 7 };
99+
}
100+

src/cursor/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Shared cursor module barrel export
3+
*/
4+
5+
// Asset path resolution (used by cursor-renderer.ts)
6+
export { getCursorAssetFilePath } from './assets';

src/main/ipc/dialogs.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Dialog-related IPC handlers
3+
*/
4+
5+
import { ipcMain, dialog, BrowserWindow } from 'electron';
6+
import { setConfiguredOutputPath, getConfiguredOutputPath } from '../state';
7+
8+
/**
9+
* Register dialog-related IPC handlers
10+
* @param getMainWindow - Function to get main window
11+
*/
12+
export function registerDialogHandlers(
13+
getMainWindow: () => BrowserWindow | null
14+
): void {
15+
ipcMain.handle('select-output-path', async () => {
16+
const mainWindow = getMainWindow();
17+
const result = await dialog.showSaveDialog(mainWindow!, {
18+
title: 'Save Recording',
19+
defaultPath: `recording_${Date.now()}.mp4`,
20+
filters: [
21+
{ name: 'Video Files', extensions: ['mp4', 'mov'] },
22+
{ name: 'All Files', extensions: ['*'] },
23+
],
24+
});
25+
26+
if (result.canceled) {
27+
return null;
28+
}
29+
30+
// Store the configured output path
31+
setConfiguredOutputPath(result.filePath || null);
32+
return result.filePath;
33+
});
34+
35+
ipcMain.handle('set-output-path', async (_, path: string | null) => {
36+
setConfiguredOutputPath(path);
37+
return { success: true };
38+
});
39+
40+
ipcMain.handle('get-output-path', async () => {
41+
return getConfiguredOutputPath();
42+
});
43+
44+
ipcMain.handle('select-video-file', async () => {
45+
const mainWindow = getMainWindow();
46+
const result = await dialog.showOpenDialog(mainWindow!, {
47+
title: 'Select Video File',
48+
filters: [
49+
{ name: 'Video Files', extensions: ['mp4', 'mov', 'mkv', 'avi', 'webm'] },
50+
{ name: 'All Files', extensions: ['*'] },
51+
],
52+
properties: ['openFile'],
53+
});
54+
55+
if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
56+
return null;
57+
}
58+
59+
return result.filePaths[0];
60+
});
61+
62+
ipcMain.handle('select-metadata-file', async () => {
63+
const mainWindow = getMainWindow();
64+
const result = await dialog.showOpenDialog(mainWindow!, {
65+
title: 'Select Metadata File',
66+
filters: [
67+
{ name: 'JSON Files', extensions: ['json'] },
68+
{ name: 'All Files', extensions: ['*'] },
69+
],
70+
properties: ['openFile'],
71+
});
72+
73+
if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
74+
return null;
75+
}
76+
77+
return result.filePaths[0];
78+
});
79+
}

src/main/ipc/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* IPC handlers barrel export and registration
3+
*/
4+
5+
import type { BrowserWindow } from 'electron';
6+
import type { Platform } from '../../platform';
7+
import { registerPermissionHandlers } from './permissions';
8+
import { registerRecordingHandlers } from './recording';
9+
import { registerRecordingBarHandlers } from './recording-bar';
10+
import { registerDialogHandlers } from './dialogs';
11+
import { registerStudioHandlers } from './studio';
12+
13+
/**
14+
* Register all IPC handlers
15+
* @param initPlatform - Function to initialize/get platform instance
16+
* @param getMainWindow - Function to get main window
17+
* @param createWindow - Function to create main window
18+
*/
19+
export function registerAllIpcHandlers(
20+
initPlatform: () => Promise<Platform>,
21+
getMainWindow: () => BrowserWindow | null,
22+
createWindow: () => void
23+
): void {
24+
registerPermissionHandlers(initPlatform);
25+
registerRecordingHandlers(initPlatform, getMainWindow);
26+
registerRecordingBarHandlers(initPlatform, getMainWindow, createWindow);
27+
registerDialogHandlers(getMainWindow);
28+
registerStudioHandlers(getMainWindow);
29+
}

0 commit comments

Comments
 (0)