mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-27 00:05:39 +00:00
fix(editor): polish cursor sway and HUD capture controls
Keep the recording HUD visible on screen without burning it into captures, and make cursor sway match cursor motion while preserving the old 2x strength at the new 1x slider position.
This commit is contained in:
Vendored
+220
-116
@@ -1,116 +1,220 @@
|
||||
/// <reference types="vite-plugin-electron/electron-env" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/**
|
||||
* The built directory structure
|
||||
*
|
||||
* ```tree
|
||||
* ├─┬─┬ dist
|
||||
* │ │ └── index.html
|
||||
* │ │
|
||||
* │ ├─┬ dist-electron
|
||||
* │ │ ├── main.js
|
||||
* │ │ └── preload.js
|
||||
* │
|
||||
* ```
|
||||
*/
|
||||
APP_ROOT: string
|
||||
/** /dist/ or /public/ */
|
||||
VITE_PUBLIC: string
|
||||
}
|
||||
}
|
||||
|
||||
// Used in Renderer process, expose in `preload.ts`
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
getSources: (opts: Electron.SourcesOptions) => Promise<ProcessedDesktopSource[]>
|
||||
switchToEditor: () => Promise<void>
|
||||
openSourceSelector: () => Promise<void>
|
||||
selectSource: (source: any) => Promise<any>
|
||||
getSelectedSource: () => Promise<any>
|
||||
startNativeScreenRecording: (
|
||||
source: any,
|
||||
options?: {
|
||||
capturesSystemAudio?: boolean
|
||||
capturesMicrophone?: boolean
|
||||
microphoneDeviceId?: string
|
||||
microphoneLabel?: string
|
||||
}
|
||||
) => Promise<{ success: boolean; path?: string; message?: string; error?: string }>
|
||||
stopNativeScreenRecording: () => Promise<{ success: boolean; path?: string; message?: string; error?: string }>
|
||||
startFfmpegRecording: (source: any) => Promise<{ success: boolean; path?: string; message?: string; error?: string }>
|
||||
stopFfmpegRecording: () => Promise<{ success: boolean; path?: string; message?: string; error?: string }>
|
||||
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }>
|
||||
getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }>
|
||||
readLocalFile: (filePath: string) => Promise<{ success: boolean; data?: Uint8Array; error?: string }>
|
||||
setRecordingState: (recording: boolean) => Promise<void>
|
||||
getCursorTelemetry: (videoPath?: string) => Promise<{ success: boolean; samples: CursorTelemetryPoint[]; message?: string; error?: string }>
|
||||
getSystemCursorAssets: () => Promise<{ success: boolean; cursors: Record<string, SystemCursorAsset>; error?: string }>
|
||||
onStopRecordingFromTray: (callback: () => void) => () => void
|
||||
onRecordingStateChanged: (callback: (state: { recording: boolean; sourceName: string }) => void) => () => void
|
||||
onRecordingInterrupted: (callback: (state: { reason: string; message: string }) => void) => () => void
|
||||
onCursorStateChanged: (callback: (state: { cursorType: CursorTelemetryPoint['cursorType'] }) => void) => () => void
|
||||
openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||
getAccessibilityPermissionStatus: () => Promise<{ success: boolean; trusted: boolean; prompted: boolean; error?: string }>
|
||||
requestAccessibilityPermission: () => Promise<{ success: boolean; trusted: boolean; prompted: boolean; error?: string }>
|
||||
getScreenRecordingPermissionStatus: () => Promise<{ success: boolean; status: string; error?: string }>
|
||||
openScreenRecordingPreferences: () => Promise<{ success: boolean; error?: string }>
|
||||
openAccessibilityPreferences: () => Promise<{ success: boolean; error?: string }>
|
||||
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean }>
|
||||
openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>
|
||||
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>
|
||||
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>
|
||||
clearCurrentVideoPath: () => Promise<{ success: boolean }>
|
||||
saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean; error?: string }>
|
||||
loadProjectFile: () => Promise<{ success: boolean; path?: string; project?: unknown; message?: string; canceled?: boolean; error?: string }>
|
||||
loadCurrentProjectFile: () => Promise<{ success: boolean; path?: string; project?: unknown; message?: string; canceled?: boolean; error?: string }>
|
||||
onMenuLoadProject: (callback: () => void) => () => void
|
||||
onMenuSaveProject: (callback: () => void) => () => void
|
||||
onMenuSaveProjectAs: (callback: () => void) => () => void
|
||||
getPlatform: () => Promise<string>
|
||||
revealInFolder: (filePath: string) => Promise<{ success: boolean; error?: string; message?: string }>,
|
||||
openRecordingsFolder: () => Promise<{ success: boolean; error?: string; message?: string }>,
|
||||
getRecordingsDirectory: () => Promise<{ success: boolean; path: string; isDefault: boolean; error?: string }>
|
||||
chooseRecordingsDirectory: () => Promise<{ success: boolean; canceled?: boolean; path?: string; isDefault?: boolean; message?: string; error?: string }>
|
||||
getShortcuts: () => Promise<Record<string, unknown> | null>
|
||||
saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>
|
||||
hudOverlayHide: () => void;
|
||||
hudOverlayClose: () => void;
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void
|
||||
onRequestSaveBeforeClose: (callback: () => Promise<void>) => () => void
|
||||
isWgcAvailable: () => Promise<{ available: boolean }>
|
||||
muxWgcRecording: () => Promise<{ success: boolean; path?: string; message?: string; error?: string }>
|
||||
/** Hide the OS cursor before browser capture starts. */
|
||||
hideOsCursor: () => Promise<{ success: boolean }>
|
||||
}
|
||||
}
|
||||
|
||||
interface ProcessedDesktopSource {
|
||||
id: string
|
||||
name: string
|
||||
display_id: string
|
||||
thumbnail: string | null
|
||||
appIcon: string | null
|
||||
originalName?: string
|
||||
sourceType?: 'screen' | 'window'
|
||||
appName?: string
|
||||
windowTitle?: string
|
||||
}
|
||||
|
||||
interface CursorTelemetryPoint {
|
||||
timeMs: number
|
||||
cx: number
|
||||
cy: number
|
||||
interactionType?: 'move' | 'click' | 'double-click' | 'right-click' | 'middle-click' | 'mouseup'
|
||||
cursorType?: 'arrow' | 'text' | 'pointer' | 'crosshair' | 'open-hand' | 'closed-hand' | 'resize-ew' | 'resize-ns' | 'not-allowed'
|
||||
}
|
||||
|
||||
interface SystemCursorAsset {
|
||||
dataUrl: string
|
||||
hotspotX: number
|
||||
hotspotY: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
/// <reference types="vite-plugin-electron/electron-env" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/**
|
||||
* The built directory structure
|
||||
*
|
||||
* ```tree
|
||||
* ├─┬─┬ dist
|
||||
* │ │ └── index.html
|
||||
* │ │
|
||||
* │ ├─┬ dist-electron
|
||||
* │ │ ├── main.js
|
||||
* │ │ └── preload.js
|
||||
* │
|
||||
* ```
|
||||
*/
|
||||
APP_ROOT: string;
|
||||
/** /dist/ or /public/ */
|
||||
VITE_PUBLIC: string;
|
||||
}
|
||||
}
|
||||
|
||||
// Used in Renderer process, expose in `preload.ts`
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
getSources: (opts: Electron.SourcesOptions) => Promise<ProcessedDesktopSource[]>;
|
||||
switchToEditor: () => Promise<void>;
|
||||
openSourceSelector: () => Promise<void>;
|
||||
selectSource: (source: any) => Promise<any>;
|
||||
getSelectedSource: () => Promise<any>;
|
||||
startNativeScreenRecording: (
|
||||
source: any,
|
||||
options?: {
|
||||
capturesSystemAudio?: boolean;
|
||||
capturesMicrophone?: boolean;
|
||||
microphoneDeviceId?: string;
|
||||
microphoneLabel?: string;
|
||||
},
|
||||
) => Promise<{ success: boolean; path?: string; message?: string; error?: string }>;
|
||||
stopNativeScreenRecording: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
startFfmpegRecording: (
|
||||
source: any,
|
||||
) => Promise<{ success: boolean; path?: string; message?: string; error?: string }>;
|
||||
stopFfmpegRecording: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
storeRecordedVideo: (
|
||||
videoData: ArrayBuffer,
|
||||
fileName: string,
|
||||
) => Promise<{ success: boolean; path?: string; message?: string }>;
|
||||
getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }>;
|
||||
readLocalFile: (
|
||||
filePath: string,
|
||||
) => Promise<{ success: boolean; data?: Uint8Array; error?: string }>;
|
||||
setRecordingState: (recording: boolean) => Promise<void>;
|
||||
getCursorTelemetry: (videoPath?: string) => Promise<{
|
||||
success: boolean;
|
||||
samples: CursorTelemetryPoint[];
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
getSystemCursorAssets: () => Promise<{
|
||||
success: boolean;
|
||||
cursors: Record<string, SystemCursorAsset>;
|
||||
error?: string;
|
||||
}>;
|
||||
onStopRecordingFromTray: (callback: () => void) => () => void;
|
||||
onRecordingStateChanged: (
|
||||
callback: (state: { recording: boolean; sourceName: string }) => void,
|
||||
) => () => void;
|
||||
onRecordingInterrupted: (
|
||||
callback: (state: { reason: string; message: string }) => void,
|
||||
) => () => void;
|
||||
onCursorStateChanged: (
|
||||
callback: (state: { cursorType: CursorTelemetryPoint["cursorType"] }) => void,
|
||||
) => () => void;
|
||||
openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>;
|
||||
getAccessibilityPermissionStatus: () => Promise<{
|
||||
success: boolean;
|
||||
trusted: boolean;
|
||||
prompted: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
requestAccessibilityPermission: () => Promise<{
|
||||
success: boolean;
|
||||
trusted: boolean;
|
||||
prompted: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
getScreenRecordingPermissionStatus: () => Promise<{
|
||||
success: boolean;
|
||||
status: string;
|
||||
error?: string;
|
||||
}>;
|
||||
openScreenRecordingPreferences: () => Promise<{ success: boolean; error?: string }>;
|
||||
openAccessibilityPreferences: () => Promise<{ success: boolean; error?: string }>;
|
||||
saveExportedVideo: (
|
||||
videoData: ArrayBuffer,
|
||||
fileName: string,
|
||||
) => Promise<{ success: boolean; path?: string; message?: string; canceled?: boolean }>;
|
||||
openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>;
|
||||
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>;
|
||||
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>;
|
||||
clearCurrentVideoPath: () => Promise<{ success: boolean }>;
|
||||
saveProjectFile: (
|
||||
projectData: unknown,
|
||||
suggestedName?: string,
|
||||
existingProjectPath?: string,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
loadProjectFile: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
project?: unknown;
|
||||
message?: string;
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
loadCurrentProjectFile: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
project?: unknown;
|
||||
message?: string;
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
onMenuLoadProject: (callback: () => void) => () => void;
|
||||
onMenuSaveProject: (callback: () => void) => () => void;
|
||||
onMenuSaveProjectAs: (callback: () => void) => () => void;
|
||||
getPlatform: () => Promise<string>;
|
||||
revealInFolder: (
|
||||
filePath: string,
|
||||
) => Promise<{ success: boolean; error?: string; message?: string }>;
|
||||
openRecordingsFolder: () => Promise<{ success: boolean; error?: string; message?: string }>;
|
||||
getRecordingsDirectory: () => Promise<{
|
||||
success: boolean;
|
||||
path: string;
|
||||
isDefault: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
chooseRecordingsDirectory: () => Promise<{
|
||||
success: boolean;
|
||||
canceled?: boolean;
|
||||
path?: string;
|
||||
isDefault?: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
getShortcuts: () => Promise<Record<string, unknown> | null>;
|
||||
saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>;
|
||||
hudOverlayHide: () => void;
|
||||
hudOverlayClose: () => void;
|
||||
getHudOverlayCaptureProtection: () => Promise<{ success: boolean; enabled: boolean }>;
|
||||
setHudOverlayCaptureProtection: (
|
||||
enabled: boolean,
|
||||
) => Promise<{ success: boolean; enabled: boolean }>;
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
onRequestSaveBeforeClose: (callback: () => Promise<void>) => () => void;
|
||||
isWgcAvailable: () => Promise<{ available: boolean }>;
|
||||
muxWgcRecording: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
/** Hide the OS cursor before browser capture starts. */
|
||||
hideOsCursor: () => Promise<{ success: boolean }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProcessedDesktopSource {
|
||||
id: string;
|
||||
name: string;
|
||||
display_id: string;
|
||||
thumbnail: string | null;
|
||||
appIcon: string | null;
|
||||
originalName?: string;
|
||||
sourceType?: "screen" | "window";
|
||||
appName?: string;
|
||||
windowTitle?: string;
|
||||
}
|
||||
|
||||
interface CursorTelemetryPoint {
|
||||
timeMs: number;
|
||||
cx: number;
|
||||
cy: number;
|
||||
interactionType?: "move" | "click" | "double-click" | "right-click" | "middle-click" | "mouseup";
|
||||
cursorType?:
|
||||
| "arrow"
|
||||
| "text"
|
||||
| "pointer"
|
||||
| "crosshair"
|
||||
| "open-hand"
|
||||
| "closed-hand"
|
||||
| "resize-ew"
|
||||
| "resize-ns"
|
||||
| "not-allowed";
|
||||
}
|
||||
|
||||
interface SystemCursorAsset {
|
||||
dataUrl: string;
|
||||
hotspotX: number;
|
||||
hotspotY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
+201
-183
@@ -1,186 +1,204 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
hudOverlayHide: () => {
|
||||
ipcRenderer.send('hud-overlay-hide');
|
||||
},
|
||||
hudOverlayClose: () => {
|
||||
ipcRenderer.send('hud-overlay-close');
|
||||
},
|
||||
getAssetBasePath: async () => {
|
||||
// ask main process for the correct base path (production vs dev)
|
||||
return await ipcRenderer.invoke('get-asset-base-path')
|
||||
},
|
||||
readLocalFile: (filePath: string) => {
|
||||
return ipcRenderer.invoke('read-local-file', filePath)
|
||||
},
|
||||
getSources: async (opts: Electron.SourcesOptions) => {
|
||||
return await ipcRenderer.invoke('get-sources', opts)
|
||||
},
|
||||
switchToEditor: () => {
|
||||
return ipcRenderer.invoke('switch-to-editor')
|
||||
},
|
||||
openSourceSelector: () => {
|
||||
return ipcRenderer.invoke('open-source-selector')
|
||||
},
|
||||
selectSource: (source: any) => {
|
||||
return ipcRenderer.invoke('select-source', source)
|
||||
},
|
||||
getSelectedSource: () => {
|
||||
return ipcRenderer.invoke('get-selected-source')
|
||||
},
|
||||
startNativeScreenRecording: (
|
||||
source: any,
|
||||
options?: {
|
||||
capturesSystemAudio?: boolean
|
||||
capturesMicrophone?: boolean
|
||||
microphoneDeviceId?: string
|
||||
microphoneLabel?: string
|
||||
},
|
||||
) => {
|
||||
return ipcRenderer.invoke('start-native-screen-recording', source, options)
|
||||
},
|
||||
stopNativeScreenRecording: () => {
|
||||
return ipcRenderer.invoke('stop-native-screen-recording')
|
||||
},
|
||||
startFfmpegRecording: (source: any) => {
|
||||
return ipcRenderer.invoke('start-ffmpeg-recording', source)
|
||||
},
|
||||
stopFfmpegRecording: () => {
|
||||
return ipcRenderer.invoke('stop-ffmpeg-recording')
|
||||
},
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
hudOverlayHide: () => {
|
||||
ipcRenderer.send("hud-overlay-hide");
|
||||
},
|
||||
hudOverlayClose: () => {
|
||||
ipcRenderer.send("hud-overlay-close");
|
||||
},
|
||||
getHudOverlayCaptureProtection: () => {
|
||||
return ipcRenderer.invoke("get-hud-overlay-capture-protection");
|
||||
},
|
||||
setHudOverlayCaptureProtection: (enabled: boolean) => {
|
||||
return ipcRenderer.invoke("set-hud-overlay-capture-protection", enabled);
|
||||
},
|
||||
getAssetBasePath: async () => {
|
||||
// ask main process for the correct base path (production vs dev)
|
||||
return await ipcRenderer.invoke("get-asset-base-path");
|
||||
},
|
||||
readLocalFile: (filePath: string) => {
|
||||
return ipcRenderer.invoke("read-local-file", filePath);
|
||||
},
|
||||
getSources: async (opts: Electron.SourcesOptions) => {
|
||||
return await ipcRenderer.invoke("get-sources", opts);
|
||||
},
|
||||
switchToEditor: () => {
|
||||
return ipcRenderer.invoke("switch-to-editor");
|
||||
},
|
||||
openSourceSelector: () => {
|
||||
return ipcRenderer.invoke("open-source-selector");
|
||||
},
|
||||
selectSource: (source: any) => {
|
||||
return ipcRenderer.invoke("select-source", source);
|
||||
},
|
||||
getSelectedSource: () => {
|
||||
return ipcRenderer.invoke("get-selected-source");
|
||||
},
|
||||
startNativeScreenRecording: (
|
||||
source: any,
|
||||
options?: {
|
||||
capturesSystemAudio?: boolean;
|
||||
capturesMicrophone?: boolean;
|
||||
microphoneDeviceId?: string;
|
||||
microphoneLabel?: string;
|
||||
},
|
||||
) => {
|
||||
return ipcRenderer.invoke("start-native-screen-recording", source, options);
|
||||
},
|
||||
stopNativeScreenRecording: () => {
|
||||
return ipcRenderer.invoke("stop-native-screen-recording");
|
||||
},
|
||||
startFfmpegRecording: (source: any) => {
|
||||
return ipcRenderer.invoke("start-ffmpeg-recording", source);
|
||||
},
|
||||
stopFfmpegRecording: () => {
|
||||
return ipcRenderer.invoke("stop-ffmpeg-recording");
|
||||
},
|
||||
|
||||
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => {
|
||||
return ipcRenderer.invoke('store-recorded-video', videoData, fileName)
|
||||
},
|
||||
|
||||
getRecordedVideoPath: () => {
|
||||
return ipcRenderer.invoke('get-recorded-video-path')
|
||||
},
|
||||
setRecordingState: (recording: boolean) => {
|
||||
return ipcRenderer.invoke('set-recording-state', recording)
|
||||
},
|
||||
setCursorScale: (scale: number) => {
|
||||
return ipcRenderer.invoke('set-cursor-scale', scale)
|
||||
},
|
||||
getCursorTelemetry: (videoPath?: string) => {
|
||||
return ipcRenderer.invoke('get-cursor-telemetry', videoPath)
|
||||
},
|
||||
getSystemCursorAssets: () => {
|
||||
return ipcRenderer.invoke('get-system-cursor-assets')
|
||||
},
|
||||
onStopRecordingFromTray: (callback: () => void) => {
|
||||
const listener = () => callback()
|
||||
ipcRenderer.on('stop-recording-from-tray', listener)
|
||||
return () => ipcRenderer.removeListener('stop-recording-from-tray', listener)
|
||||
},
|
||||
onRecordingStateChanged: (callback: (state: { recording: boolean; sourceName: string }) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, payload: { recording: boolean; sourceName: string }) => callback(payload)
|
||||
ipcRenderer.on('recording-state-changed', listener)
|
||||
return () => ipcRenderer.removeListener('recording-state-changed', listener)
|
||||
},
|
||||
onRecordingInterrupted: (callback: (state: { reason: string; message: string }) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, payload: { reason: string; message: string }) => callback(payload)
|
||||
ipcRenderer.on('recording-interrupted', listener)
|
||||
return () => ipcRenderer.removeListener('recording-interrupted', listener)
|
||||
},
|
||||
onCursorStateChanged: (callback: (state: { cursorType: CursorTelemetryPoint['cursorType'] }) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, payload: { cursorType: CursorTelemetryPoint['cursorType'] }) => callback(payload)
|
||||
ipcRenderer.on('cursor-state-changed', listener)
|
||||
return () => ipcRenderer.removeListener('cursor-state-changed', listener)
|
||||
},
|
||||
openExternalUrl: (url: string) => {
|
||||
return ipcRenderer.invoke('open-external-url', url)
|
||||
},
|
||||
getAccessibilityPermissionStatus: () => {
|
||||
return ipcRenderer.invoke('get-accessibility-permission-status')
|
||||
},
|
||||
requestAccessibilityPermission: () => {
|
||||
return ipcRenderer.invoke('request-accessibility-permission')
|
||||
},
|
||||
getScreenRecordingPermissionStatus: () => {
|
||||
return ipcRenderer.invoke('get-screen-recording-permission-status')
|
||||
},
|
||||
openScreenRecordingPreferences: () => {
|
||||
return ipcRenderer.invoke('open-screen-recording-preferences')
|
||||
},
|
||||
openAccessibilityPreferences: () => {
|
||||
return ipcRenderer.invoke('open-accessibility-preferences')
|
||||
},
|
||||
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => {
|
||||
return ipcRenderer.invoke('save-exported-video', videoData, fileName)
|
||||
},
|
||||
openVideoFilePicker: () => {
|
||||
return ipcRenderer.invoke('open-video-file-picker')
|
||||
},
|
||||
setCurrentVideoPath: (path: string) => {
|
||||
return ipcRenderer.invoke('set-current-video-path', path)
|
||||
},
|
||||
getCurrentVideoPath: () => {
|
||||
return ipcRenderer.invoke('get-current-video-path')
|
||||
},
|
||||
clearCurrentVideoPath: () => {
|
||||
return ipcRenderer.invoke('clear-current-video-path')
|
||||
},
|
||||
saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => {
|
||||
return ipcRenderer.invoke('save-project-file', projectData, suggestedName, existingProjectPath)
|
||||
},
|
||||
loadProjectFile: () => {
|
||||
return ipcRenderer.invoke('load-project-file')
|
||||
},
|
||||
loadCurrentProjectFile: () => {
|
||||
return ipcRenderer.invoke('load-current-project-file')
|
||||
},
|
||||
onMenuLoadProject: (callback: () => void) => {
|
||||
const listener = () => callback()
|
||||
ipcRenderer.on('menu-load-project', listener)
|
||||
return () => ipcRenderer.removeListener('menu-load-project', listener)
|
||||
},
|
||||
onMenuSaveProject: (callback: () => void) => {
|
||||
const listener = () => callback()
|
||||
ipcRenderer.on('menu-save-project', listener)
|
||||
return () => ipcRenderer.removeListener('menu-save-project', listener)
|
||||
},
|
||||
onMenuSaveProjectAs: (callback: () => void) => {
|
||||
const listener = () => callback()
|
||||
ipcRenderer.on('menu-save-project-as', listener)
|
||||
return () => ipcRenderer.removeListener('menu-save-project-as', listener)
|
||||
},
|
||||
getPlatform: () => {
|
||||
return ipcRenderer.invoke('get-platform')
|
||||
},
|
||||
revealInFolder: (filePath: string) => {
|
||||
return ipcRenderer.invoke('reveal-in-folder', filePath)
|
||||
},
|
||||
openRecordingsFolder: () => {
|
||||
return ipcRenderer.invoke('open-recordings-folder')
|
||||
},
|
||||
getRecordingsDirectory: () => {
|
||||
return ipcRenderer.invoke('get-recordings-directory')
|
||||
},
|
||||
chooseRecordingsDirectory: () => {
|
||||
return ipcRenderer.invoke('choose-recordings-directory')
|
||||
},
|
||||
getShortcuts: () => {
|
||||
return ipcRenderer.invoke('get-shortcuts')
|
||||
},
|
||||
saveShortcuts: (shortcuts: unknown) => {
|
||||
return ipcRenderer.invoke('save-shortcuts', shortcuts)
|
||||
},
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => {
|
||||
ipcRenderer.send('set-has-unsaved-changes', hasChanges)
|
||||
},
|
||||
onRequestSaveBeforeClose: (callback: () => Promise<void>) => {
|
||||
const listener = async () => {
|
||||
await callback()
|
||||
ipcRenderer.send('save-before-close-done')
|
||||
}
|
||||
ipcRenderer.on('request-save-before-close', listener)
|
||||
return () => ipcRenderer.removeListener('request-save-before-close', listener)
|
||||
},
|
||||
isWgcAvailable: () => ipcRenderer.invoke('is-wgc-available'),
|
||||
muxWgcRecording: () => ipcRenderer.invoke('mux-wgc-recording'),
|
||||
// Cursor visibility control for cursor-free browser capture fallback
|
||||
hideOsCursor: () => ipcRenderer.invoke('hide-cursor'),
|
||||
})
|
||||
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => {
|
||||
return ipcRenderer.invoke("store-recorded-video", videoData, fileName);
|
||||
},
|
||||
|
||||
getRecordedVideoPath: () => {
|
||||
return ipcRenderer.invoke("get-recorded-video-path");
|
||||
},
|
||||
setRecordingState: (recording: boolean) => {
|
||||
return ipcRenderer.invoke("set-recording-state", recording);
|
||||
},
|
||||
setCursorScale: (scale: number) => {
|
||||
return ipcRenderer.invoke("set-cursor-scale", scale);
|
||||
},
|
||||
getCursorTelemetry: (videoPath?: string) => {
|
||||
return ipcRenderer.invoke("get-cursor-telemetry", videoPath);
|
||||
},
|
||||
getSystemCursorAssets: () => {
|
||||
return ipcRenderer.invoke("get-system-cursor-assets");
|
||||
},
|
||||
onStopRecordingFromTray: (callback: () => void) => {
|
||||
const listener = () => callback();
|
||||
ipcRenderer.on("stop-recording-from-tray", listener);
|
||||
return () => ipcRenderer.removeListener("stop-recording-from-tray", listener);
|
||||
},
|
||||
onRecordingStateChanged: (
|
||||
callback: (state: { recording: boolean; sourceName: string }) => void,
|
||||
) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
payload: { recording: boolean; sourceName: string },
|
||||
) => callback(payload);
|
||||
ipcRenderer.on("recording-state-changed", listener);
|
||||
return () => ipcRenderer.removeListener("recording-state-changed", listener);
|
||||
},
|
||||
onRecordingInterrupted: (callback: (state: { reason: string; message: string }) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
payload: { reason: string; message: string },
|
||||
) => callback(payload);
|
||||
ipcRenderer.on("recording-interrupted", listener);
|
||||
return () => ipcRenderer.removeListener("recording-interrupted", listener);
|
||||
},
|
||||
onCursorStateChanged: (
|
||||
callback: (state: { cursorType: CursorTelemetryPoint["cursorType"] }) => void,
|
||||
) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
payload: { cursorType: CursorTelemetryPoint["cursorType"] },
|
||||
) => callback(payload);
|
||||
ipcRenderer.on("cursor-state-changed", listener);
|
||||
return () => ipcRenderer.removeListener("cursor-state-changed", listener);
|
||||
},
|
||||
openExternalUrl: (url: string) => {
|
||||
return ipcRenderer.invoke("open-external-url", url);
|
||||
},
|
||||
getAccessibilityPermissionStatus: () => {
|
||||
return ipcRenderer.invoke("get-accessibility-permission-status");
|
||||
},
|
||||
requestAccessibilityPermission: () => {
|
||||
return ipcRenderer.invoke("request-accessibility-permission");
|
||||
},
|
||||
getScreenRecordingPermissionStatus: () => {
|
||||
return ipcRenderer.invoke("get-screen-recording-permission-status");
|
||||
},
|
||||
openScreenRecordingPreferences: () => {
|
||||
return ipcRenderer.invoke("open-screen-recording-preferences");
|
||||
},
|
||||
openAccessibilityPreferences: () => {
|
||||
return ipcRenderer.invoke("open-accessibility-preferences");
|
||||
},
|
||||
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => {
|
||||
return ipcRenderer.invoke("save-exported-video", videoData, fileName);
|
||||
},
|
||||
openVideoFilePicker: () => {
|
||||
return ipcRenderer.invoke("open-video-file-picker");
|
||||
},
|
||||
setCurrentVideoPath: (path: string) => {
|
||||
return ipcRenderer.invoke("set-current-video-path", path);
|
||||
},
|
||||
getCurrentVideoPath: () => {
|
||||
return ipcRenderer.invoke("get-current-video-path");
|
||||
},
|
||||
clearCurrentVideoPath: () => {
|
||||
return ipcRenderer.invoke("clear-current-video-path");
|
||||
},
|
||||
saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => {
|
||||
return ipcRenderer.invoke("save-project-file", projectData, suggestedName, existingProjectPath);
|
||||
},
|
||||
loadProjectFile: () => {
|
||||
return ipcRenderer.invoke("load-project-file");
|
||||
},
|
||||
loadCurrentProjectFile: () => {
|
||||
return ipcRenderer.invoke("load-current-project-file");
|
||||
},
|
||||
onMenuLoadProject: (callback: () => void) => {
|
||||
const listener = () => callback();
|
||||
ipcRenderer.on("menu-load-project", listener);
|
||||
return () => ipcRenderer.removeListener("menu-load-project", listener);
|
||||
},
|
||||
onMenuSaveProject: (callback: () => void) => {
|
||||
const listener = () => callback();
|
||||
ipcRenderer.on("menu-save-project", listener);
|
||||
return () => ipcRenderer.removeListener("menu-save-project", listener);
|
||||
},
|
||||
onMenuSaveProjectAs: (callback: () => void) => {
|
||||
const listener = () => callback();
|
||||
ipcRenderer.on("menu-save-project-as", listener);
|
||||
return () => ipcRenderer.removeListener("menu-save-project-as", listener);
|
||||
},
|
||||
getPlatform: () => {
|
||||
return ipcRenderer.invoke("get-platform");
|
||||
},
|
||||
revealInFolder: (filePath: string) => {
|
||||
return ipcRenderer.invoke("reveal-in-folder", filePath);
|
||||
},
|
||||
openRecordingsFolder: () => {
|
||||
return ipcRenderer.invoke("open-recordings-folder");
|
||||
},
|
||||
getRecordingsDirectory: () => {
|
||||
return ipcRenderer.invoke("get-recordings-directory");
|
||||
},
|
||||
chooseRecordingsDirectory: () => {
|
||||
return ipcRenderer.invoke("choose-recordings-directory");
|
||||
},
|
||||
getShortcuts: () => {
|
||||
return ipcRenderer.invoke("get-shortcuts");
|
||||
},
|
||||
saveShortcuts: (shortcuts: unknown) => {
|
||||
return ipcRenderer.invoke("save-shortcuts", shortcuts);
|
||||
},
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => {
|
||||
ipcRenderer.send("set-has-unsaved-changes", hasChanges);
|
||||
},
|
||||
onRequestSaveBeforeClose: (callback: () => Promise<void>) => {
|
||||
const listener = async () => {
|
||||
await callback();
|
||||
ipcRenderer.send("save-before-close-done");
|
||||
};
|
||||
ipcRenderer.on("request-save-before-close", listener);
|
||||
return () => ipcRenderer.removeListener("request-save-before-close", listener);
|
||||
},
|
||||
isWgcAvailable: () => ipcRenderer.invoke("is-wgc-available"),
|
||||
muxWgcRecording: () => ipcRenderer.invoke("mux-wgc-recording"),
|
||||
// Cursor visibility control for cursor-free browser capture fallback
|
||||
hideOsCursor: () => ipcRenderer.invoke("hide-cursor"),
|
||||
});
|
||||
|
||||
+157
-134
@@ -1,168 +1,191 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const nodeRequire = createRequire(import.meta.url)
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
|
||||
const APP_ROOT = path.join(__dirname, '..')
|
||||
const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
|
||||
const RENDERER_DIST = path.join(APP_ROOT, 'dist')
|
||||
const WINDOW_ICON_PATH = path.join(process.env.VITE_PUBLIC || RENDERER_DIST, 'app-icons', 'recordly-512.png')
|
||||
const APP_ROOT = path.join(__dirname, "..");
|
||||
const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"];
|
||||
const RENDERER_DIST = path.join(APP_ROOT, "dist");
|
||||
const WINDOW_ICON_PATH = path.join(
|
||||
process.env.VITE_PUBLIC || RENDERER_DIST,
|
||||
"app-icons",
|
||||
"recordly-512.png",
|
||||
);
|
||||
|
||||
let hudOverlayWindow: BrowserWindow | null = null;
|
||||
let hudOverlayHiddenFromCapture = false;
|
||||
|
||||
function getScreen() {
|
||||
return nodeRequire('electron').screen as typeof import('electron').screen
|
||||
return nodeRequire("electron").screen as typeof import("electron").screen;
|
||||
}
|
||||
|
||||
ipcMain.on('hud-overlay-hide', () => {
|
||||
if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) {
|
||||
hudOverlayWindow.minimize();
|
||||
}
|
||||
ipcMain.on("hud-overlay-hide", () => {
|
||||
if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) {
|
||||
hudOverlayWindow.minimize();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-hud-overlay-capture-protection", () => {
|
||||
return {
|
||||
success: true,
|
||||
enabled: hudOverlayHiddenFromCapture,
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) => {
|
||||
hudOverlayHiddenFromCapture = Boolean(enabled);
|
||||
|
||||
if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) {
|
||||
hudOverlayWindow.setContentProtection(hudOverlayHiddenFromCapture);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
enabled: hudOverlayHiddenFromCapture,
|
||||
};
|
||||
});
|
||||
|
||||
export function createHudOverlayWindow(): BrowserWindow {
|
||||
const primaryDisplay = getScreen().getPrimaryDisplay();
|
||||
const { workArea } = primaryDisplay;
|
||||
const primaryDisplay = getScreen().getPrimaryDisplay();
|
||||
const { workArea } = primaryDisplay;
|
||||
|
||||
const windowWidth = 660;
|
||||
const windowHeight = 170;
|
||||
|
||||
const windowWidth = 600;
|
||||
const windowHeight = 155;
|
||||
const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2);
|
||||
const y = Math.floor(workArea.y + workArea.height - windowHeight - 5);
|
||||
|
||||
const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2);
|
||||
const y = Math.floor(workArea.y + workArea.height - windowHeight - 5);
|
||||
const win = new BrowserWindow({
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
minWidth: windowWidth,
|
||||
maxWidth: windowWidth,
|
||||
minHeight: windowHeight,
|
||||
maxHeight: windowHeight,
|
||||
x: x,
|
||||
y: y,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
hasShadow: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.mjs"),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
});
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
minWidth: 600,
|
||||
maxWidth: 600,
|
||||
minHeight: 155,
|
||||
maxHeight: 155,
|
||||
x: x,
|
||||
y: y,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
hasShadow: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.mjs'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
})
|
||||
win.setContentProtection(hudOverlayHiddenFromCapture);
|
||||
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||
});
|
||||
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
win?.webContents.send('main-process-message', (new Date).toLocaleString())
|
||||
})
|
||||
hudOverlayWindow = win;
|
||||
|
||||
hudOverlayWindow = win;
|
||||
win.on("closed", () => {
|
||||
if (hudOverlayWindow === win) {
|
||||
hudOverlayWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
win.on('closed', () => {
|
||||
if (hudOverlayWindow === win) {
|
||||
hudOverlayWindow = null;
|
||||
}
|
||||
});
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=hud-overlay");
|
||||
} else {
|
||||
win.loadFile(path.join(RENDERER_DIST, "index.html"), {
|
||||
query: { windowType: "hud-overlay" },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL + '?windowType=hud-overlay')
|
||||
} else {
|
||||
win.loadFile(path.join(RENDERER_DIST, 'index.html'), {
|
||||
query: { windowType: 'hud-overlay' }
|
||||
})
|
||||
}
|
||||
|
||||
return win
|
||||
return win;
|
||||
}
|
||||
|
||||
export function createEditorWindow(): BrowserWindow {
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
...(process.platform !== 'darwin' && {
|
||||
icon: WINDOW_ICON_PATH,
|
||||
}),
|
||||
...(isMac && {
|
||||
titleBarStyle: 'hiddenInset',
|
||||
trafficLightPosition: { x: 12, y: 12 },
|
||||
}),
|
||||
transparent: false,
|
||||
resizable: true,
|
||||
alwaysOnTop: false,
|
||||
skipTaskbar: false,
|
||||
title: 'Recordly',
|
||||
backgroundColor: '#000000',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.mjs'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
})
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
...(process.platform !== "darwin" && {
|
||||
icon: WINDOW_ICON_PATH,
|
||||
}),
|
||||
...(isMac && {
|
||||
titleBarStyle: "hiddenInset",
|
||||
trafficLightPosition: { x: 12, y: 12 },
|
||||
}),
|
||||
transparent: false,
|
||||
resizable: true,
|
||||
alwaysOnTop: false,
|
||||
skipTaskbar: false,
|
||||
title: "Recordly",
|
||||
backgroundColor: "#000000",
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.mjs"),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Maximize the window by default
|
||||
win.maximize();
|
||||
// Maximize the window by default
|
||||
win.maximize();
|
||||
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
win?.webContents.send('main-process-message', (new Date).toLocaleString())
|
||||
})
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||
});
|
||||
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL + '?windowType=editor')
|
||||
} else {
|
||||
win.loadFile(path.join(RENDERER_DIST, 'index.html'), {
|
||||
query: { windowType: 'editor' }
|
||||
})
|
||||
}
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=editor");
|
||||
} else {
|
||||
win.loadFile(path.join(RENDERER_DIST, "index.html"), {
|
||||
query: { windowType: "editor" },
|
||||
});
|
||||
}
|
||||
|
||||
return win
|
||||
return win;
|
||||
}
|
||||
|
||||
export function createSourceSelectorWindow(): BrowserWindow {
|
||||
const { width, height } = getScreen().getPrimaryDisplay().workAreaSize
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 620,
|
||||
height: 420,
|
||||
minHeight: 350,
|
||||
maxHeight: 500,
|
||||
x: Math.round((width - 620) / 2),
|
||||
y: Math.round((height - 420) / 2),
|
||||
frame: false,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
transparent: true,
|
||||
...(process.platform !== 'darwin' && {
|
||||
icon: WINDOW_ICON_PATH,
|
||||
}),
|
||||
backgroundColor: '#00000000',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.mjs'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
})
|
||||
const { width, height } = getScreen().getPrimaryDisplay().workAreaSize;
|
||||
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL + '?windowType=source-selector')
|
||||
} else {
|
||||
win.loadFile(path.join(RENDERER_DIST, 'index.html'), {
|
||||
query: { windowType: 'source-selector' }
|
||||
})
|
||||
}
|
||||
const win = new BrowserWindow({
|
||||
width: 620,
|
||||
height: 420,
|
||||
minHeight: 350,
|
||||
maxHeight: 500,
|
||||
x: Math.round((width - 620) / 2),
|
||||
y: Math.round((height - 420) / 2),
|
||||
frame: false,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
transparent: true,
|
||||
...(process.platform !== "darwin" && {
|
||||
icon: WINDOW_ICON_PATH,
|
||||
}),
|
||||
backgroundColor: "#00000000",
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.mjs"),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
|
||||
return win
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=source-selector");
|
||||
} else {
|
||||
win.loadFile(path.join(RENDERER_DIST, "index.html"), {
|
||||
query: { windowType: "source-selector" },
|
||||
});
|
||||
}
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,355 +1,441 @@
|
||||
import { Eye, EyeOff, Languages } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BsRecordCircle } from "react-icons/bs";
|
||||
import { FaRegStopCircle } from "react-icons/fa";
|
||||
import { FaFolderOpen } from "react-icons/fa6";
|
||||
import { FiMinus, FiX } from "react-icons/fi";
|
||||
import { MdMic, MdMicOff, MdMonitor, MdVideoFile, MdVolumeOff, MdVolumeUp } from "react-icons/md";
|
||||
import { Languages } from "lucide-react";
|
||||
import { RxDragHandleDots2 } from "react-icons/rx";
|
||||
import { useI18n } from "@/contexts/I18nContext";
|
||||
import type { AppLocale } from "@/i18n/config";
|
||||
import { SUPPORTED_LOCALES } from "@/i18n/config";
|
||||
import { useScopedT } from "../../contexts/I18nContext";
|
||||
import { useAudioLevelMeter } from "../../hooks/useAudioLevelMeter";
|
||||
import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices";
|
||||
import { useScreenRecorder } from "../../hooks/useScreenRecorder";
|
||||
import { useScopedT } from "../../contexts/I18nContext";
|
||||
import { Button } from "../ui/button";
|
||||
import { AudioLevelMeter } from "../ui/audio-level-meter";
|
||||
import { Button } from "../ui/button";
|
||||
import { ContentClamp } from "../ui/content-clamp";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { useI18n } from "@/contexts/I18nContext";
|
||||
import { SUPPORTED_LOCALES } from "@/i18n/config";
|
||||
import type { AppLocale } from "@/i18n/config";
|
||||
import styles from "./LaunchWindow.module.css";
|
||||
|
||||
export function LaunchWindow() {
|
||||
const { locale, setLocale } = useI18n();
|
||||
const t = useScopedT('launch');
|
||||
const { locale, setLocale } = useI18n();
|
||||
const t = useScopedT("launch");
|
||||
|
||||
const LOCALE_LABELS: Record<string, string> = { en: "EN", es: "ES", "zh-CN": "中文" };
|
||||
const {
|
||||
recording,
|
||||
toggleRecording,
|
||||
microphoneEnabled,
|
||||
setMicrophoneEnabled,
|
||||
microphoneDeviceId,
|
||||
setMicrophoneDeviceId,
|
||||
systemAudioEnabled,
|
||||
setSystemAudioEnabled,
|
||||
} = useScreenRecorder();
|
||||
const [recordingStart, setRecordingStart] = useState<number | null>(null);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
const showMicControls = microphoneEnabled && !recording;
|
||||
const { devices, selectedDeviceId, setSelectedDeviceId } = useMicrophoneDevices(microphoneEnabled);
|
||||
const { level } = useAudioLevelMeter({
|
||||
enabled: showMicControls,
|
||||
deviceId: microphoneDeviceId,
|
||||
});
|
||||
const LOCALE_LABELS: Record<string, string> = { en: "EN", es: "ES", "zh-CN": "中文" };
|
||||
const {
|
||||
recording,
|
||||
toggleRecording,
|
||||
microphoneEnabled,
|
||||
setMicrophoneEnabled,
|
||||
microphoneDeviceId,
|
||||
setMicrophoneDeviceId,
|
||||
systemAudioEnabled,
|
||||
setSystemAudioEnabled,
|
||||
} = useScreenRecorder();
|
||||
const [recordingStart, setRecordingStart] = useState<number | null>(null);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
const showMicControls = microphoneEnabled && !recording;
|
||||
const { devices, selectedDeviceId, setSelectedDeviceId } =
|
||||
useMicrophoneDevices(microphoneEnabled);
|
||||
const { level } = useAudioLevelMeter({
|
||||
enabled: showMicControls,
|
||||
deviceId: microphoneDeviceId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDeviceId && selectedDeviceId !== "default") {
|
||||
setMicrophoneDeviceId(selectedDeviceId);
|
||||
}
|
||||
}, [selectedDeviceId, setMicrophoneDeviceId]);
|
||||
useEffect(() => {
|
||||
if (selectedDeviceId && selectedDeviceId !== "default") {
|
||||
setMicrophoneDeviceId(selectedDeviceId);
|
||||
}
|
||||
}, [selectedDeviceId, setMicrophoneDeviceId]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
if (recording) {
|
||||
if (!recordingStart) setRecordingStart(Date.now());
|
||||
timer = setInterval(() => {
|
||||
if (recordingStart) {
|
||||
setElapsed(Math.floor((Date.now() - recordingStart) / 1000));
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
setRecordingStart(null);
|
||||
setElapsed(0);
|
||||
if (timer) clearInterval(timer);
|
||||
}
|
||||
return () => {
|
||||
if (timer) clearInterval(timer);
|
||||
};
|
||||
}, [recording, recordingStart]);
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
if (recording) {
|
||||
if (!recordingStart) setRecordingStart(Date.now());
|
||||
timer = setInterval(() => {
|
||||
if (recordingStart) {
|
||||
setElapsed(Math.floor((Date.now() - recordingStart) / 1000));
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
setRecordingStart(null);
|
||||
setElapsed(0);
|
||||
if (timer) clearInterval(timer);
|
||||
}
|
||||
return () => {
|
||||
if (timer) clearInterval(timer);
|
||||
};
|
||||
}, [recording, recordingStart]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const m = Math.floor(seconds / 60).toString().padStart(2, "0");
|
||||
const s = (seconds % 60).toString().padStart(2, "0");
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
const formatTime = (seconds: number) => {
|
||||
const m = Math.floor(seconds / 60)
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
const s = (seconds % 60).toString().padStart(2, "0");
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
const [selectedSource, setSelectedSource] = useState("Screen");
|
||||
const [hasSelectedSource, setHasSelectedSource] = useState(false);
|
||||
const [recordingsDirectory, setRecordingsDirectory] = useState<string | null>(null);
|
||||
const [selectedSource, setSelectedSource] = useState("Screen");
|
||||
const [hasSelectedSource, setHasSelectedSource] = useState(false);
|
||||
const [recordingsDirectory, setRecordingsDirectory] = useState<string | null>(null);
|
||||
const [hideHudFromCapture, setHideHudFromCapture] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkSelectedSource = async () => {
|
||||
if (window.electronAPI) {
|
||||
const source = await window.electronAPI.getSelectedSource();
|
||||
if (source) {
|
||||
setSelectedSource(source.name);
|
||||
setHasSelectedSource(true);
|
||||
} else {
|
||||
setSelectedSource("Screen");
|
||||
setHasSelectedSource(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const checkSelectedSource = async () => {
|
||||
if (window.electronAPI) {
|
||||
const source = await window.electronAPI.getSelectedSource();
|
||||
if (source) {
|
||||
setSelectedSource(source.name);
|
||||
setHasSelectedSource(true);
|
||||
} else {
|
||||
setSelectedSource("Screen");
|
||||
setHasSelectedSource(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void checkSelectedSource();
|
||||
const interval = setInterval(checkSelectedSource, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
void checkSelectedSource();
|
||||
const interval = setInterval(checkSelectedSource, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const openSourceSelector = () => {
|
||||
window.electronAPI?.openSourceSelector();
|
||||
};
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const openVideoFile = async () => {
|
||||
const result = await window.electronAPI.openVideoFilePicker();
|
||||
if (result.canceled) {
|
||||
return;
|
||||
}
|
||||
const loadHudCaptureProtection = async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.getHudOverlayCaptureProtection();
|
||||
if (!cancelled && result.success) {
|
||||
setHideHudFromCapture(result.enabled);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load HUD capture protection state:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (result.success && result.path) {
|
||||
await window.electronAPI.setCurrentVideoPath(result.path);
|
||||
await window.electronAPI.switchToEditor();
|
||||
}
|
||||
};
|
||||
void loadHudCaptureProtection();
|
||||
|
||||
const openProjectFile = async () => {
|
||||
const result = await window.electronAPI.loadProjectFile();
|
||||
if (result.canceled || !result.success) {
|
||||
return;
|
||||
}
|
||||
await window.electronAPI.switchToEditor();
|
||||
};
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sendHudOverlayHide = () => {
|
||||
window.electronAPI?.hudOverlayHide?.();
|
||||
};
|
||||
const openSourceSelector = () => {
|
||||
window.electronAPI?.openSourceSelector();
|
||||
};
|
||||
|
||||
const sendHudOverlayClose = () => {
|
||||
window.electronAPI?.hudOverlayClose?.();
|
||||
};
|
||||
const openVideoFile = async () => {
|
||||
const result = await window.electronAPI.openVideoFilePicker();
|
||||
if (result.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chooseRecordingsDirectory = async () => {
|
||||
const result = await window.electronAPI.chooseRecordingsDirectory();
|
||||
if (result.canceled) {
|
||||
return;
|
||||
}
|
||||
if (result.success && result.path) {
|
||||
setRecordingsDirectory(result.path);
|
||||
}
|
||||
};
|
||||
if (result.success && result.path) {
|
||||
await window.electronAPI.setCurrentVideoPath(result.path);
|
||||
await window.electronAPI.switchToEditor();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadRecordingsDirectory = async () => {
|
||||
const result = await window.electronAPI.getRecordingsDirectory();
|
||||
if (result.success) {
|
||||
setRecordingsDirectory(result.path);
|
||||
}
|
||||
};
|
||||
const openProjectFile = async () => {
|
||||
const result = await window.electronAPI.loadProjectFile();
|
||||
if (result.canceled || !result.success) {
|
||||
return;
|
||||
}
|
||||
await window.electronAPI.switchToEditor();
|
||||
};
|
||||
|
||||
void loadRecordingsDirectory();
|
||||
}, []);
|
||||
const sendHudOverlayHide = () => {
|
||||
window.electronAPI?.hudOverlayHide?.();
|
||||
};
|
||||
|
||||
const recordingsDirectoryName = recordingsDirectory
|
||||
? recordingsDirectory.split(/[\\/]/).filter(Boolean).pop() || recordingsDirectory
|
||||
: "recordings";
|
||||
const dividerClass = "mx-1 h-5 w-px shrink-0 bg-white/35";
|
||||
const sendHudOverlayClose = () => {
|
||||
window.electronAPI?.hudOverlayClose?.();
|
||||
};
|
||||
|
||||
const toggleMicrophone = () => {
|
||||
if (!recording) {
|
||||
setMicrophoneEnabled(!microphoneEnabled);
|
||||
}
|
||||
};
|
||||
const toggleHudCaptureProtection = async () => {
|
||||
const nextValue = !hideHudFromCapture;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-end justify-center bg-transparent overflow-hidden">
|
||||
<div className={`flex flex-col items-center gap-2 mx-auto ${styles.electronDrag}`}>
|
||||
{showMicControls && (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-full border border-white/15 bg-[rgba(18,18,26,0.92)] px-3 py-2 shadow-xl backdrop-blur-xl ${styles.electronNoDrag}`}
|
||||
>
|
||||
<select
|
||||
value={microphoneDeviceId || selectedDeviceId}
|
||||
onChange={(event) => {
|
||||
setSelectedDeviceId(event.target.value);
|
||||
setMicrophoneDeviceId(event.target.value);
|
||||
}}
|
||||
className={`max-w-[230px] rounded-full border border-white/15 bg-[#131722] px-3 py-1 text-xs text-slate-100 outline-none ${styles.micSelect}`}
|
||||
>
|
||||
{devices.map((device) => (
|
||||
<option key={device.deviceId} value={device.deviceId}>
|
||||
{device.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<AudioLevelMeter level={level} className="w-24" />
|
||||
</div>
|
||||
)}
|
||||
setHideHudFromCapture(nextValue);
|
||||
|
||||
<div
|
||||
className={`w-full mx-auto flex items-center gap-1.5 px-3 py-2 ${styles.electronDrag} ${styles.hudBar}`}
|
||||
style={{
|
||||
borderRadius: 9999,
|
||||
background: "linear-gradient(135deg, rgba(28,28,36,0.97) 0%, rgba(18,18,26,0.96) 100%)",
|
||||
backdropFilter: "blur(16px) saturate(140%)",
|
||||
WebkitBackdropFilter: "blur(16px) saturate(140%)",
|
||||
border: "1px solid rgba(80,80,120,0.25)",
|
||||
minHeight: 48,
|
||||
}}
|
||||
>
|
||||
<div className={`flex items-center px-1 ${styles.electronDrag}`}>
|
||||
<RxDragHandleDots2 size={16} className="text-white/35" />
|
||||
</div>
|
||||
try {
|
||||
const result = await window.electronAPI.setHudOverlayCaptureProtection(nextValue);
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={`gap-1 text-white/80 bg-transparent hover:bg-transparent px-0 text-xs ${styles.electronNoDrag}`}
|
||||
onClick={openSourceSelector}
|
||||
disabled={recording}
|
||||
title={selectedSource}
|
||||
>
|
||||
<MdMonitor size={14} className="text-white/80" />
|
||||
<ContentClamp truncateLength={6}>{selectedSource}</ContentClamp>
|
||||
</Button>
|
||||
if (!result.success) {
|
||||
setHideHudFromCapture(!nextValue);
|
||||
return;
|
||||
}
|
||||
|
||||
<div className={dividerClass} />
|
||||
setHideHudFromCapture(result.enabled);
|
||||
} catch (error) {
|
||||
console.error("Failed to update HUD capture protection:", error);
|
||||
setHideHudFromCapture(!nextValue);
|
||||
}
|
||||
};
|
||||
|
||||
<div className={`flex items-center gap-1 ${styles.electronNoDrag}`}>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
|
||||
disabled={recording}
|
||||
title={systemAudioEnabled ? t('recording.disableSystemAudio') : t('recording.enableSystemAudio')}
|
||||
className="text-white/80 hover:bg-transparent"
|
||||
>
|
||||
{systemAudioEnabled ? <MdVolumeUp size={16} className="text-[#2563EB]" /> : <MdVolumeOff size={16} className="text-white/35" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={toggleMicrophone}
|
||||
disabled={recording}
|
||||
title={microphoneEnabled ? t('recording.disableMicrophone') : t('recording.enableMicrophone')}
|
||||
className="text-white/80 hover:bg-transparent"
|
||||
>
|
||||
{microphoneEnabled ? <MdMic size={16} className="text-[#2563EB]" /> : <MdMicOff size={16} className="text-white/35" />}
|
||||
</Button>
|
||||
</div>
|
||||
const chooseRecordingsDirectory = async () => {
|
||||
const result = await window.electronAPI.chooseRecordingsDirectory();
|
||||
if (result.canceled) {
|
||||
return;
|
||||
}
|
||||
if (result.success && result.path) {
|
||||
setRecordingsDirectory(result.path);
|
||||
}
|
||||
};
|
||||
|
||||
<div className={dividerClass} />
|
||||
useEffect(() => {
|
||||
const loadRecordingsDirectory = async () => {
|
||||
const result = await window.electronAPI.getRecordingsDirectory();
|
||||
if (result.success) {
|
||||
setRecordingsDirectory(result.path);
|
||||
}
|
||||
};
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={hasSelectedSource ? toggleRecording : openSourceSelector}
|
||||
disabled={!hasSelectedSource && !recording}
|
||||
className={`gap-1 text-white bg-transparent hover:bg-transparent px-0 text-xs ${styles.electronNoDrag}`}
|
||||
>
|
||||
{recording ? (
|
||||
<>
|
||||
<FaRegStopCircle size={14} className="text-red-400" />
|
||||
<span className="text-red-400 font-medium tabular-nums">{formatTime(elapsed)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BsRecordCircle size={14} className={hasSelectedSource ? "text-white/85" : "text-white/35"} />
|
||||
<span className={hasSelectedSource ? "text-white/80" : "text-white/35"}>{t('recording.record')}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
void loadRecordingsDirectory();
|
||||
}, []);
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={chooseRecordingsDirectory}
|
||||
disabled={recording}
|
||||
title={recordingsDirectory ? t('recording.recordingFolder', undefined, { path: recordingsDirectory }) : t('recording.chooseRecordingsFolder')}
|
||||
className={`text-white/75 hover:bg-transparent px-1 text-[11px] underline decoration-white/45 underline-offset-2 ${styles.electronNoDrag}`}
|
||||
>
|
||||
<ContentClamp truncateLength={18}>{t('recording.folderPath', undefined, { name: recordingsDirectoryName })}</ContentClamp>
|
||||
</Button>
|
||||
const recordingsDirectoryName = recordingsDirectory
|
||||
? recordingsDirectory.split(/[\\/]/).filter(Boolean).pop() || recordingsDirectory
|
||||
: "recordings";
|
||||
const dividerClass = "mx-1 h-5 w-px shrink-0 bg-white/35";
|
||||
|
||||
<div className="ml-auto flex items-center gap-0.5">
|
||||
<div className={dividerClass} />
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={openVideoFile}
|
||||
disabled={recording}
|
||||
title={t('recording.openVideoFile')}
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<MdVideoFile size={15} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={openProjectFile}
|
||||
disabled={recording}
|
||||
title={t('recording.openProject')}
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<FaFolderOpen size={14} />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
title="Language"
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<Languages size={14} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="end"
|
||||
className="min-w-[90px] bg-[rgba(28,28,36,0.97)] border-white/15 text-white/90 backdrop-blur-xl"
|
||||
>
|
||||
{SUPPORTED_LOCALES.map((code) => (
|
||||
<DropdownMenuItem
|
||||
key={code}
|
||||
onSelect={() => setLocale(code as AppLocale)}
|
||||
className={`text-xs cursor-pointer ${
|
||||
locale === code ? "text-white font-medium" : "text-white/60"
|
||||
}`}
|
||||
>
|
||||
{LOCALE_LABELS[code] ?? code}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className={dividerClass} />
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={sendHudOverlayHide}
|
||||
title={t('recording.hideHud')}
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<FiMinus size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={sendHudOverlayClose}
|
||||
title={t('recording.closeApp')}
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<FiX size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const toggleMicrophone = () => {
|
||||
if (!recording) {
|
||||
setMicrophoneEnabled(!microphoneEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-end justify-center overflow-hidden bg-transparent px-3 pb-3 pt-2">
|
||||
<div className={`flex flex-col items-center gap-2 mx-auto ${styles.electronDrag}`}>
|
||||
{showMicControls && (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-full border border-white/15 bg-[rgba(18,18,26,0.92)] px-3 py-2 shadow-xl backdrop-blur-xl ${styles.electronNoDrag}`}
|
||||
>
|
||||
<select
|
||||
value={microphoneDeviceId || selectedDeviceId}
|
||||
onChange={(event) => {
|
||||
setSelectedDeviceId(event.target.value);
|
||||
setMicrophoneDeviceId(event.target.value);
|
||||
}}
|
||||
className={`max-w-[230px] rounded-full border border-white/15 bg-[#131722] px-3 py-1 text-xs text-slate-100 outline-none ${styles.micSelect}`}
|
||||
>
|
||||
{devices.map((device) => (
|
||||
<option key={device.deviceId} value={device.deviceId}>
|
||||
{device.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<AudioLevelMeter level={level} className="w-24" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`mx-auto inline-flex max-w-full items-center gap-1.5 px-3 py-2 ${styles.electronDrag} ${styles.hudBar}`}
|
||||
style={{
|
||||
borderRadius: 9999,
|
||||
background: "linear-gradient(135deg, rgba(28,28,36,0.97) 0%, rgba(18,18,26,0.96) 100%)",
|
||||
backdropFilter: "blur(16px) saturate(140%)",
|
||||
WebkitBackdropFilter: "blur(16px) saturate(140%)",
|
||||
border: "1px solid rgba(80,80,120,0.25)",
|
||||
minHeight: 48,
|
||||
}}
|
||||
>
|
||||
<div className={`flex items-center px-1 ${styles.electronDrag}`}>
|
||||
<RxDragHandleDots2 size={16} className="text-white/35" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={`gap-1 text-white/80 bg-transparent hover:bg-transparent px-0 text-xs ${styles.electronNoDrag}`}
|
||||
onClick={openSourceSelector}
|
||||
disabled={recording}
|
||||
title={selectedSource}
|
||||
>
|
||||
<MdMonitor size={14} className="text-white/80" />
|
||||
<ContentClamp truncateLength={6}>{selectedSource}</ContentClamp>
|
||||
</Button>
|
||||
|
||||
<div className={dividerClass} />
|
||||
|
||||
<div className={`flex items-center gap-1 ${styles.electronNoDrag}`}>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={() => void toggleHudCaptureProtection()}
|
||||
title={
|
||||
hideHudFromCapture ? t("recording.showHudInVideo") : t("recording.hideHudFromVideo")
|
||||
}
|
||||
className="text-white/80 hover:bg-transparent"
|
||||
>
|
||||
{hideHudFromCapture ? (
|
||||
<EyeOff size={16} className="text-[#2563EB]" />
|
||||
) : (
|
||||
<Eye size={16} className="text-white/35" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
|
||||
disabled={recording}
|
||||
title={
|
||||
systemAudioEnabled
|
||||
? t("recording.disableSystemAudio")
|
||||
: t("recording.enableSystemAudio")
|
||||
}
|
||||
className="text-white/80 hover:bg-transparent"
|
||||
>
|
||||
{systemAudioEnabled ? (
|
||||
<MdVolumeUp size={16} className="text-[#2563EB]" />
|
||||
) : (
|
||||
<MdVolumeOff size={16} className="text-white/35" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={toggleMicrophone}
|
||||
disabled={recording}
|
||||
title={
|
||||
microphoneEnabled
|
||||
? t("recording.disableMicrophone")
|
||||
: t("recording.enableMicrophone")
|
||||
}
|
||||
className="text-white/80 hover:bg-transparent"
|
||||
>
|
||||
{microphoneEnabled ? (
|
||||
<MdMic size={16} className="text-[#2563EB]" />
|
||||
) : (
|
||||
<MdMicOff size={16} className="text-white/35" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={dividerClass} />
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={hasSelectedSource ? toggleRecording : openSourceSelector}
|
||||
disabled={!hasSelectedSource && !recording}
|
||||
className={`gap-1 text-white bg-transparent hover:bg-transparent px-0 text-xs ${styles.electronNoDrag}`}
|
||||
>
|
||||
{recording ? (
|
||||
<>
|
||||
<FaRegStopCircle size={14} className="text-red-400" />
|
||||
<span className="text-red-400 font-medium tabular-nums">{formatTime(elapsed)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BsRecordCircle
|
||||
size={14}
|
||||
className={hasSelectedSource ? "text-white/85" : "text-white/35"}
|
||||
/>
|
||||
<span className={hasSelectedSource ? "text-white/80" : "text-white/35"}>
|
||||
{t("recording.record")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={chooseRecordingsDirectory}
|
||||
disabled={recording}
|
||||
title={
|
||||
recordingsDirectory
|
||||
? t("recording.recordingFolder", undefined, { path: recordingsDirectory })
|
||||
: t("recording.chooseRecordingsFolder")
|
||||
}
|
||||
className={`text-white/75 hover:bg-transparent px-1 text-[11px] underline decoration-white/45 underline-offset-2 ${styles.electronNoDrag}`}
|
||||
>
|
||||
<ContentClamp truncateLength={18}>
|
||||
{t("recording.folderPath", undefined, { name: recordingsDirectoryName })}
|
||||
</ContentClamp>
|
||||
</Button>
|
||||
|
||||
<div className="ml-auto flex items-center gap-0.5">
|
||||
<div className={dividerClass} />
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={openVideoFile}
|
||||
disabled={recording}
|
||||
title={t("recording.openVideoFile")}
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<MdVideoFile size={15} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={openProjectFile}
|
||||
disabled={recording}
|
||||
title={t("recording.openProject")}
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<FaFolderOpen size={14} />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
title="Language"
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<Languages size={14} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="end"
|
||||
className="min-w-[90px] bg-[rgba(28,28,36,0.97)] border-white/15 text-white/90 backdrop-blur-xl"
|
||||
>
|
||||
{SUPPORTED_LOCALES.map((code) => (
|
||||
<DropdownMenuItem
|
||||
key={code}
|
||||
onSelect={() => setLocale(code as AppLocale)}
|
||||
className={`text-xs cursor-pointer ${
|
||||
locale === code ? "text-white font-medium" : "text-white/60"
|
||||
}`}
|
||||
>
|
||||
{LOCALE_LABELS[code] ?? code}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className={dividerClass} />
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={sendHudOverlayHide}
|
||||
title={t("recording.hideHud")}
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<FiMinus size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
onClick={sendHudOverlayClose}
|
||||
title={t("recording.closeApp")}
|
||||
className={`text-white/70 hover:bg-transparent ${styles.electronNoDrag}`}
|
||||
>
|
||||
<FiX size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { computeCursorSwayRotation } from "./cursorSway";
|
||||
import {
|
||||
computeCursorSwayRotation,
|
||||
fromCursorSwaySliderValue,
|
||||
toCursorSwaySliderValue,
|
||||
} from "./cursorSway";
|
||||
|
||||
describe("computeCursorSwayRotation", () => {
|
||||
it("returns zero when sway is disabled or there is no movement", () => {
|
||||
expect(computeCursorSwayRotation(120, 0, 16, 0)).toBe(0);
|
||||
expect(computeCursorSwayRotation(0, 0, 16, 1)).toBe(0);
|
||||
});
|
||||
it("returns zero when sway is disabled or there is no movement", () => {
|
||||
expect(computeCursorSwayRotation(120, 0, 16, 0)).toBe(0);
|
||||
expect(computeCursorSwayRotation(0, 0, 16, 1)).toBe(0);
|
||||
});
|
||||
|
||||
it("leans opposite the motion direction", () => {
|
||||
expect(computeCursorSwayRotation(120, 0, 16, 1)).toBeLessThan(0);
|
||||
expect(computeCursorSwayRotation(-120, 0, 16, 1)).toBeGreaterThan(0);
|
||||
expect(computeCursorSwayRotation(0, 120, 16, 1)).toBeLessThan(0);
|
||||
expect(computeCursorSwayRotation(0, -120, 16, 1)).toBeGreaterThan(0);
|
||||
});
|
||||
it("leans with the motion direction", () => {
|
||||
expect(computeCursorSwayRotation(120, 0, 16, 1)).toBeGreaterThan(0);
|
||||
expect(computeCursorSwayRotation(-120, 0, 16, 1)).toBeLessThan(0);
|
||||
expect(computeCursorSwayRotation(0, 120, 16, 1)).toBeGreaterThan(0);
|
||||
expect(computeCursorSwayRotation(0, -120, 16, 1)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it("increases with faster movement for the same direction", () => {
|
||||
const slow = Math.abs(computeCursorSwayRotation(24, 0, 48, 1));
|
||||
const fast = Math.abs(computeCursorSwayRotation(120, 0, 16, 1));
|
||||
it("increases with faster movement for the same direction", () => {
|
||||
const slow = Math.abs(computeCursorSwayRotation(24, 0, 48, 1));
|
||||
const fast = Math.abs(computeCursorSwayRotation(120, 0, 16, 1));
|
||||
|
||||
expect(fast).toBeGreaterThan(slow);
|
||||
});
|
||||
expect(fast).toBeGreaterThan(slow);
|
||||
});
|
||||
|
||||
it("maps a 2x slider value to a 6x sway intensity", () => {
|
||||
expect(computeCursorSwayRotation(-140, 0, 100, 2)).toBeCloseTo(
|
||||
Math.PI / 3,
|
||||
6,
|
||||
);
|
||||
});
|
||||
it("maps a 2x slider value to a 6x sway intensity", () => {
|
||||
expect(computeCursorSwayRotation(-140, 0, 100, 2)).toBeCloseTo(-(Math.PI / 3), 6);
|
||||
});
|
||||
|
||||
it("maps a 1x slider value to the previous 2x sway strength", () => {
|
||||
expect(fromCursorSwaySliderValue(1)).toBe(2);
|
||||
expect(toCursorSwaySliderValue(2)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,46 +4,38 @@ const CURSOR_SWAY_MAX_ROTATION = Math.PI / 18;
|
||||
const CURSOR_SWAY_SPEED_REFERENCE = 1400;
|
||||
const CURSOR_SWAY_VERTICAL_WEIGHT = 0.65;
|
||||
const CURSOR_SWAY_INTENSITY_SCALE = 3;
|
||||
export const CURSOR_SWAY_SLIDER_SCALE = 2;
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
export function computeCursorSwayRotation(
|
||||
dx: number,
|
||||
dy: number,
|
||||
deltaMs: number,
|
||||
sway: number,
|
||||
) {
|
||||
if (sway <= 0) {
|
||||
return 0;
|
||||
}
|
||||
export function computeCursorSwayRotation(dx: number, dy: number, deltaMs: number, sway: number) {
|
||||
if (sway <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const distance = Math.hypot(dx, dy);
|
||||
if (!Number.isFinite(distance) || distance < 0.01) {
|
||||
return 0;
|
||||
}
|
||||
const distance = Math.hypot(dx, dy);
|
||||
if (!Number.isFinite(distance) || distance < 0.01) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const speedPxPerSecond = distance / (clampDeltaMs(deltaMs) / 1000);
|
||||
const speedFactor = clamp(
|
||||
speedPxPerSecond / CURSOR_SWAY_SPEED_REFERENCE,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
if (speedFactor <= 0) {
|
||||
return 0;
|
||||
}
|
||||
const speedPxPerSecond = distance / (clampDeltaMs(deltaMs) / 1000);
|
||||
const speedFactor = clamp(speedPxPerSecond / CURSOR_SWAY_SPEED_REFERENCE, 0, 1);
|
||||
if (speedFactor <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const directionalBias = clamp(
|
||||
(-dx - dy * CURSOR_SWAY_VERTICAL_WEIGHT) / distance,
|
||||
-1,
|
||||
1,
|
||||
);
|
||||
return (
|
||||
directionalBias *
|
||||
speedFactor *
|
||||
CURSOR_SWAY_MAX_ROTATION *
|
||||
sway *
|
||||
CURSOR_SWAY_INTENSITY_SCALE
|
||||
);
|
||||
const directionalBias = clamp((dx + dy * CURSOR_SWAY_VERTICAL_WEIGHT) / distance, -1, 1);
|
||||
return (
|
||||
directionalBias * speedFactor * CURSOR_SWAY_MAX_ROTATION * sway * CURSOR_SWAY_INTENSITY_SCALE
|
||||
);
|
||||
}
|
||||
|
||||
export function toCursorSwaySliderValue(sway: number) {
|
||||
return sway / CURSOR_SWAY_SLIDER_SCALE;
|
||||
}
|
||||
|
||||
export function fromCursorSwaySliderValue(sliderValue: number) {
|
||||
return sliderValue * CURSOR_SWAY_SLIDER_SCALE;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"folderPath": "Path: /{{name}}/",
|
||||
"openVideoFile": "Open video file",
|
||||
"openProject": "Open project",
|
||||
"hideHudFromVideo": "Hide HUD from recording",
|
||||
"showHudInVideo": "Show HUD in recording",
|
||||
"hideHud": "Hide HUD",
|
||||
"closeApp": "Close App"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"folderPath": "Ruta: /{{name}}/",
|
||||
"openVideoFile": "Abrir archivo de video",
|
||||
"openProject": "Abrir proyecto",
|
||||
"hideHudFromVideo": "Ocultar HUD en la grabación",
|
||||
"showHudInVideo": "Mostrar HUD en la grabación",
|
||||
"hideHud": "Ocultar HUD",
|
||||
"closeApp": "Cerrar aplicación"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"folderPath": "路径:/{{name}}/",
|
||||
"openVideoFile": "打开视频文件",
|
||||
"openProject": "打开项目",
|
||||
"hideHudFromVideo": "在录制中隐藏 HUD",
|
||||
"showHudInVideo": "在录制中显示 HUD",
|
||||
"hideHud": "隐藏 HUD",
|
||||
"closeApp": "关闭应用"
|
||||
},
|
||||
|
||||
Vendored
+210
-151
@@ -1,151 +1,210 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="../electron/electron-env" />
|
||||
|
||||
interface ProcessedDesktopSource {
|
||||
id: string;
|
||||
name: string;
|
||||
display_id: string;
|
||||
thumbnail: string | null;
|
||||
appIcon: string | null;
|
||||
originalName?: string;
|
||||
sourceType?: 'screen' | 'window';
|
||||
appName?: string;
|
||||
windowTitle?: string;
|
||||
}
|
||||
|
||||
interface CursorTelemetryPoint {
|
||||
timeMs: number;
|
||||
cx: number;
|
||||
cy: number;
|
||||
interactionType?: 'move' | 'click' | 'double-click' | 'right-click' | 'middle-click' | 'mouseup';
|
||||
cursorType?: 'arrow' | 'text' | 'pointer' | 'crosshair' | 'open-hand' | 'closed-hand' | 'resize-ew' | 'resize-ns' | 'not-allowed';
|
||||
}
|
||||
|
||||
interface SystemCursorAsset {
|
||||
dataUrl: string;
|
||||
hotspotX: number;
|
||||
hotspotY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
getSources: (opts: Electron.SourcesOptions) => Promise<ProcessedDesktopSource[]>
|
||||
switchToEditor: () => Promise<void>
|
||||
openSourceSelector: () => Promise<void>
|
||||
selectSource: (source: any) => Promise<any>
|
||||
getSelectedSource: () => Promise<any>
|
||||
startNativeScreenRecording: (source: any, options?: {
|
||||
capturesSystemAudio?: boolean
|
||||
capturesMicrophone?: boolean
|
||||
microphoneDeviceId?: string
|
||||
microphoneLabel?: string
|
||||
}) => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message?: string
|
||||
error?: string
|
||||
}>
|
||||
stopNativeScreenRecording: () => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message?: string
|
||||
error?: string
|
||||
}>
|
||||
startFfmpegRecording: (source: any) => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message?: string
|
||||
error?: string
|
||||
}>
|
||||
stopFfmpegRecording: () => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message?: string
|
||||
error?: string
|
||||
}>
|
||||
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message: string
|
||||
error?: string
|
||||
}>
|
||||
getRecordedVideoPath: () => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message?: string
|
||||
error?: string
|
||||
}>
|
||||
readLocalFile: (filePath: string) => Promise<{
|
||||
success: boolean
|
||||
data?: Uint8Array
|
||||
error?: string
|
||||
}>
|
||||
getAssetBasePath: () => Promise<string | null>
|
||||
setRecordingState: (recording: boolean) => Promise<void>
|
||||
getCursorTelemetry: (videoPath?: string) => Promise<{
|
||||
success: boolean
|
||||
samples: CursorTelemetryPoint[]
|
||||
message?: string
|
||||
error?: string
|
||||
}>
|
||||
getSystemCursorAssets: () => Promise<{
|
||||
success: boolean
|
||||
cursors: Record<string, SystemCursorAsset>
|
||||
error?: string
|
||||
}>
|
||||
onStopRecordingFromTray: (callback: () => void) => () => void
|
||||
onRecordingStateChanged: (callback: (state: { recording: boolean; sourceName: string }) => void) => () => void
|
||||
onRecordingInterrupted: (callback: (state: { reason: string; message: string }) => void) => () => void
|
||||
onCursorStateChanged: (callback: (state: { cursorType: CursorTelemetryPoint['cursorType'] }) => void) => () => void
|
||||
openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||
getAccessibilityPermissionStatus: () => Promise<{ success: boolean; trusted: boolean; prompted: boolean; error?: string }>
|
||||
requestAccessibilityPermission: () => Promise<{ success: boolean; trusted: boolean; prompted: boolean; error?: string }>
|
||||
getScreenRecordingPermissionStatus: () => Promise<{ success: boolean; status: string; error?: string }>
|
||||
openScreenRecordingPreferences: () => Promise<{ success: boolean; error?: string }>
|
||||
openAccessibilityPreferences: () => Promise<{ success: boolean; error?: string }>
|
||||
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message?: string
|
||||
canceled?: boolean
|
||||
}>
|
||||
openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>
|
||||
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>
|
||||
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>
|
||||
clearCurrentVideoPath: () => Promise<{ success: boolean }>
|
||||
saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
message?: string
|
||||
canceled?: boolean
|
||||
error?: string
|
||||
}>
|
||||
loadProjectFile: () => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
project?: unknown
|
||||
message?: string
|
||||
canceled?: boolean
|
||||
error?: string
|
||||
}>
|
||||
loadCurrentProjectFile: () => Promise<{
|
||||
success: boolean
|
||||
path?: string
|
||||
project?: unknown
|
||||
message?: string
|
||||
canceled?: boolean
|
||||
error?: string
|
||||
}>
|
||||
onMenuLoadProject: (callback: () => void) => () => void
|
||||
onMenuSaveProject: (callback: () => void) => () => void
|
||||
onMenuSaveProjectAs: (callback: () => void) => () => void
|
||||
hideOsCursor: () => Promise<{ success: boolean }>
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void
|
||||
onRequestSaveBeforeClose: (callback: () => Promise<void>) => () => void
|
||||
getRecordingsDirectory: () => Promise<{ success: boolean; path: string; isDefault: boolean; error?: string }>
|
||||
chooseRecordingsDirectory: () => Promise<{ success: boolean; canceled?: boolean; path?: string; isDefault?: boolean; message?: string; error?: string }>
|
||||
}
|
||||
}
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="../electron/electron-env" />
|
||||
|
||||
interface ProcessedDesktopSource {
|
||||
id: string;
|
||||
name: string;
|
||||
display_id: string;
|
||||
thumbnail: string | null;
|
||||
appIcon: string | null;
|
||||
originalName?: string;
|
||||
sourceType?: "screen" | "window";
|
||||
appName?: string;
|
||||
windowTitle?: string;
|
||||
}
|
||||
|
||||
interface CursorTelemetryPoint {
|
||||
timeMs: number;
|
||||
cx: number;
|
||||
cy: number;
|
||||
interactionType?: "move" | "click" | "double-click" | "right-click" | "middle-click" | "mouseup";
|
||||
cursorType?:
|
||||
| "arrow"
|
||||
| "text"
|
||||
| "pointer"
|
||||
| "crosshair"
|
||||
| "open-hand"
|
||||
| "closed-hand"
|
||||
| "resize-ew"
|
||||
| "resize-ns"
|
||||
| "not-allowed";
|
||||
}
|
||||
|
||||
interface SystemCursorAsset {
|
||||
dataUrl: string;
|
||||
hotspotX: number;
|
||||
hotspotY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
hudOverlayHide: () => void;
|
||||
hudOverlayClose: () => void;
|
||||
getHudOverlayCaptureProtection: () => Promise<{ success: boolean; enabled: boolean }>;
|
||||
setHudOverlayCaptureProtection: (
|
||||
enabled: boolean,
|
||||
) => Promise<{ success: boolean; enabled: boolean }>;
|
||||
getSources: (opts: Electron.SourcesOptions) => Promise<ProcessedDesktopSource[]>;
|
||||
switchToEditor: () => Promise<void>;
|
||||
openSourceSelector: () => Promise<void>;
|
||||
selectSource: (source: any) => Promise<any>;
|
||||
getSelectedSource: () => Promise<any>;
|
||||
startNativeScreenRecording: (
|
||||
source: any,
|
||||
options?: {
|
||||
capturesSystemAudio?: boolean;
|
||||
capturesMicrophone?: boolean;
|
||||
microphoneDeviceId?: string;
|
||||
microphoneLabel?: string;
|
||||
},
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
stopNativeScreenRecording: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
startFfmpegRecording: (source: any) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
stopFfmpegRecording: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
storeRecordedVideo: (
|
||||
videoData: ArrayBuffer,
|
||||
fileName: string,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message: string;
|
||||
error?: string;
|
||||
}>;
|
||||
getRecordedVideoPath: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
readLocalFile: (filePath: string) => Promise<{
|
||||
success: boolean;
|
||||
data?: Uint8Array;
|
||||
error?: string;
|
||||
}>;
|
||||
getAssetBasePath: () => Promise<string | null>;
|
||||
setRecordingState: (recording: boolean) => Promise<void>;
|
||||
getCursorTelemetry: (videoPath?: string) => Promise<{
|
||||
success: boolean;
|
||||
samples: CursorTelemetryPoint[];
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
getSystemCursorAssets: () => Promise<{
|
||||
success: boolean;
|
||||
cursors: Record<string, SystemCursorAsset>;
|
||||
error?: string;
|
||||
}>;
|
||||
onStopRecordingFromTray: (callback: () => void) => () => void;
|
||||
onRecordingStateChanged: (
|
||||
callback: (state: { recording: boolean; sourceName: string }) => void,
|
||||
) => () => void;
|
||||
onRecordingInterrupted: (
|
||||
callback: (state: { reason: string; message: string }) => void,
|
||||
) => () => void;
|
||||
onCursorStateChanged: (
|
||||
callback: (state: { cursorType: CursorTelemetryPoint["cursorType"] }) => void,
|
||||
) => () => void;
|
||||
openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>;
|
||||
getAccessibilityPermissionStatus: () => Promise<{
|
||||
success: boolean;
|
||||
trusted: boolean;
|
||||
prompted: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
requestAccessibilityPermission: () => Promise<{
|
||||
success: boolean;
|
||||
trusted: boolean;
|
||||
prompted: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
getScreenRecordingPermissionStatus: () => Promise<{
|
||||
success: boolean;
|
||||
status: string;
|
||||
error?: string;
|
||||
}>;
|
||||
openScreenRecordingPreferences: () => Promise<{ success: boolean; error?: string }>;
|
||||
openAccessibilityPreferences: () => Promise<{ success: boolean; error?: string }>;
|
||||
saveExportedVideo: (
|
||||
videoData: ArrayBuffer,
|
||||
fileName: string,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
canceled?: boolean;
|
||||
}>;
|
||||
openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>;
|
||||
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>;
|
||||
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>;
|
||||
clearCurrentVideoPath: () => Promise<{ success: boolean }>;
|
||||
saveProjectFile: (
|
||||
projectData: unknown,
|
||||
suggestedName?: string,
|
||||
existingProjectPath?: string,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
loadProjectFile: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
project?: unknown;
|
||||
message?: string;
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
loadCurrentProjectFile: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
project?: unknown;
|
||||
message?: string;
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
onMenuLoadProject: (callback: () => void) => () => void;
|
||||
onMenuSaveProject: (callback: () => void) => () => void;
|
||||
onMenuSaveProjectAs: (callback: () => void) => () => void;
|
||||
hideOsCursor: () => Promise<{ success: boolean }>;
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
onRequestSaveBeforeClose: (callback: () => Promise<void>) => () => void;
|
||||
getRecordingsDirectory: () => Promise<{
|
||||
success: boolean;
|
||||
path: string;
|
||||
isDefault: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
chooseRecordingsDirectory: () => Promise<{
|
||||
success: boolean;
|
||||
canceled?: boolean;
|
||||
path?: string;
|
||||
isDefault?: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user