Files
Recordly/electron/windows.ts
T
webadderall 65a4222294 Fix Recordly 2.0 part 2 follow-up review findings
Preserve existing PR history and integrate validated second-cycle fixes.
2026-11-07 15:57:57 +11:00

1553 lines
46 KiB
TypeScript

import { nativeT } from "./nativeI18n";
import { recordingEditorLoading } from "./recordingEditorLoading";
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { app, BrowserWindow, ipcMain, type WebContents } from "electron";
import {
getHudCaptureExcludedProcessIds,
shouldProtectHudCapture,
supportsHudCaptureProtection,
} from "../src/lib/hudCaptureProtection";
import { USER_DATA_PATH } from "./appPaths";
import { registerDesktopPresenceWindow } from "./desktopPresence";
import { resolveEditorStartupView } from "./editorStartupView";
import { isHudInEditorMode } from "./hudEditorMode";
import {
getHudOverlayWindowBounds,
resizeHudOverlayFallbackBounds,
shouldExpandHudOverlayFallback,
} from "./hudOverlayBounds";
import { getHudOverlayTaskbarOptions } from "./hudOverlayWindowOptions";
import { sendNativeHudCaptureExclusions } from "./ipc/recording/nativeCaptureCommand";
import {
currentProjectPath,
currentRecordingSession,
currentVideoPath,
nativeCaptureProcess,
} from "./ipc/state";
import type { CaptureArea } from "./ipc/types";
import { outsideRecordingRects } from "./recordingFocus";
import { getPackagedRendererBaseUrl } from "./rendererServer";
const electronWindowsDir = path.dirname(fileURLToPath(import.meta.url));
const nodeRequire = createRequire(import.meta.url);
const APP_ROOT = path.join(electronWindowsDir, "..");
const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"];
const RENDERER_DIST = path.join(APP_ROOT, "dist");
const WINDOW_ICON_FILENAME =
process.platform === "darwin" ? "recordlymac-512.png" : "recordly-512.png";
const WINDOW_ICON_PATH = path.join(
process.env.VITE_PUBLIC || RENDERER_DIST,
"app-icons",
WINDOW_ICON_FILENAME,
);
let hudOverlayWindow: BrowserWindow | null = null;
let hudOverlayHiddenFromCapture = true;
let hudOverlayCaptureProtectionLoaded = false;
let hudOverlayFallbackExpanded = false;
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 capturePickerWindows: BrowserWindow[] = [];
let areaRecordingBorderWindow: BrowserWindow | null = null;
let recordingFocusWindows: BrowserWindow[] = [];
let updateToastWindow: BrowserWindow | null = null;
let onboardingWindow: BrowserWindow | null = null;
let hudWasVisibleBeforeUpdateToast = false;
const HUD_OVERLAY_SETTINGS_FILE = path.join(USER_DATA_PATH, "hud-overlay-settings.json");
const HUD_EDGE_MARGIN_DIP = 16;
const UPDATE_TOAST_WIDTH = 420;
const UPDATE_TOAST_HEIGHT = 236;
/** Shows a capture picker that never reported ready. */
const CAPTURE_PICKER_READY_TIMEOUT_MS = 1500;
// Synchronous and metadata-only so the HTML boot shell matches its destination,
// including reloads of a Home window after a fresh recording finishes.
ipcMain.on("get-editor-startup-view", (event) => {
event.returnValue = recordingEditorLoading.active ? "editor" : resolveEditorStartupView({
projectPath: currentProjectPath,
videoPath: currentVideoPath,
recordingVideoPath: currentRecordingSession?.videoPath,
devInput: process.env.RECORDLY_DEV_OPEN_RECORDING_INPUT,
smokeExport: process.env.RECORDLY_SMOKE_EXPORT === "1",
});
});
function getEditorWindowQuery(): Record<string, string> {
const query: Record<string, string> = {
windowType: "editor",
};
if (process.env.RECORDLY_DEV_OPEN_RECORDING_INPUT) {
query.devOpenInput = process.env.RECORDLY_DEV_OPEN_RECORDING_INPUT;
}
if (process.env.RECORDLY_DEV_OPEN_RECORDING_WEBCAM) {
query.devOpenWebcam = process.env.RECORDLY_DEV_OPEN_RECORDING_WEBCAM;
}
if (process.env.RECORDLY_SMOKE_EXPORT === "1") {
query.smokeExport = "1";
if (process.env.RECORDLY_SMOKE_EXPORT_INPUT) {
query.smokeInput = process.env.RECORDLY_SMOKE_EXPORT_INPUT;
}
if (process.env.RECORDLY_SMOKE_EXPORT_OUTPUT) {
query.smokeOutput = process.env.RECORDLY_SMOKE_EXPORT_OUTPUT;
}
if (process.env.RECORDLY_SMOKE_EXPORT_USE_NATIVE === "1") {
query.smokeUseNativeExport = "1";
}
if (process.env.RECORDLY_SMOKE_EXPORT_ENCODING_MODE) {
query.smokeEncodingMode = process.env.RECORDLY_SMOKE_EXPORT_ENCODING_MODE;
}
if (process.env.RECORDLY_SMOKE_EXPORT_SHADOW_INTENSITY) {
query.smokeShadowIntensity = process.env.RECORDLY_SMOKE_EXPORT_SHADOW_INTENSITY;
}
if (process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_INPUT) {
query.smokeWebcamInput = process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_INPUT;
}
if (process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_SHADOW) {
query.smokeWebcamShadow = process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_SHADOW;
}
if (process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_SIZE) {
query.smokeWebcamSize = process.env.RECORDLY_SMOKE_EXPORT_WEBCAM_SIZE;
}
if (process.env.RECORDLY_SMOKE_EXPORT_PIPELINE) {
query.smokePipelineModel = process.env.RECORDLY_SMOKE_EXPORT_PIPELINE;
}
if (process.env.RECORDLY_SMOKE_EXPORT_BACKEND) {
query.smokeBackendPreference = process.env.RECORDLY_SMOKE_EXPORT_BACKEND;
}
if (process.env.RECORDLY_SMOKE_EXPORT_RENDER_BACKEND) {
query.smokeRenderBackend = process.env.RECORDLY_SMOKE_EXPORT_RENDER_BACKEND;
}
if (process.env.RECORDLY_SMOKE_EXPORT_MAX_ENCODE_QUEUE) {
query.smokeMaxEncodeQueue = process.env.RECORDLY_SMOKE_EXPORT_MAX_ENCODE_QUEUE;
}
if (process.env.RECORDLY_SMOKE_EXPORT_MAX_DECODE_QUEUE) {
query.smokeMaxDecodeQueue = process.env.RECORDLY_SMOKE_EXPORT_MAX_DECODE_QUEUE;
}
if (process.env.RECORDLY_SMOKE_EXPORT_MAX_PENDING_FRAMES) {
query.smokeMaxPendingFrames = process.env.RECORDLY_SMOKE_EXPORT_MAX_PENDING_FRAMES;
}
if (process.env.RECORDLY_SMOKE_EXPORT_PROJECT) {
query.smokeProject = process.env.RECORDLY_SMOKE_EXPORT_PROJECT;
}
if (process.env.RECORDLY_SMOKE_EXPORT_QUALITY) {
query.smokeQuality = process.env.RECORDLY_SMOKE_EXPORT_QUALITY;
}
if (process.env.RECORDLY_SMOKE_EXPORT_FPS) {
query.smokeFps = process.env.RECORDLY_SMOKE_EXPORT_FPS;
}
}
return query;
}
export function isHudOverlayMousePassthroughSupported(): 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;
}
export function getHudOverlayCaptureProtectionEnabled(): boolean {
return loadHudOverlayCaptureProtectionSetting();
}
function applyHudOverlayCaptureProtectionToWindow(hud: BrowserWindow, enabled: boolean): void {
if (!supportsHudCaptureProtection(process.platform)) {
return;
}
try {
// Keep the idle HUD visible to screenshots and other capture applications.
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();
if (!hud) {
return enabled;
}
applyHudOverlayCaptureProtectionToWindow(hud, enabled);
return enabled;
}
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() {
if (!app.isReady()) {
throw new Error(
"getScreen() called before app is ready. Ensure all screen access happens after app.whenReady().",
);
}
return nodeRequire("electron").screen as typeof import("electron").screen;
}
function getHudOverlayDisplay() {
const hudWindow = getHudOverlayWindow();
if (hudWindow) {
return getScreen().getDisplayMatching(hudWindow.getBounds());
}
return getScreen().getPrimaryDisplay();
}
function getHudOverlayBounds() {
const { workArea } = getHudOverlayDisplay();
const fallbackExpanded = shouldExpandHudOverlayFallback({
fallbackExpanded: hudOverlayFallbackExpanded,
recordingActive: hudOverlayRecordingActive,
webcamPreviewVisible: hudOverlayWebcamPreviewVisible,
});
return getHudOverlayWindowBounds(
workArea,
isHudOverlayMousePassthroughSupported(),
fallbackExpanded,
);
}
function applyHudOverlayBounds() {
if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) {
return;
}
hudOverlayWindow.setBounds(getHudOverlayBounds(), false);
positionUpdateToastWindow();
if (!hudOverlayWindow.isVisible()) {
return;
}
hudOverlayWindow.moveTop();
}
function getUpdateToastBounds() {
const hudWindow = getHudOverlayWindow();
if (hudWindow) {
const hudBounds = hudWindow.getBounds();
const display = getScreen().getDisplayMatching(hudBounds);
const { workArea } = display;
const x = Math.round(workArea.x + (workArea.width - UPDATE_TOAST_WIDTH) / 2);
const y = Math.round(
workArea.y + workArea.height - UPDATE_TOAST_HEIGHT - HUD_EDGE_MARGIN_DIP,
);
return {
x,
y,
width: UPDATE_TOAST_WIDTH,
height: UPDATE_TOAST_HEIGHT,
};
}
const primaryDisplay = getScreen().getPrimaryDisplay();
const { workArea } = primaryDisplay;
return {
x: Math.round(workArea.x + (workArea.width - UPDATE_TOAST_WIDTH) / 2),
y: Math.round(workArea.y + workArea.height - UPDATE_TOAST_HEIGHT - HUD_EDGE_MARGIN_DIP),
width: UPDATE_TOAST_WIDTH,
height: UPDATE_TOAST_HEIGHT,
};
}
function positionUpdateToastWindow() {
if (!updateToastWindow || updateToastWindow.isDestroyed()) {
return;
}
updateToastWindow.setBounds(getUpdateToastBounds(), false);
updateToastWindow.moveTop();
}
function setHudOverlayFallbackExpanded(expanded: boolean) {
if (hudOverlayRecordingActive) {
hudOverlayFallbackExpanded = false;
return;
}
hudOverlayFallbackExpanded = expanded;
if (
!hudOverlayWindow ||
hudOverlayWindow.isDestroyed() ||
isHudOverlayMousePassthroughSupported()
) {
return;
}
const { workArea } = getHudOverlayDisplay();
const nextBounds = resizeHudOverlayFallbackBounds(
workArea,
hudOverlayWindow.getBounds(),
expanded,
);
hudOverlayWindow.setBounds(nextBounds, false);
positionUpdateToastWindow();
if (hudOverlayWindow.isVisible()) {
hudOverlayWindow.moveTop();
}
}
function setHudOverlayMousePassthrough(ignore: boolean) {
// Retain the latest requested state while source selection temporarily forces passthrough.
hudOverlayIgnoringMouse = ignore;
const effectiveIgnore =
hudOverlaySourceSelectionActive && !hudOverlayRecordingActive ? true : ignore;
if (hudOverlayMouseReassertTimer) {
clearTimeout(hudOverlayMouseReassertTimer);
hudOverlayMouseReassertTimer = null;
}
if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) {
return;
}
if (hudOverlayRecordingActive) {
hudOverlayFallbackExpanded = false;
applyHudOverlayBounds();
}
if (!isHudOverlayMousePassthroughSupported()) {
if (process.platform !== "linux") {
setHudOverlayFallbackExpanded(!effectiveIgnore);
}
hudOverlayWindow.setIgnoreMouseEvents(false);
return;
}
if (effectiveIgnore) {
hudOverlayWindow.setIgnoreMouseEvents(true, { forward: true });
return;
}
hudOverlayWindow.setIgnoreMouseEvents(false);
}
ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => {
setHudOverlayMousePassthrough(Boolean(ignore));
});
ipcMain.on("hud-overlay-set-source-selection-active", (_event, active: boolean) => {
hudOverlaySourceSelectionActive = Boolean(active);
if (hudOverlaySourceSelectionActive) {
hudOverlayFallbackExpanded = false;
applyHudOverlayBounds();
setHudOverlayMousePassthrough(hudOverlayIgnoringMouse);
return;
}
setHudOverlayMousePassthrough(hudOverlayIgnoringMouse);
});
// Keep compatibility with existing drag IPC/state.
let hudUserPosition: { x: number; y: number } | null = null;
let hudDragOffset: { x: number; y: number } | null = null;
let hudDragLastCursor: { x: number; y: number } | null = null;
let hudDragFixedSize: { width: number; height: number } | null = null;
ipcMain.on("hud-overlay-drag", (_event, phase: string, screenX: number, screenY: number) => {
if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) return;
// On Linux the compositor (especially Wayland) refuses programmatic window
// placement, so BrowserWindow.setBounds() with x/y is silently ignored and
// the HUD appears "stuck". The renderer marks the drag handle as
// -webkit-app-region: drag on Linux, letting the OS move the window for us.
// The resulting position is captured by the win.on("moved", ...) listener
// below so `hudUserPosition` stays in sync.
if (process.platform === "linux") {
return;
}
if (phase === "start") {
const bounds = hudOverlayWindow.getBounds();
hudDragOffset = { x: screenX - bounds.x, y: screenY - bounds.y };
hudDragLastCursor = { x: screenX, y: screenY };
hudDragFixedSize = { width: bounds.width, height: bounds.height };
} else if (phase === "move" && hudDragOffset) {
if (
hudDragLastCursor &&
hudDragLastCursor.x === screenX &&
hudDragLastCursor.y === screenY
) {
return;
}
hudDragLastCursor = { x: screenX, y: screenY };
const targetX = Math.round(screenX - hudDragOffset.x);
const targetY = Math.round(screenY - hudDragOffset.y);
const fixedWidth = hudDragFixedSize?.width ?? hudOverlayWindow.getBounds().width;
const fixedHeight = hudDragFixedSize?.height ?? hudOverlayWindow.getBounds().height;
hudOverlayWindow.setBounds(
{
x: targetX,
y: targetY,
width: fixedWidth,
height: fixedHeight,
},
false,
);
} else if (phase === "end") {
const finalBounds = hudOverlayWindow.getBounds();
hudUserPosition = { x: finalBounds.x, y: finalBounds.y };
hudDragOffset = null;
hudDragLastCursor = null;
hudDragFixedSize = null;
}
});
ipcMain.on("hud-overlay-hide", () => {
if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) {
hudOverlayWindow.minimize();
}
});
ipcMain.handle("get-hud-overlay-capture-protection", () => {
const enabled = loadHudOverlayCaptureProtectionSetting();
return {
success: true,
enabled,
};
});
ipcMain.handle("get-hud-overlay-mouse-passthrough-supported", () => {
return {
success: true,
supported: isHudOverlayMousePassthroughSupported(),
};
});
ipcMain.on("hud-overlay-set-webcam-preview-visible", (_event, visible: boolean) => {
const nextVisible = Boolean(visible);
if (hudOverlayWebcamPreviewVisible === nextVisible) {
return;
}
hudOverlayWebcamPreviewVisible = nextVisible;
if (hudOverlayRecordingActive) {
applyHudOverlayBounds();
}
});
let hudCaptureProtectionUpdates = Promise.resolve();
ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) => {
// Settings can be open in more than one window. Match each helper acknowledgement
// to one change and persist preferences in the same order as capture updates.
const update = hudCaptureProtectionUpdates.then(() => setHudCaptureProtection(enabled));
hudCaptureProtectionUpdates = update.then(
() => undefined,
() => undefined,
);
return update;
});
async function setHudCaptureProtection(enabled: boolean) {
loadHudOverlayCaptureProtectionSetting();
// ScreenCaptureKit ignores window content protection. Update its live filter
// before saving the preference or reporting that the change succeeded.
if (process.platform === "darwin" && nativeCaptureProcess) {
try {
const excludedProcessIds = getHudCaptureExcludedProcessIds(
"darwin",
Boolean(enabled),
process.pid,
);
await sendNativeHudCaptureExclusions(nativeCaptureProcess, excludedProcessIds);
} catch (error) {
console.warn("Failed to update HUD capture filter:", error);
return { success: false, enabled: hudOverlayHiddenFromCapture };
}
}
hudOverlayHiddenFromCapture = Boolean(enabled);
persistHudOverlayCaptureProtectionSetting(hudOverlayHiddenFromCapture);
reassertHudOverlayCaptureProtection();
return {
success: true,
enabled: hudOverlayHiddenFromCapture,
};
}
const editorWindows = new Set<BrowserWindow>();
let recordingPreparationActive = false;
function getHudEditorMode() {
return isHudInEditorMode(
editorWindows.size,
recordingPreparationActive,
hudOverlayRecordingActive,
);
}
export function setHudRecordingPreparationActive(active: boolean) {
recordingPreparationActive = active;
notifyEditorMode();
}
function notifyEditorMode() {
if (hudOverlayWindow && !hudOverlayWindow.webContents.isDestroyed()) {
hudOverlayWindow.webContents.send("editor-mode-changed", getHudEditorMode());
}
}
ipcMain.handle("get-editor-mode", getHudEditorMode);
export function createHudOverlayWindow(): BrowserWindow {
const perfStart = Date.now();
loadHudOverlayCaptureProtectionSetting();
hudOverlayFallbackExpanded = false;
hudOverlayWebcamPreviewVisible = false;
const initialBounds = getHudOverlayBounds();
let hasShownHudWindow = false;
const win = new BrowserWindow({
width: initialBounds.width,
height: initialBounds.height,
x: initialBounds.x,
y: initialBounds.y,
frame: false,
transparent: true,
backgroundColor: "#00000000",
resizable: false,
alwaysOnTop: true,
// The HUD is Recordly's persistent top-level window, so it owns the
// Windows taskbar entry while auxiliary overlays stay hidden there.
...getHudOverlayTaskbarOptions(process.platform),
hasShadow: false,
show: false,
webPreferences: {
preload: path.join(electronWindowsDir, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
backgroundThrottling: false,
},
});
// Keep the recording controls and webcam above normal and full-screen apps.
registerDesktopPresenceWindow(win, process.platform === "win32", true);
// Transparent regions remain click-through via setIgnoreMouseEvents().
win.setAlwaysOnTop(true, "screen-saver");
if (process.platform === "darwin") {
win.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true,
skipTransformProcessType: true,
});
}
const showHudWindow = () => {
if (hasShownHudWindow || win.isDestroyed()) {
return;
}
if (
updateToastWindow &&
!updateToastWindow.isDestroyed() &&
updateToastWindow.isVisible()
) {
hudWasVisibleBeforeUpdateToast = true;
return;
}
hasShownHudWindow = true;
// Showing or changing native window state can recreate platform window
// flags. Reassert capture protection on both sides of the transition.
applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture);
if (process.platform === "win32") {
// A focusable window is required for a Windows taskbar entry, but the
// always-on-top HUD must not steal focus when Recordly starts.
win.showInactive();
} else {
win.show();
}
win.moveTop();
applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture);
if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) {
win.setIgnoreMouseEvents(false);
setTimeout(() => {
if (!win.isDestroyed()) {
setHudOverlayMousePassthrough(hudOverlayIgnoringMouse);
}
}, 50);
}
};
applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture);
win.on("show", () => {
if (!win.isDestroyed()) {
applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture);
}
});
if (isHudOverlayMousePassthroughSupported()) {
if (hudOverlayRecordingActive) {
hudOverlayIgnoringMouse = false;
win.setIgnoreMouseEvents(false);
} else {
hudOverlayIgnoringMouse = true;
win.setIgnoreMouseEvents(true, { forward: true });
}
}
// On Windows 11+, focus changes (e.g. showing a native notification) can break
// setIgnoreMouseEvents forwarding on a transparent always-on-top window, making
// it permanently click-through without hover detection. Re-initialise the
// pass-through-with-forwarding state whenever the window gains focus by toggling
// the flag off then back on so the native WS_EX_TRANSPARENT flag is fully reset.
if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) {
win.on("focus", () => {
if (!win.isDestroyed()) {
win.setIgnoreMouseEvents(false);
setTimeout(() => {
if (!win.isDestroyed()) {
setHudOverlayMousePassthrough(hudOverlayIgnoringMouse);
}
}, 50);
}
});
}
win.webContents.on("did-finish-load", () => {
console.log(`[PERF:MAIN] HUD Window: did-finish-load in ${Date.now() - perfStart}ms`);
win?.webContents.send("main-process-message", new Date().toLocaleString());
// Safety fallback if renderer-ready signal never arrives.
setTimeout(() => {
showHudWindow();
}, 1800);
});
// Safety net: on Linux the renderer may fail to fire did-finish-load
// (for example due to GPU/VAAPI startup issues). Show the window after
// ready-to-show as a fallback so the HUD still appears.
win.once("ready-to-show", () => {
setTimeout(() => {
if (!win.isDestroyed() && !win.isVisible()) {
showHudWindow();
}
}, 500);
});
const handleHudRendererReady = () => {
if (!win.isDestroyed()) {
console.log(`[PERF:MAIN] HUD Window: renderer-ready in ${Date.now() - perfStart}ms`);
showHudWindow();
}
};
ipcMain.on("hud-overlay-renderer-ready", handleHudRendererReady);
hudOverlayWindow = win;
// On Linux the HUD is dragged by the OS via -webkit-app-region (Wayland
// forbids client-side positioning). Mirror moved bounds into drag state.
if (process.platform === "linux") {
win.on("moved", () => {
if (win.isDestroyed()) return;
const { x, y } = win.getBounds();
hudUserPosition = { x, y };
});
}
// Reset the user's saved HUD position when displays change so the bar
// doesn't end up stranded off-screen after a monitor is disconnected.
const screen = getScreen();
const handleDisplayRemoved = () => {
hudUserPosition = null;
};
const handleDisplayMetricsChanged = () => {
if (hudUserPosition) {
const displays = screen.getAllDisplays();
const onScreen = displays.some(
(d) =>
hudUserPosition!.x >= d.workArea.x &&
hudUserPosition!.x < d.workArea.x + d.workArea.width &&
hudUserPosition!.y >= d.workArea.y &&
hudUserPosition!.y < d.workArea.y + d.workArea.height,
);
if (!onScreen) {
hudUserPosition = null;
}
}
applyHudOverlayBounds();
};
screen.on("display-removed", handleDisplayRemoved);
screen.on("display-metrics-changed", handleDisplayMetricsChanged);
win.on("closed", () => {
ipcMain.removeListener("hud-overlay-renderer-ready", handleHudRendererReady);
screen.removeListener("display-removed", handleDisplayRemoved);
screen.removeListener("display-metrics-changed", handleDisplayMetricsChanged);
if (hudOverlayWindow === win) {
hudOverlayWindow = null;
recordingPreparationActive = false;
hudCaptureStarting = false;
hudOverlayRecordingActive = false;
}
});
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=hud-overlay");
} else {
loadPackagedRendererWindow(win, { windowType: "hud-overlay" });
}
return win;
}
export function getHudOverlayWindow(): BrowserWindow | null {
return hudOverlayWindow && !hudOverlayWindow.isDestroyed() ? hudOverlayWindow : null;
}
/**
* Re-initialise the HUD overlay's mouse passthrough state.
*
* On Windows 11+, any new BrowserWindow appearing (even focusable:false ones
* like the source highlight overlay) can silently corrupt the
* WS_EX_TRANSPARENT flag that backs setIgnoreMouseEvents forwarding. Call
* this after any operation that creates or destroys a sibling window so that
* hover detection on the HUD is immediately restored without requiring the
* user to move their mouse over the bar.
*/
export function reassertHudOverlayMousePassthrough(): void {
if (process.platform !== "win32" || !isHudOverlayMousePassthroughSupported()) {
return;
}
const hud = getHudOverlayWindow();
if (!hud) {
return;
}
// Toggle off then back on so the native WS_EX_TRANSPARENT flag is fully
// re-initialised rather than merely re-asserted in a potentially broken state.
hud.setIgnoreMouseEvents(false);
if (hudOverlayMouseReassertTimer) {
clearTimeout(hudOverlayMouseReassertTimer);
}
hudOverlayMouseReassertTimer = setTimeout(() => {
hudOverlayMouseReassertTimer = null;
if (!hud.isDestroyed()) {
setHudOverlayMousePassthrough(hudOverlayIgnoringMouse);
}
}, 50);
}
export function setHudOverlayRecordingActive(recording: boolean): void {
hudCaptureStarting = false;
hudOverlayRecordingActive = Boolean(recording);
notifyEditorMode();
hudOverlayFallbackExpanded = false;
applyHudOverlayBounds();
reassertHudOverlayCaptureProtection();
// Start in passthrough mode. Forwarded pointer movement lets the renderer
// make the visible HUD controls interactive when the pointer reaches them,
// while transparent parts never block the recorded application.
setHudOverlayMousePassthrough(true);
}
export function createUpdateToastWindow(): BrowserWindow {
const initialBounds = getUpdateToastBounds();
const win = new BrowserWindow({
width: initialBounds.width,
height: initialBounds.height,
x: initialBounds.x,
y: initialBounds.y,
frame: false,
transparent: true,
resizable: false,
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
show: false,
focusable: true,
backgroundColor: "#00000000",
webPreferences: {
preload: path.join(electronWindowsDir, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
backgroundThrottling: false,
},
});
if (process.platform === "darwin") {
win.setAlwaysOnTop(true, "status");
}
win.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true,
// Keep Recordly a foreground application so macOS does not temporarily
// remove its Dock icon while showing an overlay window.
skipTransformProcessType: process.platform === "darwin",
});
updateToastWindow = win;
win.on("closed", () => {
if (updateToastWindow === win) {
updateToastWindow = null;
}
restoreHudAfterUpdateToast();
});
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=update-toast");
} else {
win.loadFile(path.join(RENDERER_DIST, "index.html"), {
query: { windowType: "update-toast" },
});
}
return win;
}
export function getUpdateToastWindow(): BrowserWindow | null {
return updateToastWindow && !updateToastWindow.isDestroyed() ? updateToastWindow : null;
}
export function showUpdateToastWindow(): BrowserWindow {
const win = getUpdateToastWindow() ?? createUpdateToastWindow();
const hud = getHudOverlayWindow();
if (!win.isVisible()) {
hudWasVisibleBeforeUpdateToast = Boolean(hud?.isVisible());
}
if (hud?.isVisible()) {
hud.hide();
}
positionUpdateToastWindow();
if (!win.isVisible()) {
if (process.platform === "win32") {
win.show();
} else {
win.showInactive();
}
}
win.moveTop();
return win;
}
function restoreHudAfterUpdateToast(): void {
if (!hudWasVisibleBeforeUpdateToast) {
return;
}
hudWasVisibleBeforeUpdateToast = false;
const hud = getHudOverlayWindow();
if (!hud) {
return;
}
if (process.platform === "win32") {
hud.showInactive();
} else {
hud.show();
}
hud.moveTop();
setHudOverlayMousePassthrough(hudOverlayIgnoringMouse);
}
export function hideUpdateToastWindow(): void {
if (updateToastWindow && !updateToastWindow.isDestroyed()) {
updateToastWindow.hide();
}
restoreHudAfterUpdateToast();
}
function loadPackagedRendererWindow(win: BrowserWindow, query: Record<string, string>) {
const queryString = new URLSearchParams(query).toString();
const indexHtmlPath = path.join(RENDERER_DIST, "index.html");
const packagedRendererBaseUrl = getPackagedRendererBaseUrl();
const webContents = win.webContents;
const loadFromFile = () => {
if (win.isDestroyed()) {
return;
}
console.log("[editor-window] load-file", indexHtmlPath);
void win.loadFile(indexHtmlPath, { query });
};
if (!packagedRendererBaseUrl) {
loadFromFile();
return;
}
const targetUrl = `${packagedRendererBaseUrl}/?${queryString}`;
let settled = false;
let timeoutId: NodeJS.Timeout | null = setTimeout(() => {
fallbackToFile("load-timeout");
}, 5000);
const clearTimeoutIfNeeded = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
const detachLoadListeners = () => {
clearTimeoutIfNeeded();
if (webContents.isDestroyed()) {
return;
}
webContents.removeListener("did-fail-load", handleDidFailLoad);
webContents.removeListener("did-finish-load", handleDidFinishLoad);
};
const fallbackToFile = (reason: string, details?: Record<string, unknown>) => {
if (settled || win.isDestroyed()) {
return;
}
settled = true;
detachLoadListeners();
console.warn("[editor-window] packaged renderer URL failed, falling back to file", {
reason,
targetUrl,
...details,
});
loadFromFile();
};
const handleDidFailLoad = (
_event: Electron.Event,
errorCode: number,
errorDescription: string,
validatedURL: string,
isMainFrame: boolean,
) => {
if (!isMainFrame || validatedURL !== targetUrl) {
return;
}
fallbackToFile("did-fail-load", {
errorCode,
errorDescription,
validatedURL,
});
};
const handleDidFinishLoad = () => {
if (webContents.getURL() !== targetUrl) {
return;
}
settled = true;
detachLoadListeners();
};
webContents.on("did-fail-load", handleDidFailLoad);
webContents.on("did-finish-load", handleDidFinishLoad);
win.once("closed", clearTimeoutIfNeeded);
console.log("[editor-window] load-url", targetUrl);
void win.loadURL(targetUrl).catch((error) => {
fallbackToFile("load-url-rejected", {
error: error instanceof Error ? error.message : String(error),
});
});
}
export function createEditorWindow(): BrowserWindow {
const perfStart = Date.now();
console.log("[PERF:MAIN] createEditorWindow: STARTED");
const isMac = process.platform === "darwin";
const { workArea, workAreaSize } = getScreen().getPrimaryDisplay();
const initialWidth = isMac ? Math.round(workAreaSize.width * 0.85) : workArea.width;
const initialHeight = isMac ? Math.round(workAreaSize.height * 0.85) : workArea.height;
const win = new BrowserWindow({
width: initialWidth,
height: initialHeight,
...(!isMac && {
x: workArea.x,
y: workArea.y,
}),
minWidth: 800,
minHeight: 600,
...(process.platform !== "darwin" && {
icon: WINDOW_ICON_PATH,
}),
...(isMac && {
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 16, y: 20 },
}),
autoHideMenuBar: !isMac,
transparent: false,
resizable: true,
alwaysOnTop: false,
skipTaskbar: false,
title: "Recordly",
show: false,
backgroundColor: "#000000",
webPreferences: {
preload: path.join(electronWindowsDir, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
backgroundThrottling: false,
},
});
recordingPreparationActive = false;
registerDesktopPresenceWindow(win);
editorWindows.add(win);
notifyEditorMode();
win.once("closed", () => {
editorWindows.delete(win);
notifyEditorMode();
});
const publishWindowChrome = () => {
if (!win.isDestroyed())
win.webContents.send("window-chrome-changed", {
trafficLightsVisible: isMac && !win.isFullScreen() && !win.isSimpleFullScreen(),
});
};
win.on("enter-full-screen", publishWindowChrome);
win.on("leave-full-screen", publishWindowChrome);
win.on("resize", publishWindowChrome);
win.webContents.on("did-finish-load", publishWindowChrome);
win.once("ready-to-show", () => {
console.log(`[PERF:MAIN] Editor Window: ready-to-show in ${Date.now() - perfStart}ms`);
win.show();
});
win.webContents.on("did-finish-load", () => {
console.log(`[PERF:MAIN] Editor Window: did-finish-load in ${Date.now() - perfStart}ms`);
win?.webContents.send("main-process-message", new Date().toLocaleString());
// Fallback for Linux/Wayland where `ready-to-show` may not fire reliably.
if (!win.isDestroyed() && !win.isVisible()) {
console.log("[editor-window] forcing show after did-finish-load");
win.show();
}
});
win.webContents.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL) => {
console.error("[editor-window] did-fail-load", {
errorCode,
errorDescription,
validatedURL,
});
});
win.webContents.on("render-process-gone", (_event, details) => {
console.error("[editor-window] render-process-gone", details);
});
win.on("show", () => {
console.log("[editor-window] show");
});
win.on("focus", () => {
console.log("[editor-window] focus");
});
win.on("enter-full-screen", () => {
if (!win.isDestroyed()) win.webContents.send("window-fullscreen-changed", true);
});
win.on("leave-full-screen", () => {
if (!win.isDestroyed()) win.webContents.send("window-fullscreen-changed", false);
});
if (VITE_DEV_SERVER_URL) {
const query = new URLSearchParams(getEditorWindowQuery());
win.loadURL(`${VITE_DEV_SERVER_URL}?${query.toString()}`);
} else {
loadPackagedRendererWindow(win, getEditorWindowQuery());
}
return win;
}
function getOnboardingWindowBounds() {
const { x, y, width, height } = getScreen().getPrimaryDisplay().workArea;
const side = Math.min(width, Math.round(height * 0.85));
return {
x: Math.round(x + (width - side) / 2),
y: Math.round(y + (height - side) / 2),
width: side,
height: side,
};
}
function bringOnboardingWindowForward(win: BrowserWindow): void {
win.show();
if (process.platform === "darwin") app.focus({ steal: true });
win.moveTop();
win.focus();
}
export function getOnboardingWindow(): BrowserWindow | null {
return onboardingWindow && !onboardingWindow.isDestroyed() ? onboardingWindow : null;
}
function closeOtherOnboardingWindows(win: BrowserWindow) {
for (const other of BrowserWindow.getAllWindows()) {
if (other !== win && !other.isDestroyed()) other.close();
}
}
export function showOnboardingWindow(options?: { preview?: boolean }): BrowserWindow {
const query = {
windowType: "onboarding",
...(!app.isPackaged && options?.preview ? { onboardingPreview: "1" } : {}),
};
const loadOnboarding = (win: BrowserWindow) => {
if (VITE_DEV_SERVER_URL) {
void win.loadURL(`${VITE_DEV_SERVER_URL}?${new URLSearchParams(query)}`);
} else {
loadPackagedRendererWindow(win, query);
}
};
if (onboardingWindow && !onboardingWindow.isDestroyed()) {
closeOtherOnboardingWindows(onboardingWindow);
if (options?.preview) loadOnboarding(onboardingWindow);
if (onboardingWindow.isMinimized()) onboardingWindow.restore();
onboardingWindow.setBounds(getOnboardingWindowBounds());
bringOnboardingWindowForward(onboardingWindow);
return onboardingWindow;
}
const bounds = getOnboardingWindowBounds();
const minimumSide = Math.min(360, bounds.width);
const win = new BrowserWindow({
...bounds,
minWidth: minimumSide,
minHeight: minimumSide,
...(process.platform !== "darwin" && { icon: WINDOW_ICON_PATH }),
frame: false,
alwaysOnTop: false,
autoHideMenuBar: true,
resizable: true,
show: false,
backgroundColor: "#111214",
title: nativeT("welcome"),
webPreferences: {
preload: path.join(electronWindowsDir, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
backgroundThrottling: false,
},
});
registerDesktopPresenceWindow(win);
if (process.platform !== "darwin") win.setMenu(null);
onboardingWindow = win;
win.once("closed", () => {
if (onboardingWindow === win) onboardingWindow = null;
});
win.once("ready-to-show", () => {
win.setBounds(bounds);
bringOnboardingWindowForward(win);
});
// Keep onboarding alive before closing other windows to avoid window-all-closed.
closeOtherOnboardingWindows(win);
loadOnboarding(win);
return win;
}
export function closeOnboardingWindow(): void {
if (onboardingWindow && !onboardingWindow.isDestroyed()) onboardingWindow.close();
}
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,
show: false,
...(process.platform !== "darwin" && {
icon: WINDOW_ICON_PATH,
}),
backgroundColor: "#00000000",
webPreferences: {
preload: path.join(electronWindowsDir, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
},
});
win.webContents.on("did-finish-load", () => {
setTimeout(() => {
if (!win.isDestroyed()) {
win.show();
}
}, 100);
});
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;
}
export function createCountdownWindow(): BrowserWindow {
const primaryDisplay = getScreen().getPrimaryDisplay();
const { width, height } = primaryDisplay.workAreaSize;
const windowSize = 200;
const x = Math.floor((width - windowSize) / 2);
const y = Math.floor((height - windowSize) / 2);
const win = new BrowserWindow({
width: windowSize,
height: windowSize,
x: x,
y: y,
frame: false,
transparent: true,
resizable: false,
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
focusable: true,
show: false,
webPreferences: {
preload: path.join(electronWindowsDir, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
},
});
countdownWindow = win;
win.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true,
// Keep Recordly a foreground application so macOS does not temporarily
// remove its Dock icon while showing the countdown.
skipTransformProcessType: process.platform === "darwin",
});
win.webContents.on("did-finish-load", () => {
if (!win.isDestroyed()) {
if (process.platform === "win32") {
win.showInactive();
win.moveTop();
} else {
win.show();
}
}
});
win.on("closed", () => {
if (countdownWindow === win) {
countdownWindow = null;
}
});
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=countdown");
} else {
win.loadFile(path.join(RENDERER_DIST, "index.html"), {
query: { windowType: "countdown" },
});
}
return win;
}
export function getCountdownWindow(): BrowserWindow | null {
return countdownWindow;
}
export function closeCountdownWindow(): void {
if (countdownWindow && !countdownWindow.isDestroyed()) {
countdownWindow.close();
countdownWindow = null;
}
}
/**
* Covers every display with a transparent overlay for picking what to record.
* Each overlay renders the `capture-picker` window and reports back over IPC.
*/
export function createCapturePickerWindows(onClosed: () => void): BrowserWindow[] {
closeCapturePickerWindows();
const cursorDisplayId = getScreen().getDisplayNearestPoint(
getScreen().getCursorScreenPoint(),
).id;
capturePickerWindows = getScreen()
.getAllDisplays()
.map((display) => {
const win = new BrowserWindow({
...display.bounds,
frame: false,
transparent: true,
resizable: false,
movable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
enableLargerThanScreen: true,
show: false,
backgroundColor: "#00000000",
...(process.platform === "darwin" ? { type: "panel" as const } : {}),
webPreferences: {
preload: path.join(electronWindowsDir, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
},
});
// Above the menu bar and the HUD, on every Space and over full-screen apps.
win.setAlwaysOnTop(true, "screen-saver");
win.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true,
skipTransformProcessType: true,
});
win.setBounds(display.bounds);
// The page paints the app's opaque background before React makes it
// transparent, so the overlay stays hidden until the picker has painted.
let shown = false;
const show = () => {
if (shown || win.isDestroyed()) return;
shown = true;
win.show();
if (display.id === cursorDisplayId) {
app.focus({ steal: true });
win.focus();
}
};
win.webContents.ipc.once("capture-picker-ready", show);
win.webContents.once("did-finish-load", () => {
setTimeout(show, CAPTURE_PICKER_READY_TIMEOUT_MS);
});
win.on("closed", () => {
const wasOpen = capturePickerWindows.includes(win);
capturePickerWindows = capturePickerWindows.filter(
(candidate) => candidate !== win,
);
if (wasOpen) onClosed();
});
const query = { windowType: "capture-picker", displayId: String(display.id) };
if (VITE_DEV_SERVER_URL) {
win.loadURL(`${VITE_DEV_SERVER_URL}?${new URLSearchParams(query).toString()}`);
} else {
win.loadFile(path.join(RENDERER_DIST, "index.html"), { query });
}
return win;
});
return capturePickerWindows;
}
export function getCapturePickerWindows(): BrowserWindow[] {
return [...capturePickerWindows];
}
export function closeCapturePickerWindows(): void {
const windows = capturePickerWindows;
capturePickerWindows = [];
for (const win of windows) {
if (!win.isDestroyed()) win.close();
}
}
/** Whether a message came from one of the open picker overlays. */
export function isCapturePickerWebContents(contents: WebContents): boolean {
return capturePickerWindows.some((win) => !win.isDestroyed() && win.webContents === contents);
}
/**
* Outlines the area being recorded. The outline sits just outside the recorded
* rectangle and belongs to Recordly, which recordings exclude, so it never shows
* up in the video.
*/
export function showAreaRecordingBorder(area: {
x: number;
y: number;
width: number;
height: number;
}): void {
hideAreaRecordingBorder();
const outset = 3;
const win = new BrowserWindow({
x: Math.round(area.x) - outset,
y: Math.round(area.y) - outset,
width: Math.round(area.width) + outset * 2,
height: Math.round(area.height) + outset * 2,
frame: false,
transparent: true,
resizable: false,
movable: false,
focusable: false,
skipTaskbar: true,
hasShadow: false,
enableLargerThanScreen: true,
show: false,
backgroundColor: "#00000000",
...(process.platform === "darwin" ? { type: "panel" as const } : {}),
webPreferences: { nodeIntegration: false, contextIsolation: true },
});
win.setIgnoreMouseEvents(true);
win.setAlwaysOnTop(true, "screen-saver");
win.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true,
skipTransformProcessType: true,
});
win.setContentProtection(true);
areaRecordingBorderWindow = win;
win.on("closed", () => {
if (areaRecordingBorderWindow === win) areaRecordingBorderWindow = null;
});
const html = `<!DOCTYPE html><html><head><style>
html,body{margin:0;height:100%;background:transparent;overflow:hidden}
div{position:fixed;inset:0;border:2px dashed rgba(255,255,255,.95);box-shadow:0 0 0 1px rgba(0,0,0,.45),inset 0 0 0 1px rgba(0,0,0,.45)}
</style></head><body><div></div></body></html>`;
void win
.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`)
.then(() => {
if (!win.isDestroyed()) win.showInactive();
})
.catch(() => {
if (!win.isDestroyed()) win.close();
});
}
export function hideAreaRecordingBorder(): void {
const win = areaRecordingBorderWindow;
areaRecordingBorderWindow = null;
if (win && !win.isDestroyed()) win.close();
}
/** Click-through exterior strips leave the recorded pixels completely untouched. */
export function showRecordingFocus(target: CaptureArea): void {
const rectangles = outsideRecordingRects(
getScreen()
.getAllDisplays()
.map((display) => display.bounds),
target,
);
for (let i = 0; i < rectangles.length; i++) {
const bounds = rectangles[i];
let win = recordingFocusWindows[i];
if (!win || win.isDestroyed()) {
win = new BrowserWindow({
...bounds,
frame: false,
// Exterior strips must meet flush; native rounding leaves undimmed corner gaps.
roundedCorners: false,
transparent: true,
resizable: false,
movable: false,
focusable: false,
skipTaskbar: true,
hasShadow: false,
enableLargerThanScreen: true,
show: false,
backgroundColor: "#00000000",
...(process.platform === "darwin" ? { type: "panel" as const } : {}),
webPreferences: { nodeIntegration: false, contextIsolation: true },
});
win.setIgnoreMouseEvents(true);
win.setContentProtection(true);
win.setAlwaysOnTop(true, "floating", -1);
win.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true,
skipTransformProcessType: true,
});
recordingFocusWindows[i] = win;
const created = win;
void win
.loadURL(
`data:text/html;charset=utf-8,${encodeURIComponent('<!DOCTYPE html><html style="margin:0;height:100%;background:rgba(0,0,0,.38)"><body style="margin:0"></body></html>')}`,
)
.then(() => {
if (!created.isDestroyed() && recordingFocusWindows.includes(created))
created.showInactive();
})
.catch(() => {
if (!created.isDestroyed()) created.close();
});
} else {
const old = win.getBounds();
if (
old.x !== bounds.x ||
old.y !== bounds.y ||
old.width !== bounds.width ||
old.height !== bounds.height
)
win.setBounds(bounds);
}
}
for (const win of recordingFocusWindows.splice(rectangles.length))
if (!win.isDestroyed()) win.close();
}
export function hideRecordingFocus(): void {
const windows = recordingFocusWindows;
recordingFocusWindows = [];
for (const win of windows) if (!win.isDestroyed()) win.close();
}