Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ interface Window {
saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>;
hudOverlayHide: () => void;
hudOverlayClose: () => void;
showCountdownOverlay: (value: number, runId: number) => Promise<void>;
setCountdownOverlayValue: (value: number, runId: number) => Promise<void>;
hideCountdownOverlay: (runId: number) => Promise<void>;
onCountdownOverlayValue: (callback: (value: number | null) => void) => () => void;
setMicrophoneExpanded: (expanded: boolean) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void;
onRequestSaveBeforeClose: (callback: () => Promise<boolean> | boolean) => () => void;
Expand Down
154 changes: 150 additions & 4 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,15 +352,163 @@ function sampleCursorPoint() {
export function registerIpcHandlers(
createEditorWindow: () => void,
createSourceSelectorWindow: () => BrowserWindow,
createCountdownOverlayWindow: () => BrowserWindow,
getMainWindow: () => BrowserWindow | null,
getSourceSelectorWindow: () => BrowserWindow | null,
getCountdownOverlayWindow: () => BrowserWindow | null,
onRecordingStateChange?: (recording: boolean, sourceName: string) => void,
switchToHud?: () => void,
) {
const supportsWindowOpacity = process.platform !== "linux";
const countdownOverlayState = {
visible: false,
value: null as number | null,
activeRunId: null as number | null,
hideCommitId: 0,
hideCommitTimer: null as ReturnType<typeof setTimeout> | null,
};
const COUNTDOWN_OVERLAY_HIDE_DEBOUNCE_MS = 1200;

const clearCountdownOverlayHideCommit = () => {
if (countdownOverlayState.hideCommitTimer) {
clearTimeout(countdownOverlayState.hideCommitTimer);
countdownOverlayState.hideCommitTimer = null;
}
};

const commitCountdownOverlayHide = (win: BrowserWindow, hideCommitId: number) => {
if (win.isDestroyed()) {
return;
}

if (countdownOverlayState.visible || countdownOverlayState.hideCommitId !== hideCommitId) {
return;
}

win.hide();
if (supportsWindowOpacity) {
// Reset baseline opacity for the next show cycle.
win.setOpacity(1);
}
};

const flushCountdownOverlayState = (win: BrowserWindow) => {
if (win.isDestroyed()) {
return;
}

clearCountdownOverlayHideCommit();
win.webContents.send("countdown-overlay-value", countdownOverlayState.value);
if (!countdownOverlayState.visible) {
return;
}

if (win.isVisible()) {
if (supportsWindowOpacity) {
win.setOpacity(1);
}
return;
}

setTimeout(() => {
if (!win.isDestroyed() && countdownOverlayState.visible && !win.isVisible()) {
if (supportsWindowOpacity) {
win.setOpacity(0);
}
win.showInactive();

if (supportsWindowOpacity) {
setTimeout(() => {
if (!win.isDestroyed() && countdownOverlayState.visible && win.isVisible()) {
win.setOpacity(1);
}
}, 0);
}
}
}, 16);
};

ipcMain.handle("countdown-overlay-show", (_, value: number, runId: number) => {
countdownOverlayState.activeRunId = runId;
countdownOverlayState.visible = true;
countdownOverlayState.value = value;

const win = getCountdownOverlayWindow() ?? createCountdownOverlayWindow();
if (win.isDestroyed()) {
return;
}

if (win.webContents.isLoading()) {
win.webContents.once("did-finish-load", () => {
if (!win.isDestroyed()) {
flushCountdownOverlayState(win);
}
Comment thread
Galactic99 marked this conversation as resolved.
});
} else {
flushCountdownOverlayState(win);
}
});

ipcMain.handle("countdown-overlay-set-value", (_, value: number, runId: number) => {
if (countdownOverlayState.activeRunId !== runId || !countdownOverlayState.visible) {
return;
}

countdownOverlayState.value = value;

const win = getCountdownOverlayWindow();
if (!win || win.isDestroyed()) {
return;
}

if (win.webContents.isLoading()) {
return;
}

win.webContents.send("countdown-overlay-value", value);
});

ipcMain.handle("countdown-overlay-hide", (_, runId: number) => {
if (countdownOverlayState.activeRunId !== runId) {
return;
}

countdownOverlayState.visible = false;
countdownOverlayState.hideCommitId += 1;
const hideCommitId = countdownOverlayState.hideCommitId;
clearCountdownOverlayHideCommit();

const win = getCountdownOverlayWindow();
if (!win || win.isDestroyed()) {
countdownOverlayState.value = null;
return;
}

if (supportsWindowOpacity) {
// Hide visually immediately to avoid hide/show compositor flashes on rapid restart.
win.setOpacity(0);
}

countdownOverlayState.value = null;
if (!win.webContents.isLoading()) {
win.webContents.send("countdown-overlay-value", countdownOverlayState.value);
}

if (!supportsWindowOpacity) {
win.hide();
return;
}

countdownOverlayState.hideCommitTimer = setTimeout(() => {
countdownOverlayState.hideCommitTimer = null;
commitCountdownOverlayHide(win, hideCommitId);
}, COUNTDOWN_OVERLAY_HIDE_DEBOUNCE_MS);
});

ipcMain.handle("switch-to-hud", () => {
if (switchToHud) switchToHud();
});
ipcMain.handle("start-new-recording", async () => {
ipcMain.handle("start-new-recording", () => {
try {
setCurrentRecordingSessionState(null);
if (switchToHud) {
Expand Down Expand Up @@ -518,9 +666,8 @@ export function registerIpcHandlers(
});

ipcMain.handle("read-binary-file", async (_, inputPath: string) => {
let normalizedPath: string | null = null;
try {
normalizedPath = normalizeVideoSourcePath(inputPath);
const normalizedPath = normalizeVideoSourcePath(inputPath);
if (!normalizedPath) {
return { success: false, message: "Invalid file path" };
}
Expand All @@ -545,7 +692,6 @@ export function registerIpcHandlers(
success: false,
message: "Failed to read binary file",
error: String(error),
path: normalizedPath,
};
}
});
Expand Down
35 changes: 32 additions & 3 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import {
} from "electron";
import { mainT, setMainLocale } from "./i18n";
import { registerIpcHandlers } from "./ipc/handlers";
import { createEditorWindow, createHudOverlayWindow, createSourceSelectorWindow } from "./windows";
import {
createCountdownOverlayWindow,
createEditorWindow,
createHudOverlayWindow,
createSourceSelectorWindow,
} from "./windows";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -60,6 +65,7 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
// Window references
let mainWindow: BrowserWindow | null = null;
let sourceSelectorWindow: BrowserWindow | null = null;
let countdownOverlayWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
let selectedSourceName = "";
const isMac = process.platform === "darwin";
Expand Down Expand Up @@ -322,6 +328,18 @@ function createSourceSelectorWindowWrapper() {
return sourceSelectorWindow;
}

function createCountdownOverlayWindowWrapper() {
if (countdownOverlayWindow && !countdownOverlayWindow.isDestroyed()) {
return countdownOverlayWindow;
}

countdownOverlayWindow = createCountdownOverlayWindow();
countdownOverlayWindow.on("closed", () => {
countdownOverlayWindow = null;
});
return countdownOverlayWindow;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// On macOS, applications and their menu bar stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
Expand All @@ -331,8 +349,17 @@ app.on("window-all-closed", () => {
app.on("activate", () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
const hasVisibleWindow = BrowserWindow.getAllWindows().some((window) => {
if (window.isDestroyed() || !window.isVisible()) {
return false;
}

const url = window.webContents.getURL();
const isCountdownOverlayWindow = url.includes("windowType=countdown-overlay");
return !isCountdownOverlayWindow;
});
if (!hasVisibleWindow) {
showMainWindow();
}
});

Expand Down Expand Up @@ -386,8 +413,10 @@ app.whenReady().then(async () => {
registerIpcHandlers(
createEditorWindowWrapper,
createSourceSelectorWindowWrapper,
createCountdownOverlayWindowWrapper,
() => mainWindow,
() => sourceSelectorWindow,
() => countdownOverlayWindow,
(recording: boolean, sourceName: string) => {
selectedSourceName = sourceName;
if (!tray) createTray();
Expand Down
14 changes: 14 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ contextBridge.exposeInMainWorld("electronAPI", {
setHasUnsavedChanges: (hasChanges: boolean) => {
ipcRenderer.send("set-has-unsaved-changes", hasChanges);
},
showCountdownOverlay: (value: number, runId: number) => {
return ipcRenderer.invoke("countdown-overlay-show", value, runId);
},
setCountdownOverlayValue: (value: number, runId: number) => {
return ipcRenderer.invoke("countdown-overlay-set-value", value, runId);
},
hideCountdownOverlay: (runId: number) => {
return ipcRenderer.invoke("countdown-overlay-hide", runId);
},
onCountdownOverlayValue: (callback: (value: number | null) => void) => {
const listener = (_event: unknown, value: number | null) => callback(value);
ipcRenderer.on("countdown-overlay-value", listener);
return () => ipcRenderer.removeListener("countdown-overlay-value", listener);
},
onRequestSaveBeforeClose: (callback: () => Promise<boolean> | boolean) => {
const listener = async () => {
try {
Expand Down
52 changes: 52 additions & 0 deletions electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,55 @@ export function createSourceSelectorWindow(): BrowserWindow {

return win;
}

/**
* Creates a centered transparent countdown overlay window that sits above the
* HUD while recording pre-roll is running.
*/
export function createCountdownOverlayWindow(): BrowserWindow {
const { workArea } = screen.getPrimaryDisplay();
const overlayWidth = 420;
const overlayHeight = 260;

const win = new BrowserWindow({
width: overlayWidth,
height: overlayHeight,
minWidth: overlayWidth,
maxWidth: overlayWidth,
minHeight: overlayHeight,
maxHeight: overlayHeight,
x: Math.round(workArea.x + (workArea.width - overlayWidth) / 2),
y: Math.round(workArea.y + (workArea.height - overlayHeight) / 2),
frame: false,
resizable: false,
alwaysOnTop: true,
skipTaskbar: true,
focusable: false,
transparent: true,
backgroundColor: "#00000000",
hasShadow: false,
show: false,
webPreferences: {
preload: path.join(__dirname, "preload.mjs"),
nodeIntegration: false,
contextIsolation: true,
backgroundThrottling: false,
},
});

win.setIgnoreMouseEvents(true);

if (process.platform === "darwin") {
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
}

if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=countdown-overlay");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
win.loadFile(path.join(RENDERER_DIST, "index.html"), {
query: { windowType: "countdown-overlay" },
});
}

return win;
}
19 changes: 14 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { CountdownOverlay } from "./components/launch/CountdownOverlay.tsx";
import { LaunchWindow } from "./components/launch/LaunchWindow";
import { SourceSelector } from "./components/launch/SourceSelector";
import { Toaster } from "./components/ui/sonner";
Expand All @@ -9,18 +10,24 @@ import { ShortcutsProvider } from "./contexts/ShortcutsContext";
import { loadAllCustomFonts } from "./lib/customFonts";

export default function App() {
const [windowType, setWindowType] = useState("");
const [windowType, setWindowType] = useState(
() => new URLSearchParams(window.location.search).get("windowType") || "",
);

useEffect(() => {
const params = new URLSearchParams(window.location.search);
const type = params.get("windowType") || "";
setWindowType(type);
if (type === "hud-overlay" || type === "source-selector") {
const type = new URLSearchParams(window.location.search).get("windowType") || "";
if (type !== windowType) {
setWindowType(type);
}

if (type === "hud-overlay" || type === "source-selector" || type === "countdown-overlay") {
document.body.style.background = "transparent";
document.documentElement.style.background = "transparent";
document.getElementById("root")?.style.setProperty("background", "transparent");
}
}, [windowType]);

useEffect(() => {
// Load custom fonts on app initialization
loadAllCustomFonts().catch((error) => {
console.error("Failed to load custom fonts:", error);
Expand All @@ -33,6 +40,8 @@ export default function App() {
return <LaunchWindow />;
case "source-selector":
return <SourceSelector />;
case "countdown-overlay":
return <CountdownOverlay />;
case "editor":
return (
<ShortcutsProvider>
Expand Down
Loading
Loading