6e6ecba172
Implements GIF export alongside MP4, including new export types, a GIF exporter module, UI components for format selection and GIF options, and integration into the export dialog and video editor. Adds property-based and unit tests for GIF export correctness, updates dependencies to include gif.js and related types, and refines Electron save dialog to support GIF files.
223 lines
6.3 KiB
TypeScript
223 lines
6.3 KiB
TypeScript
import { ipcMain, desktopCapturer, BrowserWindow, shell, app, dialog } from 'electron'
|
|
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { RECORDINGS_DIR } from '../main'
|
|
|
|
let selectedSource: any = null
|
|
|
|
export function registerIpcHandlers(
|
|
createEditorWindow: () => void,
|
|
createSourceSelectorWindow: () => BrowserWindow,
|
|
getMainWindow: () => BrowserWindow | null,
|
|
getSourceSelectorWindow: () => BrowserWindow | null,
|
|
onRecordingStateChange?: (recording: boolean, sourceName: string) => void
|
|
) {
|
|
ipcMain.handle('get-sources', async (_, opts) => {
|
|
const sources = await desktopCapturer.getSources(opts)
|
|
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
|
|
}))
|
|
})
|
|
|
|
ipcMain.handle('select-source', (_, source) => {
|
|
selectedSource = source
|
|
const sourceSelectorWin = getSourceSelectorWindow()
|
|
if (sourceSelectorWin) {
|
|
sourceSelectorWin.close()
|
|
}
|
|
return selectedSource
|
|
})
|
|
|
|
ipcMain.handle('get-selected-source', () => {
|
|
return selectedSource
|
|
})
|
|
|
|
ipcMain.handle('open-source-selector', () => {
|
|
const sourceSelectorWin = getSourceSelectorWindow()
|
|
if (sourceSelectorWin) {
|
|
sourceSelectorWin.focus()
|
|
return
|
|
}
|
|
createSourceSelectorWindow()
|
|
})
|
|
|
|
ipcMain.handle('switch-to-editor', () => {
|
|
const mainWin = getMainWindow()
|
|
if (mainWin) {
|
|
mainWin.close()
|
|
}
|
|
createEditorWindow()
|
|
})
|
|
|
|
|
|
|
|
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))
|
|
currentVideoPath = videoPath;
|
|
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('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) }
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('set-recording-state', (_, recording: boolean) => {
|
|
const source = selectedSource || { name: 'Screen' }
|
|
if (onRecordingStateChange) {
|
|
onRecordingStateChange(recording, source.name)
|
|
}
|
|
})
|
|
|
|
|
|
ipcMain.handle('open-external-url', async (_, url: string) => {
|
|
try {
|
|
await shell.openExternal(url)
|
|
return { success: true }
|
|
} catch (error) {
|
|
console.error('Failed to open URL:', error)
|
|
return { success: false, error: String(error) }
|
|
}
|
|
})
|
|
|
|
// Return base path for assets so renderer can resolve file:// paths in production
|
|
ipcMain.handle('get-asset-base-path', () => {
|
|
try {
|
|
if (app.isPackaged) {
|
|
return path.join(process.resourcesPath, 'assets')
|
|
}
|
|
return path.join(app.getAppPath(), 'public', 'assets')
|
|
} catch (err) {
|
|
console.error('Failed to resolve asset base path:', err)
|
|
return null
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('save-exported-video', async (_, videoData: ArrayBuffer, fileName: string) => {
|
|
try {
|
|
const mainWindow = getMainWindow();
|
|
|
|
// Determine file type from extension
|
|
const isGif = fileName.toLowerCase().endsWith('.gif');
|
|
const filters = isGif
|
|
? [{ name: 'GIF Image', extensions: ['gif'] }]
|
|
: [{ name: 'MP4 Video', extensions: ['mp4'] }];
|
|
|
|
const result = await dialog.showSaveDialog(mainWindow || undefined, {
|
|
title: isGif ? 'Save Exported GIF' : 'Save Exported Video',
|
|
defaultPath: path.join(app.getPath('downloads'), fileName),
|
|
filters,
|
|
properties: ['createDirectory', 'showOverwriteConfirmation']
|
|
});
|
|
|
|
if (result.canceled || !result.filePath) {
|
|
return {
|
|
success: false,
|
|
cancelled: true,
|
|
message: 'Export cancelled'
|
|
};
|
|
}
|
|
|
|
await fs.writeFile(result.filePath, Buffer.from(videoData));
|
|
|
|
return {
|
|
success: true,
|
|
path: result.filePath,
|
|
message: 'Video exported successfully'
|
|
};
|
|
} catch (error) {
|
|
console.error('Failed to save exported video:', error)
|
|
return {
|
|
success: false,
|
|
message: 'Failed to save exported video',
|
|
error: String(error)
|
|
}
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('open-video-file-picker', async () => {
|
|
try {
|
|
const result = await dialog.showOpenDialog({
|
|
title: 'Select Video File',
|
|
defaultPath: RECORDINGS_DIR,
|
|
filters: [
|
|
{ name: 'Video Files', extensions: ['webm', 'mp4', 'mov', 'avi', 'mkv'] },
|
|
{ name: 'All Files', extensions: ['*'] }
|
|
],
|
|
properties: ['openFile']
|
|
});
|
|
|
|
if (result.canceled || result.filePaths.length === 0) {
|
|
return { success: false, cancelled: true };
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
path: result.filePaths[0]
|
|
};
|
|
} catch (error) {
|
|
console.error('Failed to open file picker:', error);
|
|
return {
|
|
success: false,
|
|
message: 'Failed to open file picker',
|
|
error: String(error)
|
|
};
|
|
}
|
|
});
|
|
|
|
let currentVideoPath: string | null = null;
|
|
|
|
ipcMain.handle('set-current-video-path', (_, path: string) => {
|
|
currentVideoPath = path;
|
|
return { success: true };
|
|
});
|
|
|
|
ipcMain.handle('get-current-video-path', () => {
|
|
return currentVideoPath ? { success: true, path: currentVideoPath } : { success: false };
|
|
});
|
|
|
|
ipcMain.handle('clear-current-video-path', () => {
|
|
currentVideoPath = null;
|
|
return { success: true };
|
|
});
|
|
|
|
ipcMain.handle('get-platform', () => {
|
|
return process.platform;
|
|
});
|
|
}
|