Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
382de78
feat: add Open Studio button, empty-state dropzone, and broad video i…
makaradam May 11, 2026
437f121
fix: show empty state instead of error when editor opens with no video
makaradam May 11, 2026
8c2d41c
fix: conditionally render editor workspace to prevent video load error
makaradam May 11, 2026
0e34bec
fix: register start-new-recording IPC handler and skip dialog with no…
makaradam May 11, 2026
a1c6045
fix: dark background with spinner for loading/error states in editor
makaradam May 11, 2026
413d433
fix: clear session on return-to-recorder and eliminate white flash
makaradam May 11, 2026
660dad9
fix: remove double-close of HUD in switch-to-editor handler
makaradam May 11, 2026
8e04013
fix: expand HUD window bounds to prevent CSS shadow clipping
makaradam May 11, 2026
52cd754
fix: eliminate black flash and bottom shadow clipping on HUD window
makaradam May 11, 2026
53a6b30
fix: use custom zoom scale for focus clamping in drag handler
makaradam May 11, 2026
9e7ffd8
feat: remove open-video-file button from HUD toolbar
makaradam May 11, 2026
8510cce
feat: remove load-project button from HUD toolbar
makaradam May 11, 2026
499c9ef
fix: prevent white flash on first editor window open
makaradam May 11, 2026
22cebec
fix: remove Import Video File from File menu; kill white flash with i…
makaradam May 11, 2026
1b71362
fix: eliminate white flash on editor open
makaradam May 11, 2026
3a9c3a3
feat: show "Loading editor..." vs "Loading video..." contextually
makaradam May 11, 2026
8a81e83
fix: prompt unsaved changes dialog for imported/recorded videos with …
makaradam May 11, 2026
8241e75
feat: show contextual unsaved-changes dialog for File > New Project
makaradam May 11, 2026
ab08513
fix: eliminate black rectangle flash before countdown overlay appears
makaradam May 11, 2026
cf64581
fix: fall back to web MediaRecorder when native Windows helper is mis…
makaradam May 11, 2026
7dd54a1
fix: use getUserMedia on all platforms instead of broken getDisplayMe…
makaradam May 11, 2026
c8f8742
fix: freeze dialog variant during close animation to prevent flash
makaradam May 11, 2026
f258786
fix: raise device selector panel above HUD toolbar
makaradam May 11, 2026
0ff83b7
fix: clear webcam state on new project and video import
makaradam May 11, 2026
800749e
fix: make drag-and-drop project file actually open the project
makaradam May 11, 2026
3d27efa
style: add more top padding to drag-and-drop hint on empty state
makaradam May 11, 2026
b6439ad
fix: show error dialog on unsupported or failed drag-and-drop
makaradam May 11, 2026
04c1280
fix: resolve double-dialog and drag-drop load failure
makaradam May 11, 2026
800f906
fix(electron): expose webUtils.getPathForFile and loadProjectFileFrom…
makaradam May 11, 2026
655cb05
fix(ipc): clear project path and recording session when starting a ne…
makaradam May 11, 2026
23f5fbe
feat(editor): rewrite EditorEmptyState — webUtils drag-drop, i18n, di…
makaradam May 11, 2026
8750d6f
feat(editor): unsaved changes guard for Load Project + New Project he…
makaradam May 11, 2026
5b8f718
feat(i18n): add Load Project unsaved-changes dialog strings — 11 lang…
makaradam May 11, 2026
4ec35e7
feat(i18n): add editor empty state strings — 11 languages
makaradam May 11, 2026
370323c
feat(i18n): add New Project label to settings strings — 11 languages
makaradam May 11, 2026
5c33435
fix(editor): reset all editor state when starting a new project
makaradam May 11, 2026
20ccf45
fix(ipc): make .openscreen extension check case-insensitive
makaradam May 11, 2026
f4a3dec
fix(i18n): strip UTF-8 BOM from all locale JSON files
makaradam May 11, 2026
d35b8f2
fix(electron): wrap webUtils.getPathForFile in try/catch in preload
makaradam May 11, 2026
e70d46e
fix(editor): wrap getPathForFile call in try/catch in drop handler
makaradam May 11, 2026
9e30232
fix(editor): use handleNewRecordingConfirm on fast-path new recording…
makaradam May 11, 2026
79a9b35
fix(recorder): turn off system audio toggle when capture fails
makaradam May 11, 2026
a6e91e3
docs(editor): add warning comment to clampFocusForRegion to prevent r…
makaradam May 11, 2026
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
11 changes: 11 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ interface Window {
canceled?: boolean;
error?: string;
}>;
getPathForFile: (file: File) => string;
loadProjectFileFromPath: (filePath: string) => Promise<{
success: boolean;
path?: string;
project?: unknown;
message?: string;
canceled?: boolean;
error?: string;
}>;
onMenuNewProject: (callback: () => void) => () => void;
onMenuImportVideo: (callback: () => void) => () => void;
onMenuLoadProject: (callback: () => void) => () => void;
onMenuSaveProject: (callback: () => void) => () => void;
onMenuSaveProjectAs: (callback: () => void) => () => void;
Expand Down
90 changes: 79 additions & 11 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,17 @@ const PROJECT_FILE_EXTENSION = "openscreen";
const SHORTCUTS_FILE = path.join(app.getPath("userData"), "shortcuts.json");
const RECORDING_FILE_PREFIX = "recording-";
const RECORDING_SESSION_SUFFIX = ".session.json";
const ALLOWED_IMPORT_VIDEO_EXTENSIONS = new Set([".webm", ".mp4", ".mov", ".avi", ".mkv"]);
const ALLOWED_IMPORT_VIDEO_EXTENSIONS = new Set([
".webm",
".mp4",
".mov",
".avi",
".mkv",
".m4v",
".wmv",
".flv",
".ts",
]);

