Skip to content
This repository was archived by the owner on Jun 7, 2026. It is now read-only.
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
11 changes: 11 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,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
83 changes: 72 additions & 11 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,17 @@ const PROJECT_FILE_EXTENSION = "openscreen";
export 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",
]);
const PREVIEW_AUDIO_DIR = path.join(app.getPath("userData"), "preview-audio");
const nativeMacCaptureEvents = new EventEmitter();

Expand Down Expand Up @@ -1430,10 +1440,10 @@ 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();
});

Expand All @@ -1453,16 +1463,19 @@ export function registerIpcHandlers(
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 @@ -2430,7 +2443,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 @@ -2658,6 +2671,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 @@ -2740,6 +2798,8 @@ export function registerIpcHandlers(

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

Expand Down Expand Up @@ -2814,6 +2874,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 @@ -114,7 +114,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 @@ -173,6 +173,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 { NativeMacRecordingRequest } from "../src/lib/nativeMacRecording";
import type { NativeWindowsRecordingRequest } from "../src/lib/nativeWindowsRecording";
import type { RecordingSession, StoreRecordedSessionInput } from "../src/lib/recordingSession";
Expand Down Expand Up @@ -173,9 +173,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
25 changes: 22 additions & 3 deletions electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function createHudOverlayWindow(): BrowserWindow {
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
show: !HEADLESS,
show: false, // shown via ready-to-show to avoid black rectangle flash
webPreferences: {
preload: path.join(__dirname, "preload.mjs"),
additionalArguments: [ASSET_BASE_URL_ARG],
Expand All @@ -91,6 +91,12 @@ export function createHudOverlayWindow(): BrowserWindow {
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
}

// Show only once content is painted — prevents the black rectangle flash
// that appears 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());
});
Expand Down Expand Up @@ -135,8 +141,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 @@ -150,6 +156,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(() => {
// 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());
});
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
32 changes: 31 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { LaunchWindow } from "./components/launch/LaunchWindow";
import { SourceSelector } from "./components/launch/SourceSelector";
import { Toaster } from "./components/ui/sonner";
import { TooltipProvider } from "./components/ui/tooltip";
import { useScopedT } from "./contexts/I18nContext";
import { ShortcutsProvider } from "./contexts/ShortcutsContext";
import { loadAllCustomFonts } from "./lib/customFonts";

Expand All @@ -18,6 +19,7 @@ export default function App() {
const [windowType, setWindowType] = useState(
() => new URLSearchParams(window.location.search).get("windowType") || "",
);
const tEditor = useScopedT("editor");

useEffect(() => {
const type = new URLSearchParams(window.location.search).get("windowType") || "";
Expand Down Expand Up @@ -64,7 +66,35 @@ export default function App() {
case "editor":
return (
<ShortcutsProvider>
<Suspense fallback={<div className="h-screen bg-background" />}>
<Suspense
fallback={
<div className="flex flex-col items-center justify-center gap-3 h-screen bg-[#09090b]">
<svg
className="animate-spin text-[#34B27B]"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
width={28}
height={28}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<span className="text-white/50 text-sm">{tEditor("loadingEditor")}</span>
</div>
}
>
<VideoEditor />
<ShortcutsConfigDialog />
</Suspense>
Expand Down
Loading
Loading