mirror of
https://github.com/siddharthvaddem/openscreen.git
synced 2026-08-30 17:06:13 +08:00
323 lines
9.4 KiB
TypeScript
323 lines
9.4 KiB
TypeScript
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { BrowserWindow, ipcMain, screen } from "electron";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
const APP_ROOT = path.join(__dirname, "..");
|
|
const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"];
|
|
const RENDERER_DIST = path.join(APP_ROOT, "dist");
|
|
const HEADLESS = process.env["HEADLESS"] === "true";
|
|
|
|
// Asset base URL for renderer (wallpapers, etc.). Packaged: extraResources copies
|
|
// public/wallpapers to resources/wallpapers. Unpackaged: <appRoot>/public/.
|
|
const ASSET_BASE_DIR = process.defaultApp
|
|
? path.join(__dirname, "..", "public")
|
|
: process.resourcesPath;
|
|
const ASSET_BASE_URL_ARG = `--asset-base-url=${pathToFileURL(`${ASSET_BASE_DIR}${path.sep}`).toString()}`;
|
|
|
|
let hudOverlayWindow: BrowserWindow | null = null;
|
|
|
|
ipcMain.on("hud-overlay-hide", () => {
|
|
if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) {
|
|
hudOverlayWindow.minimize();
|
|
}
|
|
});
|
|
|
|
ipcMain.on("hud-overlay-ignore-mouse-events", (_event, ignore: boolean) => {
|
|
if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) {
|
|
hudOverlayWindow.setIgnoreMouseEvents(ignore, { forward: true });
|
|
}
|
|
});
|
|
|
|
ipcMain.on("hud-overlay-move-by", (_event, deltaX: number, deltaY: number) => {
|
|
if (
|
|
!hudOverlayWindow ||
|
|
hudOverlayWindow.isDestroyed() ||
|
|
!Number.isFinite(deltaX) ||
|
|
!Number.isFinite(deltaY)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const [x, y] = hudOverlayWindow.getPosition();
|
|
hudOverlayWindow.setPosition(Math.round(x + deltaX), Math.round(y + deltaY), false);
|
|
});
|
|
|
|
// Resize the HUD to fit its rendered content. Anchored by its bottom-centre so it
|
|
// stays where the user dragged it while only growing/shrinking, which lets the
|
|
// vertical tray layout grow tall instead of scrolling inside a fixed window.
|
|
ipcMain.on("hud-overlay-set-size", (_event, width: number, height: number) => {
|
|
if (
|
|
!hudOverlayWindow ||
|
|
hudOverlayWindow.isDestroyed() ||
|
|
!Number.isFinite(width) ||
|
|
!Number.isFinite(height)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const bounds = hudOverlayWindow.getBounds();
|
|
|
|
// Clamp to the work area of the display the HUD sits on; on a short screen the
|
|
// vertical layout can exceed the display, where the bar's own overflow scroll takes over.
|
|
const { workArea } = screen.getDisplayMatching(bounds);
|
|
const nextWidth = Math.min(workArea.width, Math.max(1, Math.round(width)));
|
|
const nextHeight = Math.min(workArea.height, Math.max(1, Math.round(height)));
|
|
|
|
if (bounds.width === nextWidth && bounds.height === nextHeight) {
|
|
return;
|
|
}
|
|
|
|
const centerX = bounds.x + bounds.width / 2;
|
|
const bottomY = bounds.y + bounds.height;
|
|
|
|
hudOverlayWindow.setBounds({
|
|
x: Math.round(centerX - nextWidth / 2),
|
|
y: Math.round(bottomY - nextHeight),
|
|
width: nextWidth,
|
|
height: nextHeight,
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Frameless transparent HUD overlay, always-on-top, centred at the bottom of the
|
|
* primary display. Follows the user across macOS Spaces so it isn't lost on switch.
|
|
*/
|
|
export function createHudOverlayWindow(): BrowserWindow {
|
|
const primaryDisplay = screen.getPrimaryDisplay();
|
|
const { workArea } = primaryDisplay;
|
|
|
|
const windowWidth = 600;
|
|
const windowHeight = 160;
|
|
|
|
const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2);
|
|
const y = Math.floor(workArea.y + workArea.height - windowHeight - 5);
|
|
|
|
const win = new BrowserWindow({
|
|
width: windowWidth,
|
|
height: windowHeight,
|
|
// Min/max are intentionally loose: the renderer resizes to fit content via
|
|
// "hud-overlay-set-size" (above), needed for the vertical tray to grow taller.
|
|
minWidth: 120,
|
|
minHeight: 80,
|
|
x: x,
|
|
y: y,
|
|
frame: false,
|
|
transparent: true,
|
|
// Fully-transparent ARGB backing. Without this macOS draws the window as a
|
|
// rounded glass panel with a border around the HUD content.
|
|
backgroundColor: "#00000000",
|
|
// Don't let macOS mask the window into a rounded rect; the HUD bar provides
|
|
// its own rounding and the window itself must be invisible.
|
|
roundedCorners: false,
|
|
resizable: false,
|
|
alwaysOnTop: true,
|
|
skipTaskbar: true,
|
|
hasShadow: false,
|
|
show: false, // shown via ready-to-show to avoid black rectangle flash
|
|
webPreferences: {
|
|
preload: path.join(__dirname, "preload.mjs"),
|
|
additionalArguments: [ASSET_BASE_URL_ARG],
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
backgroundThrottling: false,
|
|
},
|
|
});
|
|
win.setIgnoreMouseEvents(true, { forward: true });
|
|
|
|
// Follow the user across macOS Spaces, else the HUD stays pinned to the Space
|
|
// it was first opened on.
|
|
if (process.platform === "darwin") {
|
|
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
}
|
|
|
|
// Show only once painted to avoid the black rectangle flash when a transparent
|
|
// window is shown before its first paint.
|
|
win.once("ready-to-show", () => {
|
|
if (!HEADLESS) win.show();
|
|
});
|
|
|
|
win.webContents.on("did-finish-load", () => {
|
|
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
|
});
|
|
|
|
hudOverlayWindow = win;
|
|
|
|
win.on("closed", () => {
|
|
if (hudOverlayWindow === win) {
|
|
hudOverlayWindow = null;
|
|
}
|
|
});
|
|
|
|
if (VITE_DEV_SERVER_URL) {
|
|
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=hud-overlay");
|
|
} else {
|
|
win.loadFile(path.join(RENDERER_DIST, "index.html"), {
|
|
query: { windowType: "hud-overlay" },
|
|
});
|
|
}
|
|
|
|
return win;
|
|
}
|
|
|
|
/**
|
|
* Main editor window. Starts maximised with a hidden title bar on macOS; not
|
|
* always-on-top and appears in the taskbar/dock.
|
|
*/
|
|
export function createEditorWindow(): BrowserWindow {
|
|
const isMac = process.platform === "darwin";
|
|
|
|
const win = new BrowserWindow({
|
|
width: 1200,
|
|
height: 800,
|
|
minWidth: 800,
|
|
minHeight: 600,
|
|
...(isMac && {
|
|
titleBarStyle: "hiddenInset",
|
|
trafficLightPosition: { x: 12, y: 12 },
|
|
}),
|
|
transparent: false,
|
|
resizable: true,
|
|
alwaysOnTop: false,
|
|
skipTaskbar: false,
|
|
title: "OpenScreen",
|
|
backgroundColor: "#09090b",
|
|
show: false, // shown via ready-to-show to avoid white flash on first load
|
|
webPreferences: {
|
|
preload: path.join(__dirname, "preload.mjs"),
|
|
additionalArguments: [ASSET_BASE_URL_ARG],
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
webSecurity: false,
|
|
backgroundThrottling: false,
|
|
},
|
|
});
|
|
|
|
win.maximize();
|
|
|
|
// Show only once painted to avoid a white flash on cold Vite start.
|
|
win.once("ready-to-show", () => {
|
|
if (!HEADLESS) win.show();
|
|
});
|
|
|
|
// Inject dark background before any React paint so the sub-titlebar area never
|
|
// flashes white on a cold Vite load.
|
|
win.webContents.on("dom-ready", () => {
|
|
win.webContents.insertCSS("html, body, #root { background: #09090b !important; }").catch(() => {
|
|
// Best-effort cosmetic; ignore if the page is mid-teardown.
|
|
});
|
|
});
|
|
|
|
win.webContents.on("did-finish-load", () => {
|
|
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
|
});
|
|
|
|
if (VITE_DEV_SERVER_URL) {
|
|
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=editor");
|
|
} else {
|
|
win.loadFile(path.join(RENDERER_DIST, "index.html"), {
|
|
query: { windowType: "editor" },
|
|
});
|
|
}
|
|
|
|
return win;
|
|
}
|
|
|
|
/**
|
|
* Floating source-selector window for picking a screen or window to record.
|
|
* Frameless, transparent, and follows the user across macOS Spaces.
|
|
*/
|
|
export function createSourceSelectorWindow(): BrowserWindow {
|
|
const { width, height } = screen.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,
|
|
backgroundColor: "#00000000",
|
|
webPreferences: {
|
|
preload: path.join(__dirname, "preload.mjs"),
|
|
additionalArguments: [ASSET_BASE_URL_ARG],
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
},
|
|
});
|
|
|
|
// Follow the user across macOS Spaces so the selector appears on the active
|
|
// desktop regardless of where the HUD was opened.
|
|
if (process.platform === "darwin") {
|
|
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Centered transparent countdown overlay that sits above the HUD during
|
|
* recording pre-roll.
|
|
*/
|
|
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"),
|
|
additionalArguments: [ASSET_BASE_URL_ARG],
|
|
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");
|
|
} else {
|
|
win.loadFile(path.join(RENDERER_DIST, "index.html"), {
|
|
query: { windowType: "countdown-overlay" },
|
|
});
|
|
}
|
|
|
|
return win;
|
|
}
|