mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 23:43:35 +00:00
Address validated CodeRabbit findings for project saves and HUD lifecycle
This commit is contained in:
1 parent
18c3824bb5
commit
b7997e902b
39 files changed
+168
-66
No files matched your search
Vendored
+1
@@ -1014,6 +1014,7 @@ interface Window {
|
||||
/** Countdown timer before recording */
|
||||
getCountdownDelay: () => Promise<{ success: boolean; delay: number }>;
|
||||
setCountdownDelay: (delay: number) => Promise<{ success: boolean; error?: string }>;
|
||||
finishRecordingStartup: () => Promise<void>;
|
||||
startCountdown: (seconds: number) => Promise<{ success: boolean; cancelled?: boolean }>;
|
||||
cancelCountdown: () => Promise<{ success: boolean }>;
|
||||
getActiveCountdown: () => Promise<{ success: boolean; seconds: number | null }>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getHudCaptureExcludedProcessIds,
|
||||
shouldProtectHudCapture,
|
||||
supportsHudCaptureProtection,
|
||||
} from "../src/lib/hudCaptureProtection";
|
||||
|
||||
@@ -19,3 +20,11 @@ describe("HUD capture protection lifecycle", () => {
|
||||
expect(getHudCaptureExcludedProcessIds("linux", true, 734)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("protects initial capture frames while keeping idle and failed starts visible", () => {
|
||||
expect(shouldProtectHudCapture(true, false, false)).toBe(false);
|
||||
expect(shouldProtectHudCapture(true, false, true)).toBe(true);
|
||||
expect(shouldProtectHudCapture(true, true, false)).toBe(true);
|
||||
expect(shouldProtectHudCapture(true, false, false)).toBe(false);
|
||||
expect(shouldProtectHudCapture(false, true, true)).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { persistRecentMetadata } from "./recentMetadata";
|
||||
it("reports a metadata warning without rejecting an already committed operation", async () => {
|
||||
const log = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
const result = await persistRecentMetadata(async () => {
|
||||
throw Error("disk full");
|
||||
});
|
||||
expect(result).toContain("operation completed");
|
||||
expect(result).toContain("disk full");
|
||||
expect(log).toHaveBeenCalledOnce();
|
||||
log.mockRestore();
|
||||
});
|
||||
it("has no warning after a successful metadata update", async () => {
|
||||
const update = vi.fn().mockResolvedValue(undefined);
|
||||
await expect(persistRecentMetadata(update)).resolves.toBeUndefined();
|
||||
expect(update).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
/** Library history is secondary to a filesystem operation that already committed. */
|
||||
export async function persistRecentMetadata(
|
||||
action: () => Promise<void>,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
await action();
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
const warning = `Project operation completed, but recent-project history could not be updated: ${String(error)}`;
|
||||
console.warn(warning);
|
||||
return warning;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { persistRecentMetadata } from "../project/recentMetadata";
|
||||
import { renameLibraryProject } from "../project/renameLibraryProject";
|
||||
import { createUntitledProject } from "../project/createUntitledProject";
|
||||
import { trashLibraryProjects } from "../project/trashProjects";
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
getTelemetryPathForVideo,
|
||||
isAutoRecordingPath,
|
||||
normalizeVideoSourcePath,
|
||||
normalizePath,
|
||||
parseJsonWithByteOrderMark,
|
||||
} from "../utils";
|
||||
|
||||
@@ -227,9 +229,10 @@ export function registerProjectHandlers() {
|
||||
entries.entries.map((entry) => entry.path),
|
||||
(value) => [getProjectThumbnailPath(value), getProjectBackupPath(value)],
|
||||
);
|
||||
if (currentProjectPath === source) setCurrentProjectPath(target);
|
||||
await rememberRecentProject(target);
|
||||
return { success: true, path: target };
|
||||
if (currentProjectPath && normalizePath(currentProjectPath) === normalizePath(source))
|
||||
setCurrentProjectPath(target);
|
||||
const warning = await persistRecentMetadata(() => rememberRecentProject(target));
|
||||
return { success: true, path: target, warning };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
@@ -711,13 +714,13 @@ export function registerProjectHandlers() {
|
||||
JSON.stringify(prepared.projectData, null, 2),
|
||||
);
|
||||
setCurrentProjectPath(target);
|
||||
await rememberRecentProject(target);
|
||||
const warning = await persistRecentMetadata(() => rememberRecentProject(target));
|
||||
try {
|
||||
await saveProjectThumbnail(target, thumbnailDataUrl);
|
||||
} catch (error) {
|
||||
console.warn("Could not save project thumbnail", error);
|
||||
}
|
||||
return { success: true, path: target, projectId: prepared.projectId };
|
||||
return { success: true, path: target, projectId: prepared.projectId, warning };
|
||||
} catch (error) {
|
||||
return { success: false, message: String(error) };
|
||||
}
|
||||
@@ -732,12 +735,14 @@ export function registerProjectHandlers() {
|
||||
});
|
||||
if (currentProjectPath && result.deleted.includes(path.resolve(currentProjectPath)))
|
||||
setCurrentProjectPath(null);
|
||||
await saveRecentProjectPaths(
|
||||
(await loadRecentProjectPaths()).filter(
|
||||
(p) => !result.deleted.includes(path.resolve(p)),
|
||||
const warning = await persistRecentMetadata(async () =>
|
||||
saveRecentProjectPaths(
|
||||
(await loadRecentProjectPaths()).filter(
|
||||
(p) => !result.deleted.includes(path.resolve(p)),
|
||||
),
|
||||
),
|
||||
);
|
||||
return { success: result.errors.length === 0, ...result };
|
||||
return { success: result.errors.length === 0, ...result, warning };
|
||||
} catch (error) {
|
||||
return { success: false, deleted: [], errors: [String(error)] };
|
||||
}
|
||||
|
||||
@@ -14,10 +14,7 @@ import {
|
||||
} from "electron";
|
||||
import { getHudCaptureExcludedProcessIds } from "../../../src/lib/hudCaptureProtection";
|
||||
import { showCursor } from "../../cursorHider";
|
||||
import {
|
||||
getHudOverlayCaptureProtectionEnabled,
|
||||
reassertHudOverlayCaptureProtection,
|
||||
} from "../../windows";
|
||||
import { getHudOverlayCaptureProtectionEnabled, beginHudCaptureProtection } from "../../windows";
|
||||
import { ALLOW_RECORDLY_WINDOW_CAPTURE } from "../constants";
|
||||
import { startWindowBoundsCapture, stopWindowBoundsCapture } from "../cursor/bounds";
|
||||
import { startInteractionCapture, stopInteractionCapture } from "../cursor/interaction";
|
||||
@@ -404,8 +401,8 @@ export function registerRecordingHandlers(
|
||||
ipcMain.handle(
|
||||
"start-native-screen-recording",
|
||||
async (_, source: SelectedSource, options?: NativeMacRecordingOptions) => {
|
||||
// Refresh window protection; it applies only while recording is active.
|
||||
reassertHudOverlayCaptureProtection();
|
||||
// Protect the HUD at the capture boundary, before the renderer publishes recording state.
|
||||
beginHudCaptureProtection();
|
||||
const visibleWindowBounds = source.id?.startsWith("window:")
|
||||
? await bringSelectedWindowForward(source)
|
||||
: null;
|
||||
|
||||
+8
-4
@@ -55,7 +55,7 @@ import {
|
||||
getUpdateToastWindow,
|
||||
hideUpdateToastWindow,
|
||||
isHudOverlayMousePassthroughSupported,
|
||||
reassertHudOverlayCaptureProtection,
|
||||
beginHudCaptureProtection,
|
||||
reassertHudOverlayMousePassthrough as reassertHudOverlayMouseState,
|
||||
setHudOverlayRecordingActive,
|
||||
showUpdateToastWindow,
|
||||
@@ -873,7 +873,9 @@ function createSourceSelectorWindowWrapper() {
|
||||
app.on("before-quit", () => {
|
||||
isAppQuitting = true;
|
||||
authCallbacks.close();
|
||||
void clearRecordingTrashUndo().catch((error) => console.warn("Could not clear recording undo cache", error));
|
||||
void clearRecordingTrashUndo().catch((error) =>
|
||||
console.warn("Could not clear recording undo cache", error),
|
||||
);
|
||||
killWindowsCaptureProcess();
|
||||
showCursor();
|
||||
cleanupNativeVideoExportSessions();
|
||||
@@ -902,7 +904,9 @@ app.on("second-instance", (_event, commandLine) => {
|
||||
app.whenReady().then(async () => {
|
||||
authCallbacks.startDevServer();
|
||||
if (process.defaultApp && process.argv[1]) {
|
||||
app.setAsDefaultProtocolClient(authCallbacks.protocol, process.execPath, [path.resolve(process.argv[1])]);
|
||||
app.setAsDefaultProtocolClient(authCallbacks.protocol, process.execPath, [
|
||||
path.resolve(process.argv[1]),
|
||||
]);
|
||||
} else {
|
||||
app.setAsDefaultProtocolClient(authCallbacks.protocol);
|
||||
}
|
||||
@@ -1105,7 +1109,7 @@ app.whenReady().then(async () => {
|
||||
|
||||
// Browser and Linux portal capture starts as soon as this callback
|
||||
// resolves, before recording-state-changed is emitted.
|
||||
reassertHudOverlayCaptureProtection();
|
||||
beginHudCaptureProtection();
|
||||
|
||||
const sourceId = getSelectedSourceId();
|
||||
// On Linux/Wayland, calling desktopCapturer.getSources() itself
|
||||
|
||||
@@ -1089,6 +1089,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
}) => ipcRenderer.invoke("set-recording-preferences", prefs),
|
||||
getCountdownDelay: () => ipcRenderer.invoke("get-countdown-delay"),
|
||||
setCountdownDelay: (delay: number) => ipcRenderer.invoke("set-countdown-delay", delay),
|
||||
finishRecordingStartup: () => ipcRenderer.invoke("finish-recording-startup"),
|
||||
startCountdown: (seconds: number) => ipcRenderer.invoke("start-countdown", seconds),
|
||||
cancelCountdown: () => ipcRenderer.invoke("cancel-countdown"),
|
||||
getActiveCountdown: () => ipcRenderer.invoke("get-active-countdown"),
|
||||
|
||||
+21
-2
@@ -4,7 +4,10 @@ import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { app, BrowserWindow, ipcMain } from "electron";
|
||||
import { supportsHudCaptureProtection } from "../src/lib/hudCaptureProtection";
|
||||
import {
|
||||
supportsHudCaptureProtection,
|
||||
shouldProtectHudCapture,
|
||||
} from "../src/lib/hudCaptureProtection";
|
||||
import { USER_DATA_PATH } from "./appPaths";
|
||||
import {
|
||||
getHudOverlayWindowBounds,
|
||||
@@ -36,6 +39,7 @@ let hudOverlayIgnoringMouse = true;
|
||||
let hudOverlaySourceSelectionActive = false;
|
||||
let hudOverlayMouseReassertTimer: NodeJS.Timeout | null = null;
|
||||
let hudOverlayRecordingActive = false;
|
||||
let hudCaptureStarting = false;
|
||||
let hudOverlayWebcamPreviewVisible = false;
|
||||
let countdownWindow: BrowserWindow | null = null;
|
||||
let updateToastWindow: BrowserWindow | null = null;
|
||||
@@ -155,12 +159,23 @@ function applyHudOverlayCaptureProtectionToWindow(hud: BrowserWindow, enabled: b
|
||||
|
||||
try {
|
||||
// Keep the idle HUD visible to screenshots and other capture applications.
|
||||
hud.setContentProtection(enabled && hudOverlayRecordingActive);
|
||||
hud.setContentProtection(
|
||||
shouldProtectHudCapture(enabled, hudOverlayRecordingActive, hudCaptureStarting),
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("Failed to apply HUD capture protection:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function beginHudCaptureProtection(): void {
|
||||
hudCaptureStarting = true;
|
||||
reassertHudOverlayCaptureProtection();
|
||||
}
|
||||
ipcMain.handle("finish-recording-startup", () => {
|
||||
hudCaptureStarting = false;
|
||||
reassertHudOverlayCaptureProtection();
|
||||
});
|
||||
|
||||
export function reassertHudOverlayCaptureProtection(): boolean {
|
||||
const enabled = loadHudOverlayCaptureProtectionSetting();
|
||||
const hud = getHudOverlayWindow();
|
||||
@@ -650,6 +665,9 @@ export function createHudOverlayWindow(): BrowserWindow {
|
||||
screen.removeListener("display-metrics-changed", handleDisplayMetricsChanged);
|
||||
if (hudOverlayWindow === win) {
|
||||
hudOverlayWindow = null;
|
||||
recordingPreparationActive = false;
|
||||
hudCaptureStarting = false;
|
||||
hudOverlayRecordingActive = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -703,6 +721,7 @@ export function reassertHudOverlayMousePassthrough(): void {
|
||||
}
|
||||
|
||||
export function setHudOverlayRecordingActive(recording: boolean): void {
|
||||
hudCaptureStarting = false;
|
||||
hudOverlayRecordingActive = Boolean(recording);
|
||||
notifyEditorMode();
|
||||
hudOverlayFallbackExpanded = false;
|
||||
|
||||
@@ -195,8 +195,8 @@ function LaunchWindowContent() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
iconSize="lg"
|
||||
aria-label="Home"
|
||||
title="Home"
|
||||
aria-label={t("recording.home")}
|
||||
title={t("recording.home")}
|
||||
onClick={openHome}
|
||||
>
|
||||
<House weight="fill" className="size-5" />
|
||||
|
||||
@@ -41,7 +41,7 @@ export function RecordingControls({
|
||||
isIconOnly
|
||||
variant="ghost"
|
||||
className={actionClass}
|
||||
aria-label="Home"
|
||||
aria-label={t("recording.home")}
|
||||
onPress={onHome}
|
||||
>
|
||||
<House weight="fill" className="size-4" />
|
||||
|
||||
@@ -30,7 +30,10 @@ describe("announcement configuration", () => {
|
||||
<DashboardAnnouncements
|
||||
config={{
|
||||
enabled: true,
|
||||
banners: [{ ...banner, href: "javascript:alert(1)" }, banner],
|
||||
banners: [
|
||||
{ ...banner, src: "/banner-1.svg", href: "javascript:alert(1)" },
|
||||
{ ...banner, src: "/banner-2.svg" },
|
||||
],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -36,6 +36,7 @@ export function DashboardAnnouncements({
|
||||
{href ? (
|
||||
<Link
|
||||
href={href}
|
||||
onAuxClick={(event) => event.preventDefault()}
|
||||
className="block w-full"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
@@ -67,7 +68,7 @@ export function DashboardAnnouncements({
|
||||
>
|
||||
{banners.map((banner, slide) => (
|
||||
<Button
|
||||
key={banner.src}
|
||||
key={`${slide}:${banner.src}`}
|
||||
isIconOnly
|
||||
variant="ghost"
|
||||
aria-label={`Announcement ${slide + 1}`}
|
||||
|
||||
@@ -4,7 +4,7 @@ import VideoPlayback, { type VideoPlaybackRef } from "../VideoPlayback";
|
||||
import { normalizeProjectEditor } from "../projectPersistence";
|
||||
import type { CursorTelemetryPoint } from "../types";
|
||||
|
||||
const ignore = () => {};
|
||||
const ignore = () => undefined;
|
||||
/** One muted five-second pass through the saved timeline using the editor renderer. */
|
||||
export function ProjectHoverPreview({
|
||||
data,
|
||||
|
||||
@@ -32,7 +32,7 @@ export function ProjectThumbnail({
|
||||
.then((result) => {
|
||||
if (active && !document.hidden && result.success) setPreview(result.value);
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => undefined);
|
||||
}, 300);
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries.every((entry) => !entry.isIntersecting)) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export function RawThumbnail({ entry, active }: { entry: RecordingLibraryEntry;
|
||||
.then((result) => {
|
||||
if (active && result.success) setPoster(result.value);
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
@@ -30,7 +30,7 @@ export function RawThumbnail({ entry, active }: { entry: RecordingLibraryEntry;
|
||||
}
|
||||
const timer = window.setTimeout(() => {
|
||||
element.currentTime = 0;
|
||||
void element.play().catch(() => {});
|
||||
void element.play().catch(() => undefined);
|
||||
}, 300);
|
||||
const stop = () => {
|
||||
if (document.hidden) element.pause();
|
||||
|
||||
@@ -21,6 +21,7 @@ export function useDashboardModel({
|
||||
setSelected((prev) =>
|
||||
prev.includes(path) ? prev.filter((p) => p !== path) : [...prev, path],
|
||||
);
|
||||
const { folders, save } = useProjectFolders();
|
||||
const assignFolder = (path: string, id: string) =>
|
||||
save(
|
||||
folders.map((f) => ({
|
||||
@@ -52,7 +53,6 @@ export function useDashboardModel({
|
||||
};
|
||||
const libraryEntries = isRaw ? raw.entries : entries;
|
||||
const [busy, setBusy] = useState(false);
|
||||
const { folders, save } = useProjectFolders();
|
||||
const visible = useMemo(
|
||||
() =>
|
||||
libraryEntries
|
||||
|
||||
@@ -42,6 +42,7 @@ export function useProjectFolders() {
|
||||
try {
|
||||
localStorage.setItem(KEY, JSON.stringify(next));
|
||||
setFolders(next);
|
||||
window.dispatchEvent(new CustomEvent("recordly-folders-changed", { detail: next }));
|
||||
} catch {
|
||||
toast.error("Could not save folders");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useRecordingLibrary } from "../library/useRecordingLibrary";
|
||||
import { RecordingLibraryPanel } from "../library/RecordingLibraryPanel";
|
||||
import { RECORDING_DRAG_TYPE } from "@/types/recordingLibrary";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useCallback, useEffect, useRef, useState, type ComponentProps } from "react";
|
||||
import { useCallback, useMemo, useEffect, useRef, useState, type ComponentProps } from "react";
|
||||
import { EditorAnnouncementBanner } from "@/components/announcements/EditorAnnouncementBanner";
|
||||
import { Toaster } from "@/components/ui/toast";
|
||||
import type { useI18n } from "@/contexts/I18nContext";
|
||||
@@ -133,19 +133,21 @@ export function EditorShell(props: Props) {
|
||||
handleAutoSuggestZoomsConsumed,
|
||||
} = editing;
|
||||
const { dialogActions, status: exportStatus, exportMessage } = exportController;
|
||||
const dashboardSettingsContent = useMemo(
|
||||
() => (
|
||||
<SettingsPanel
|
||||
{...settingsPanelProps}
|
||||
activeEffectSection="settings"
|
||||
selectedAnnotationId={null}
|
||||
selectedClipId={null}
|
||||
advanced
|
||||
/>
|
||||
),
|
||||
[settingsPanelProps],
|
||||
);
|
||||
const editorDialogs = (
|
||||
<AccountProfileContext.Provider value={auth.user}>
|
||||
<DashboardSettingsContext.Provider
|
||||
value={
|
||||
<SettingsPanel
|
||||
{...settingsPanelProps}
|
||||
activeEffectSection="settings"
|
||||
selectedAnnotationId={null}
|
||||
selectedClipId={null}
|
||||
advanced
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DashboardSettingsContext.Provider value={dashboardSettingsContent}>
|
||||
<EditorDialogs
|
||||
t={t}
|
||||
projectSaveDialogOpen={project.projectSaveDialogOpen}
|
||||
|
||||
@@ -112,8 +112,8 @@ export function useProjectLibraryController({
|
||||
}
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const targetWidth = 1600;
|
||||
const targetHeight = 1200;
|
||||
const targetWidth = 640;
|
||||
const targetHeight = 480;
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
|
||||
|
||||
@@ -180,12 +180,9 @@ export function useProjectOpenActions({
|
||||
}
|
||||
videoPlaybackRef.current?.pause();
|
||||
setIsPlaying(false);
|
||||
if (
|
||||
project.videoPath &&
|
||||
!project.error &&
|
||||
!(await saveProject(false, { remountPreviewAfterSave: false }))
|
||||
)
|
||||
return;
|
||||
if (project.videoPath && !project.error) {
|
||||
await saveProject(false, { remountPreviewAfterSave: false });
|
||||
}
|
||||
project.setProjectBrowserOpen(true);
|
||||
void refreshProjectLibrary();
|
||||
}, [
|
||||
@@ -236,14 +233,17 @@ export function useProjectOpenActions({
|
||||
},
|
||||
[project, refreshProjectLibrary],
|
||||
);
|
||||
const handleRenameLibraryProject = async (path: string, name: string) => {
|
||||
const result = await window.electronAPI.renameLibraryProject(path, name);
|
||||
if (!result.success || !result.path)
|
||||
throw new Error(result.error || "Could not rename project");
|
||||
if (project.currentProjectPath === path) project.setCurrentProjectPath(result.path);
|
||||
await refreshProjectLibrary();
|
||||
return result.path;
|
||||
};
|
||||
const handleRenameLibraryProject = useCallback(
|
||||
async (path: string, name: string) => {
|
||||
const result = await window.electronAPI.renameLibraryProject(path, name);
|
||||
if (!result.success || !result.path)
|
||||
throw new Error(result.error || "Could not rename project");
|
||||
if (project.currentProjectPath === path) project.setCurrentProjectPath(result.path);
|
||||
await refreshProjectLibrary();
|
||||
return result.path;
|
||||
},
|
||||
[project, refreshProjectLibrary],
|
||||
);
|
||||
|
||||
return {
|
||||
handleRenameLibraryProject,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { moveProjectFolderReferences } from "../dashboard/useProjectFolders";
|
||||
import { moveProjectShareLink } from "../cloud/projectShareLinks";
|
||||
import { type RefObject, useCallback, useEffect, useRef } from "react";
|
||||
import { type RefObject, useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { toast } from "@/components/ui/toast";
|
||||
import { createProjectData, type EditorProjectData } from "../projectPersistence";
|
||||
import type { useProjectState } from "../state/useProjectState";
|
||||
@@ -61,9 +61,13 @@ export function useProjectSaveActions({
|
||||
} = project;
|
||||
const savingNameRef = useRef(false);
|
||||
const activePathRef = useRef(currentProjectPath);
|
||||
activePathRef.current = currentProjectPath;
|
||||
const activeSourceRef = useRef(currentSourcePath);
|
||||
activeSourceRef.current = currentSourcePath;
|
||||
useLayoutEffect(() => {
|
||||
activePathRef.current = currentProjectPath;
|
||||
}, [currentProjectPath]);
|
||||
useLayoutEffect(() => {
|
||||
activeSourceRef.current = currentSourcePath;
|
||||
}, [currentSourcePath]);
|
||||
const autosaveTimeoutRef = useRef<number | null>(null);
|
||||
const saveQueueRef = useRef<Promise<unknown>>(Promise.resolve());
|
||||
const clearPendingAutosave = useCallback(() => {
|
||||
|
||||
@@ -41,13 +41,12 @@ export function useProjectSnapshotModel({
|
||||
[project.videoPath, project.videoSourcePath],
|
||||
);
|
||||
const projectDisplayName = useMemo(() => {
|
||||
const fileName =
|
||||
project.currentProjectPath?.split(/[\\/]/).pop() ?? "Untitled Project";
|
||||
const fileName = project.currentProjectPath?.split(/[\\/]/).pop() ?? "Untitled Project";
|
||||
return (
|
||||
fileName.replace(/\.recordly$/i, "").replace(/\.[^.]+$/, "") ||
|
||||
t("editor.project.untitled", "Untitled Project")
|
||||
);
|
||||
}, [project.currentProjectPath, currentSourcePath, t]);
|
||||
}, [project.currentProjectPath, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!project.isEditingProjectName) project.setProjectNameDraft(projectDisplayName);
|
||||
|
||||
@@ -2273,6 +2273,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
await stopWebcamRecorder();
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await window.electronAPI.finishRecordingStartup();
|
||||
} catch (error) {
|
||||
console.warn("Failed to release recording startup protection:", error);
|
||||
}
|
||||
setHudSourceSelectionActive(false);
|
||||
startInFlight.current = false;
|
||||
setStarting(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "Startseite",
|
||||
"disableSystemAudio": "System-Audio deaktivieren",
|
||||
"enableSystemAudio": "System-Audio aktivieren",
|
||||
"disableMicrophone": "Mikrofon deaktivieren",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "Home",
|
||||
"disableSystemAudio": "Disable system audio",
|
||||
"enableSystemAudio": "Enable system audio",
|
||||
"disableMicrophone": "Disable microphone",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "Inicio",
|
||||
"disableSystemAudio": "Desactivar audio del sistema",
|
||||
"enableSystemAudio": "Activar audio del sistema",
|
||||
"disableMicrophone": "Desactivar micrófono",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "Accueil",
|
||||
"disableSystemAudio": "Désactiver l’audio système",
|
||||
"enableSystemAudio": "Activer l’audio système",
|
||||
"disableMicrophone": "Désactiver le microphone",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "Home",
|
||||
"disableSystemAudio": "Disabilita audio di sistema",
|
||||
"enableSystemAudio": "Abilita audio di sistema",
|
||||
"disableMicrophone": "Disabilita microfono",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "홈",
|
||||
"disableSystemAudio": "시스템 오디오 끄기",
|
||||
"enableSystemAudio": "시스템 오디오 켜기",
|
||||
"disableMicrophone": "마이크 끄기",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "Start",
|
||||
"disableSystemAudio": "Systeemaudio uitschakelen",
|
||||
"enableSystemAudio": "Systeemaudio inschakelen",
|
||||
"disableMicrophone": "Microfoon uitschakelen",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "Início",
|
||||
"disableSystemAudio": "Desativar áudio do sistema",
|
||||
"enableSystemAudio": "Ativar áudio do sistema",
|
||||
"disableMicrophone": "Desativar microfone",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "Главная",
|
||||
"disableSystemAudio": "Отключить системные звуки",
|
||||
"enableSystemAudio": "Включить системные звуки",
|
||||
"disableMicrophone": "Отключить микрофон",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "主页",
|
||||
"disableSystemAudio": "禁用系统音频",
|
||||
"enableSystemAudio": "启用系统音频",
|
||||
"disableMicrophone": "禁用麦克风",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"recording": {
|
||||
"home": "首頁",
|
||||
"disableSystemAudio": "停用系統音訊",
|
||||
"enableSystemAudio": "啟用系統音訊",
|
||||
"disableMicrophone": "停用麥克風",
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
color: inherit;
|
||||
}
|
||||
.inline-project-name:focus { outline: none; box-shadow: none; }
|
||||
.inline-project-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
||||
|
||||
.dashboard-shell {
|
||||
background-color: color-mix(in srgb, var(--background) 92%, var(--foreground) 8%);
|
||||
|
||||
@@ -13,3 +13,11 @@ export function getHudCaptureExcludedProcessIds(
|
||||
|
||||
return [processId];
|
||||
}
|
||||
|
||||
export function shouldProtectHudCapture(
|
||||
enabled: boolean,
|
||||
recording: boolean,
|
||||
starting: boolean,
|
||||
): boolean {
|
||||
return enabled && (recording || starting);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export async function installDesktopBridge(page: Page, videoFixture = "preview.m
|
||||
Object.assign(window, {
|
||||
electronAPI: {
|
||||
getAppSetting: () => null,
|
||||
finishRecordingStartup: async () => undefined,
|
||||
showProjectDashboard: async () => {
|
||||
document.documentElement.dataset.dashboardOpened = "true";
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ test("recording HUD uses uniform controls and a readable timer", async ({ page }
|
||||
await installDesktopBridge(page);
|
||||
await page.addInitScript(() => {
|
||||
window.electronAPI.onRecordingStateChanged = (callback) => {
|
||||
const listener = () => callback({ recording: true });
|
||||
const listener = () => callback({ recording: true, sourceName: "Built-in Display" });
|
||||
window.addEventListener("test-recording-started", listener);
|
||||
return () => window.removeEventListener("test-recording-started", listener);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user