mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
fix(project): add named save IPC and stabilize recorder lifecycle
This commit is contained in:
Vendored
+11
@@ -25,6 +25,17 @@ interface ElectronAPIProjects {
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
saveProjectFileNamed: (
|
||||
projectData: unknown,
|
||||
projectName: string,
|
||||
thumbnailDataUrl?: string | null,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
loadProjectFile: () => Promise<{
|
||||
success: boolean;
|
||||
path?: string;
|
||||
|
||||
@@ -41,6 +41,29 @@ function normalizeRecordingTimeOffsetMs(value: unknown): number {
|
||||
return typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 0;
|
||||
}
|
||||
|
||||
function normalizeProjectSaveName(projectName?: string | null) {
|
||||
if (typeof projectName !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmedName = projectName.trim();
|
||||
if (!trimmedName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const withoutExtension = trimmedName.replace(
|
||||
new RegExp(`\\.${PROJECT_FILE_EXTENSION}$`, "i"),
|
||||
"",
|
||||
);
|
||||
const sanitizedName = withoutExtension
|
||||
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/[. ]+$/g, "")
|
||||
.trim();
|
||||
|
||||
return sanitizedName || null;
|
||||
}
|
||||
|
||||
export function registerProjectHandlers() {
|
||||
ipcMain.handle('reveal-in-folder', async (_, filePath: string) => {
|
||||
try {
|
||||
@@ -142,10 +165,8 @@ export function registerProjectHandlers() {
|
||||
}
|
||||
}
|
||||
|
||||
const safeName = (suggestedName || `project-${Date.now()}`).replace(/[^a-zA-Z0-9-_]/g, '_')
|
||||
const defaultName = safeName.endsWith(`.${PROJECT_FILE_EXTENSION}`)
|
||||
? safeName
|
||||
: `${safeName}.${PROJECT_FILE_EXTENSION}`
|
||||
const safeName = normalizeProjectSaveName(suggestedName) || `project-${Date.now()}`
|
||||
const defaultName = `${safeName}.${PROJECT_FILE_EXTENSION}`
|
||||
|
||||
const result = await dialog.showSaveDialog({
|
||||
title: 'Save Recordly Project',
|
||||
@@ -185,6 +206,42 @@ export function registerProjectHandlers() {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('save-project-file-named', async (_, projectData: unknown, projectName: string, thumbnailDataUrl?: string | null) => {
|
||||
try {
|
||||
const normalizedProjectName = normalizeProjectSaveName(projectName)
|
||||
if (!normalizedProjectName) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Project name is required',
|
||||
}
|
||||
}
|
||||
|
||||
const projectsDir = await getProjectsDir()
|
||||
const targetProjectPath = path.join(
|
||||
projectsDir,
|
||||
`${normalizedProjectName}.${PROJECT_FILE_EXTENSION}`,
|
||||
)
|
||||
|
||||
await fs.writeFile(targetProjectPath, JSON.stringify(projectData, null, 2), 'utf-8')
|
||||
setCurrentProjectPath(targetProjectPath)
|
||||
await saveProjectThumbnail(targetProjectPath, thumbnailDataUrl)
|
||||
await rememberRecentProject(targetProjectPath)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: targetProjectPath,
|
||||
message: 'Project saved successfully'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save named project file:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to save project file',
|
||||
error: String(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('load-project-file', async () => {
|
||||
try {
|
||||
const projectsDir = await getProjectsDir()
|
||||
|
||||
@@ -409,6 +409,18 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
thumbnailDataUrl,
|
||||
);
|
||||
},
|
||||
saveProjectFileNamed: (
|
||||
projectData: unknown,
|
||||
projectName: string,
|
||||
thumbnailDataUrl?: string | null,
|
||||
) => {
|
||||
return ipcRenderer.invoke(
|
||||
"save-project-file-named",
|
||||
projectData,
|
||||
projectName,
|
||||
thumbnailDataUrl,
|
||||
);
|
||||
},
|
||||
loadProjectFile: () => {
|
||||
return ipcRenderer.invoke("load-project-file");
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, type MutableRefObject } from "react";
|
||||
import { useEffect, useRef, type MutableRefObject } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ScreenRecorderRefs } from "./shared";
|
||||
|
||||
@@ -16,67 +16,71 @@ type UseScreenRecorderLifecycleOptions = {
|
||||
};
|
||||
|
||||
export function useScreenRecorderLifecycle(options: UseScreenRecorderLifecycleOptions) {
|
||||
const optionsRef = useRef(options);
|
||||
optionsRef.current = options;
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
const platform = await window.electronAPI.getPlatform();
|
||||
options.setIsMacOS(platform === "darwin");
|
||||
optionsRef.current.setIsMacOS(platform === "darwin");
|
||||
})();
|
||||
}, [options]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.refs.countdownDelayLoaded.current) return;
|
||||
options.refs.countdownDelayLoaded.current = true;
|
||||
if (optionsRef.current.refs.countdownDelayLoaded.current) return;
|
||||
optionsRef.current.refs.countdownDelayLoaded.current = true;
|
||||
|
||||
void (async () => {
|
||||
const result = await window.electronAPI.getCountdownDelay();
|
||||
if (result.success && typeof result.delay === "number") {
|
||||
options.setCountdownDelayState(result.delay);
|
||||
optionsRef.current.setCountdownDelayState(result.delay);
|
||||
}
|
||||
})();
|
||||
}, [options]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.refs.recordingPrefsLoaded.current) return;
|
||||
options.refs.recordingPrefsLoaded.current = true;
|
||||
if (optionsRef.current.refs.recordingPrefsLoaded.current) return;
|
||||
optionsRef.current.refs.recordingPrefsLoaded.current = true;
|
||||
|
||||
void (async () => {
|
||||
const result = await window.electronAPI.getRecordingPreferences();
|
||||
if (result.success) {
|
||||
options.setMicrophoneEnabled(result.microphoneEnabled);
|
||||
optionsRef.current.setMicrophoneEnabled(result.microphoneEnabled);
|
||||
if (result.microphoneDeviceId) {
|
||||
options.setMicrophoneDeviceId(result.microphoneDeviceId);
|
||||
optionsRef.current.setMicrophoneDeviceId(result.microphoneDeviceId);
|
||||
}
|
||||
options.setSystemAudioEnabled(result.systemAudioEnabled);
|
||||
optionsRef.current.setSystemAudioEnabled(result.systemAudioEnabled);
|
||||
}
|
||||
})();
|
||||
}, [options]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cleanup: (() => void) | undefined;
|
||||
|
||||
if (window.electronAPI?.onStopRecordingFromTray) {
|
||||
cleanup = window.electronAPI.onStopRecordingFromTray(() => {
|
||||
options.stopRecordingRef.current();
|
||||
optionsRef.current.stopRecordingRef.current();
|
||||
});
|
||||
}
|
||||
|
||||
const removeRecordingStateListener = window.electronAPI?.onRecordingStateChanged?.(
|
||||
(state) => {
|
||||
options.setRecording(state.recording);
|
||||
optionsRef.current.setRecording(state.recording);
|
||||
},
|
||||
);
|
||||
|
||||
const removeRecordingInterruptedListener = window.electronAPI?.onRecordingInterrupted?.(
|
||||
(state) => {
|
||||
void (async () => {
|
||||
options.setRecording(false);
|
||||
options.refs.nativeScreenRecording.current = false;
|
||||
await options.cleanupCapturedMedia();
|
||||
const currentOptions = optionsRef.current;
|
||||
currentOptions.setRecording(false);
|
||||
currentOptions.refs.nativeScreenRecording.current = false;
|
||||
await currentOptions.cleanupCapturedMedia();
|
||||
await window.electronAPI.setRecordingState(false);
|
||||
|
||||
if (state.reason !== "window-unavailable") {
|
||||
try {
|
||||
const recoveredPath = await options.recoverNativeRecordingSession();
|
||||
const recoveredPath = await currentOptions.recoverNativeRecordingSession();
|
||||
if (recoveredPath) {
|
||||
return;
|
||||
}
|
||||
@@ -90,9 +94,9 @@ export function useScreenRecorderLifecycle(options: UseScreenRecorderLifecycleOp
|
||||
|
||||
if (
|
||||
state.reason === "window-unavailable" &&
|
||||
!options.refs.hasPromptedForReselect.current
|
||||
!currentOptions.refs.hasPromptedForReselect.current
|
||||
) {
|
||||
options.refs.hasPromptedForReselect.current = true;
|
||||
currentOptions.refs.hasPromptedForReselect.current = true;
|
||||
alert(state.message);
|
||||
await window.electronAPI.openSourceSelector();
|
||||
} else {
|
||||
@@ -104,22 +108,23 @@ export function useScreenRecorderLifecycle(options: UseScreenRecorderLifecycleOp
|
||||
);
|
||||
|
||||
return () => {
|
||||
const currentOptions = optionsRef.current;
|
||||
cleanup?.();
|
||||
removeRecordingStateListener?.();
|
||||
removeRecordingInterruptedListener?.();
|
||||
|
||||
if (options.refs.nativeScreenRecording.current) {
|
||||
options.refs.nativeScreenRecording.current = false;
|
||||
if (currentOptions.refs.nativeScreenRecording.current) {
|
||||
currentOptions.refs.nativeScreenRecording.current = false;
|
||||
void window.electronAPI.stopNativeScreenRecording();
|
||||
}
|
||||
|
||||
const recorder = options.refs.mediaRecorder.current;
|
||||
const recorder = currentOptions.refs.mediaRecorder.current;
|
||||
const recorderState = recorder?.state;
|
||||
if (recorder && (recorderState === "recording" || recorderState === "paused")) {
|
||||
recorder.stop();
|
||||
}
|
||||
|
||||
void options.cleanupCapturedMedia();
|
||||
void currentOptions.cleanupCapturedMedia();
|
||||
};
|
||||
}, [options]);
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user