fix(hud): persist capture hiding review fixes

This commit is contained in:
KBCats
2026-03-15 20:36:27 -07:00
parent 43a9ec5701
commit acf796b074
3 changed files with 104 additions and 229 deletions
+61 -5
View File
@@ -1,7 +1,8 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { BrowserWindow, ipcMain } from "electron";
import { app, BrowserWindow, ipcMain } from "electron";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const nodeRequire = createRequire(import.meta.url);
@@ -16,7 +17,50 @@ const WINDOW_ICON_PATH = path.join(
);
let hudOverlayWindow: BrowserWindow | null = null;
let hudOverlayHiddenFromCapture = false;
let hudOverlayHiddenFromCapture = true;
let hudOverlayCaptureProtectionLoaded = false;
const HUD_OVERLAY_SETTINGS_FILE = path.join(app.getPath("userData"), "hud-overlay-settings.json");
function isHudOverlayCaptureProtectionSupported(): boolean {
return process.platform !== "linux";
}
function loadHudOverlayCaptureProtectionSetting(): boolean {
if (hudOverlayCaptureProtectionLoaded) {
return hudOverlayHiddenFromCapture;
}
hudOverlayCaptureProtectionLoaded = true;
try {
if (!fs.existsSync(HUD_OVERLAY_SETTINGS_FILE)) {
return hudOverlayHiddenFromCapture;
}
const raw = fs.readFileSync(HUD_OVERLAY_SETTINGS_FILE, "utf-8");
const parsed = JSON.parse(raw) as { hiddenFromCapture?: unknown };
if (typeof parsed.hiddenFromCapture === "boolean") {
hudOverlayHiddenFromCapture = parsed.hiddenFromCapture;
}
} catch {
// Ignore settings read failures and fall back to defaults.
}
return hudOverlayHiddenFromCapture;
}
function persistHudOverlayCaptureProtectionSetting(enabled: boolean): void {
try {
fs.writeFileSync(
HUD_OVERLAY_SETTINGS_FILE,
JSON.stringify({ hiddenFromCapture: enabled }, null, 2),
"utf-8",
);
} catch {
// Ignore settings write failures and keep runtime state working.
}
}
function getScreen() {
return nodeRequire("electron").screen as typeof import("electron").screen;
@@ -29,16 +73,24 @@ ipcMain.on("hud-overlay-hide", () => {
});
ipcMain.handle("get-hud-overlay-capture-protection", () => {
const enabled = loadHudOverlayCaptureProtectionSetting();
return {
success: true,
enabled: hudOverlayHiddenFromCapture,
enabled,
};
});
ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) => {
loadHudOverlayCaptureProtectionSetting();
hudOverlayHiddenFromCapture = Boolean(enabled);
persistHudOverlayCaptureProtectionSetting(hudOverlayHiddenFromCapture);
if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) {
if (
isHudOverlayCaptureProtectionSupported() &&
hudOverlayWindow &&
!hudOverlayWindow.isDestroyed()
) {
hudOverlayWindow.setContentProtection(hudOverlayHiddenFromCapture);
}
@@ -49,6 +101,8 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean)
});
export function createHudOverlayWindow(): BrowserWindow {
loadHudOverlayCaptureProtectionSetting();
const primaryDisplay = getScreen().getPrimaryDisplay();
const { workArea } = primaryDisplay;
@@ -81,7 +135,9 @@ export function createHudOverlayWindow(): BrowserWindow {
},
});
win.setContentProtection(hudOverlayHiddenFromCapture);
if (isHudOverlayCaptureProtectionSupported()) {
win.setContentProtection(hudOverlayHiddenFromCapture);
}
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
+43 -16
View File
@@ -85,7 +85,8 @@ export function LaunchWindow() {
const [selectedSource, setSelectedSource] = useState("Screen");
const [hasSelectedSource, setHasSelectedSource] = useState(false);
const [recordingsDirectory, setRecordingsDirectory] = useState<string | null>(null);
const [hideHudFromCapture, setHideHudFromCapture] = useState(false);
const [hideHudFromCapture, setHideHudFromCapture] = useState(true);
const [platform, setPlatform] = useState<string | null>(null);
useEffect(() => {
const checkSelectedSource = async () => {
@@ -106,6 +107,27 @@ export function LaunchWindow() {
return () => clearInterval(interval);
}, []);
useEffect(() => {
let cancelled = false;
const loadPlatform = async () => {
try {
const nextPlatform = await window.electronAPI.getPlatform();
if (!cancelled) {
setPlatform(nextPlatform);
}
} catch (error) {
console.error("Failed to load platform:", error);
}
};
void loadPlatform();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
let cancelled = false;
@@ -204,6 +226,7 @@ export function LaunchWindow() {
? recordingsDirectory.split(/[\\/]/).filter(Boolean).pop() || recordingsDirectory
: "recordings";
const dividerClass = "mx-1 h-5 w-px shrink-0 bg-white/35";
const supportsHudCaptureProtection = platform !== "linux";
const toggleMicrophone = () => {
if (!recording) {
@@ -266,21 +289,25 @@ export function LaunchWindow() {
<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>
{supportsHudCaptureProtection && (
<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-white/35" />
) : (
<Eye size={16} className="text-[#2563EB]" />
)}
</Button>
)}
<Button
variant="link"
size="icon"
-208
View File
@@ -1,210 +1,2 @@
/// <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;
}>;
};
}