/**
* Paths explicitly approved by the user via file picker dialogs or project loads.
Expand Down Expand Up @@ -985,29 +995,39 @@ export function registerIpcHandlers(
});

ipcMain.handle("switch-to-editor", () => {
const mainWin = getMainWindow();
if (mainWin) {
mainWin.close();
}
// createEditorWindow is createEditorWindowWrapper — it already closes
// the current mainWindow (the HUD) before opening the editor. Closing
// it here too causes a double-close which leaves ghost transparent
// windows and makes the HUD shadow compound on each cycle.
createEditorWindow();
});

ipcMain.handle("start-new-recording", () => {
if (_switchToHud) {
_switchToHud();
}
return { success: true };
});

ipcMain.handle("countdown-overlay-show", async (_, value: number, runId: number) => {
const overlayWindow = getCountdownOverlayWindow?.() ?? createCountdownOverlayWindow();
if (overlayWindow.isDestroyed()) {
return;
}

if (!overlayWindow.isVisible()) {
overlayWindow.showInactive();
}

// Wait for the first frame to be painted before showing the window.
// Showing before ready-to-show produces a black rectangle flash because
// Chromium hasn't rendered any pixels yet.
if (overlayWindow.webContents.isLoading()) {
await new Promise<void>((resolve) => {
overlayWindow.webContents.once("did-finish-load", () => resolve());
overlayWindow.once("ready-to-show", resolve);
});
}

if (!overlayWindow.isVisible()) {
overlayWindow.showInactive();
}

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

Expand Down Expand Up @@ -1533,7 +1553,7 @@ export function registerIpcHandlers(
filters: [
{
name: mainT("dialogs", "fileDialogs.videoFiles"),
extensions: ["webm", "mp4", "mov", "avi", "mkv"],
extensions: ["webm", "mp4", "mov", "avi", "mkv", "m4v", "wmv", "flv", "ts"],
},
{ name: mainT("dialogs", "fileDialogs.allFiles"), extensions: ["*"] },
],
Expand Down Expand Up @@ -1748,6 +1768,51 @@ export function registerIpcHandlers(
}
}

ipcMain.handle("load-project-file-from-path", async (_event, filePath: string) => {
return loadProjectFileFromPath(filePath);
});

async function loadProjectFileFromPath(filePath: string): Promise<ProjectFileResult> {
try {
if (!filePath || typeof filePath !== "string") {
return { success: false, message: "Invalid file path" };
}
// Validate extension and readability
if (path.extname(filePath).toLowerCase() !== `.${PROJECT_FILE_EXTENSION}`) {
return { success: false, message: "Not an Openscreen project file" };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const stats = await fs.stat(filePath).catch(() => null);
if (!stats?.isFile()) {
return { success: false, message: "File not found" };
}
const content = await fs.readFile(filePath, "utf-8");
const project = JSON.parse(content);
currentProjectPath = filePath;

// Approve session paths; tolerate failures (e.g. video moved outside
// trusted dirs) so the project still loads and the renderer can surface
// a "video not found" error rather than a generic load failure.
let session: import("../../src/lib/recordingSession").RecordingSession | null = null;
try {
session = await getApprovedProjectSession(project, filePath);
} catch (sessionError) {
console.warn(
"[loadProjectFileFromPath] Could not approve session paths, proceeding without session:",
sessionError,
);
}
setCurrentRecordingSessionState(session);
return { success: true, path: filePath, project };
} catch (error) {
console.error("Failed to load project file from path:", error);
return {
success: false,
message: "Failed to load project file",
error: String(error),
};
}
}

ipcMain.handle("load-current-project-file", async () => {
return loadCurrentProjectFile();
});
Expand Down Expand Up @@ -1830,6 +1895,8 @@ export function registerIpcHandlers(

function clearCurrentVideoPath(): ProjectPathResult {
currentVideoPath = null;
currentProjectPath = null;
setCurrentRecordingSessionState(null);
return { success: true };
}

Expand Down Expand Up @@ -1904,6 +1971,7 @@ export function registerIpcHandlers(
saveProjectFile,
loadProjectFile,
loadCurrentProjectFile,
loadProjectFileFromPath,
setCurrentVideoPath,
getCurrentVideoPathResult,
clearCurrentVideoPath,
Expand Down
7 changes: 7 additions & 0 deletions electron/ipc/nativeBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface NativeBridgeContext {
) => Promise<ProjectFileResult>;
loadProjectFile: () => Promise<ProjectFileResult>;
loadCurrentProjectFile: () => Promise<ProjectFileResult>;
loadProjectFileFromPath: (path: string) => Promise<ProjectFileResult>;
setCurrentVideoPath: (path: string) => ProjectPathResult | Promise<ProjectPathResult>;
getCurrentVideoPathResult: () => ProjectPathResult;
clearCurrentVideoPath: () => ProjectPathResult;
Expand Down Expand Up @@ -100,6 +101,7 @@ export function registerNativeBridgeHandlers(context: NativeBridgeContext) {
saveProjectFile: context.saveProjectFile,
loadProjectFile: context.loadProjectFile,
loadCurrentProjectFile: context.loadCurrentProjectFile,
loadProjectFileFromPath: context.loadProjectFileFromPath,
setCurrentVideoPath: context.setCurrentVideoPath,
getCurrentVideoPathResult: context.getCurrentVideoPathResult,
clearCurrentVideoPath: context.clearCurrentVideoPath,
Expand Down Expand Up @@ -168,6 +170,11 @@ export function registerNativeBridgeHandlers(context: NativeBridgeContext) {
requestId,
await projectService.loadCurrentProjectFile(),
);
case "loadProjectFileFromPath":
return createSuccessResponse(
requestId,
await projectService.loadProjectFileFromPath(request.payload.path),
);
case "setCurrentVideoPath":
return createSuccessResponse(
requestId,
Expand Down
8 changes: 7 additions & 1 deletion electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function isEditorWindow(window: BrowserWindow) {
}

function sendEditorMenuAction(
channel: "menu-load-project" | "menu-save-project" | "menu-save-project-as",
channel: "menu-load-project" | "menu-save-project" | "menu-save-project-as" | "menu-new-project",
) {
let targetWindow = BrowserWindow.getFocusedWindow() ?? mainWindow;

Expand Down Expand Up @@ -168,6 +168,12 @@ function setupApplicationMenu() {
{
label: mainT("common", "actions.file") || "File",
submenu: [
{
label: mainT("dialogs", "unsavedChanges.newProject") || "New Project",
accelerator: "CmdOrCtrl+N",
click: () => sendEditorMenuAction("menu-new-project"),
},
{ type: "separator" as const },
{
label: mainT("dialogs", "unsavedChanges.loadProject") || "Load Project…",
accelerator: "CmdOrCtrl+O",
Expand Down
7 changes: 7 additions & 0 deletions electron/native-bridge/services/projectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface ProjectServiceOptions {
) => Promise<ProjectFileResult>;
loadProjectFile: () => Promise<ProjectFileResult>;
loadCurrentProjectFile: () => Promise<ProjectFileResult>;
loadProjectFileFromPath: (path: string) => Promise<ProjectFileResult>;
setCurrentVideoPath: (path: string) => ProjectPathResult | Promise<ProjectPathResult>;
getCurrentVideoPathResult: () => ProjectPathResult;
clearCurrentVideoPath: () => ProjectPathResult;
Expand Down Expand Up @@ -60,6 +61,12 @@ export class ProjectService {
return result;
}

async loadProjectFileFromPath(path: string) {
const result = await this.options.loadProjectFileFromPath(path);
this.getCurrentContext();
return result;
}

async setCurrentVideoPath(path: string) {
const result = await this.options.setCurrentVideoPath(path);
this.getCurrentContext();
Expand Down
22 changes: 21 additions & 1 deletion electron/preload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { contextBridge, ipcRenderer } from "electron";
import { contextBridge, ipcRenderer, webUtils } from "electron";
import type { NativeWindowsRecordingRequest } from "../src/lib/nativeWindowsRecording";
import type { RecordingSession, StoreRecordedSessionInput } from "../src/lib/recordingSession";
import { NATIVE_BRIDGE_CHANNEL, type NativeBridgeRequest } from "../src/native/contracts";
Expand Down Expand Up @@ -121,9 +121,29 @@ contextBridge.exposeInMainWorld("electronAPI", {
loadProjectFile: () => {
return ipcRenderer.invoke("load-project-file");
},
loadProjectFileFromPath: (filePath: string) => {
return ipcRenderer.invoke("load-project-file-from-path", filePath);
},
getPathForFile: (file: File) => {
try {
return webUtils.getPathForFile(file);
} catch {
return "";
}
},
loadCurrentProjectFile: () => {
return ipcRenderer.invoke("load-current-project-file");
},
onMenuNewProject: (callback: () => void) => {
const listener = () => callback();
ipcRenderer.on("menu-new-project", listener);
return () => ipcRenderer.removeListener("menu-new-project", listener);
},
onMenuImportVideo: (callback: () => void) => {
const listener = () => callback();
ipcRenderer.on("menu-import-video", listener);
return () => ipcRenderer.removeListener("menu-import-video", listener);
},
onMenuLoadProject: (callback: () => void) => {
const listener = () => callback();
ipcRenderer.on("menu-load-project", listener);
Expand Down
46 changes: 35 additions & 11 deletions electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,25 @@ export function createHudOverlayWindow(): BrowserWindow {
const primaryDisplay = screen.getPrimaryDisplay();
const { workArea } = primaryDisplay;

const windowWidth = 600;
const windowHeight = 160;

// Extra padding around the visible pill so CSS box-shadows (60px blur)
// aren't clipped by the transparent window boundary.
// The pill sits at CSS `bottom-20` (80px from window bottom) so the
// downward shadow has ~80px of transparent space to expand into.
// The window is positioned so the pill's screen position stays unchanged.
const windowWidth = 800;
const windowHeight = 320;
// Pill is bottom-20 (80px) instead of bottom-5 (20px), so shift window
// down 60px to keep the pill at the same visual screen position.
const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2);
const y = Math.floor(workArea.y + workArea.height - windowHeight - 5);
const y = Math.floor(workArea.y + workArea.height - windowHeight + 55);

const win = new BrowserWindow({
width: windowWidth,
height: windowHeight,
minWidth: 600,
maxWidth: 600,
minHeight: 160,
maxHeight: 160,
minWidth: 800,
maxWidth: 800,
minHeight: 320,
maxHeight: 320,
x: x,
y: y,
frame: false,
Expand All @@ -60,7 +66,7 @@ export function createHudOverlayWindow(): BrowserWindow {
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
show: !HEADLESS,
show: false, // shown via ready-to-show to avoid black flash
webPreferences: {
preload: path.join(__dirname, "preload.mjs"),
additionalArguments: [ASSET_BASE_URL_ARG],
Expand All @@ -77,6 +83,11 @@ export function createHudOverlayWindow(): BrowserWindow {
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
}

// Show only once content is painted — prevents black rectangle flash
win.once("ready-to-show", () => {
if (!HEADLESS) win.show();
});

win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
Expand Down Expand Up @@ -121,8 +132,8 @@ export function createEditorWindow(): BrowserWindow {
alwaysOnTop: false,
skipTaskbar: false,
title: "OpenScreen",
backgroundColor: "#000000",
show: !HEADLESS,
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],
Expand All @@ -136,6 +147,19 @@ export function createEditorWindow(): BrowserWindow {
// Maximize the window by default
win.maximize();

// Show only once content is painted — prevents 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 even on the very first cold Vite load
win.webContents.on("dom-ready", () => {
win.webContents
.insertCSS("html, body, #root { background: #09090b !important; }")
.catch(() => {});
});

win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
Expand Down
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en">
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<body style="background:#09090b;margin:0">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
Loading
Loading