mirror of
https://github.com/siddharthvaddem/openscreen.git
synced 2026-08-30 17:06:13 +08:00
Merge upstream/main into feat/remember-last-project-folder
Resolved conflicts: - LaunchWindow.tsx: kept upstream's removal of openVideoFile/openProjectFile (moved to Studio Dashboard) - VideoEditor.tsx: used upstream's doLoadProject rename while preserving getProjectFolder() arg
This commit is contained in:
Vendored
+11
@@ -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;
|
||||
|
||||
+72
-11
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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: ["*"] },
|
||||
],
|
||||
@@ -2678,6 +2691,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" };
|
||||
}
|
||||
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();
|
||||
});
|
||||
@@ -2760,6 +2818,8 @@ export function registerIpcHandlers(
|
||||
|
||||
function clearCurrentVideoPath(): ProjectPathResult {
|
||||
currentVideoPath = null;
|
||||
currentProjectPath = null;
|
||||
setCurrentRecordingSessionState(null);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -2834,6 +2894,7 @@ export function registerIpcHandlers(
|
||||
saveProjectFile,
|
||||
loadProjectFile,
|
||||
loadCurrentProjectFile,
|
||||
loadProjectFileFromPath,
|
||||
setCurrentVideoPath,
|
||||
getCurrentVideoPathResult,
|
||||
clearCurrentVideoPath,
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface NativeBridgeContext {
|
||||
) => Promise<ProjectFileResult>;
|
||||
loadProjectFile: (projectFolder?: string) => Promise<ProjectFileResult>;
|
||||
loadCurrentProjectFile: () => Promise<ProjectFileResult>;
|
||||
loadProjectFileFromPath: (path: string) => Promise<ProjectFileResult>;
|
||||
setCurrentVideoPath: (path: string) => ProjectPathResult | Promise<ProjectPathResult>;
|
||||
getCurrentVideoPathResult: () => ProjectPathResult;
|
||||
clearCurrentVideoPath: () => ProjectPathResult;
|
||||
@@ -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,
|
||||
@@ -171,6 +173,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,
|
||||
|
||||
+7
-1
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -16,6 +16,7 @@ interface ProjectServiceOptions {
|
||||
) => Promise<ProjectFileResult>;
|
||||
loadProjectFile: (projectFolder?: string) => Promise<ProjectFileResult>;
|
||||
loadCurrentProjectFile: () => Promise<ProjectFileResult>;
|
||||
loadProjectFileFromPath: (path: string) => Promise<ProjectFileResult>;
|
||||
setCurrentVideoPath: (path: string) => ProjectPathResult | Promise<ProjectPathResult>;
|
||||
getCurrentVideoPathResult: () => ProjectPathResult;
|
||||
clearCurrentVideoPath: () => ProjectPathResult;
|
||||
@@ -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();
|
||||
|
||||
+21
-1
@@ -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";
|
||||
@@ -173,9 +173,29 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
loadProjectFile: (projectFolder?: string) => {
|
||||
return ipcRenderer.invoke("load-project-file", projectFolder);
|
||||
},
|
||||
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);
|
||||
|
||||
+22
-3
@@ -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],
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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],
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user