702b733074
Every consumer of /wallpapers/*.jpg — SettingsPanel, VideoPlayback, frameRenderer — was doing async IPC round trips, useEffect dances, and Promise.all for a value that is a build-time constant per process. Each consumer showed briefly-empty or briefly-404ing state on first paint until the handler's reply resolved. The asset base URL depends only on process.defaultApp and process.resourcesPath / __dirname — all available in preload at context-bridge time. Compute once there, expose as a sync string. - preload.ts resolves baseDir (process.resourcesPath packaged, <appRoot>/public unpackaged) and emits assetBaseUrl synchronously. - get-asset-base-path IPC handler + main-process branching deleted. - getAssetPath() is now sync. Returns string, not Promise<string>. Throws AssetBaseUnavailableError (new) when electronAPI.assetBaseUrl is missing — catastrophic preload failure, not silent 404. - resolveImageWallpaperUrl() sync; same sync throw semantics. - SettingsPanel: Promise.all + useState + useEffect collapse to one useMemo. First paint has real URLs, no 18× ERR_FILE_NOT_FOUND, no flicker. - VideoPlayback: wallpaper-resolve useEffect collapses to useMemo. - frameRenderer.setupBackground: drops the await. - electronAPI type decls updated in both .d.ts files. - 35 unit tests updated to reflect sync signature + new AssetBaseUnavailableError contract. Silent-fallback behavior from getAssetPath (returning /relative when electronAPI failed) is gone. Renderers now surface preload failures instead of rendering 404s.
170 lines
6.2 KiB
TypeScript
170 lines
6.2 KiB
TypeScript
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { contextBridge, ipcRenderer } from "electron";
|
|
import type { RecordingSession, StoreRecordedSessionInput } from "../src/lib/recordingSession";
|
|
|
|
// Asset base URL is a build-time constant per process; resolve once here so
|
|
// the renderer can consume it synchronously. Packaged: electron-builder
|
|
// extraResources copies public/wallpapers -> resources/wallpapers (see
|
|
// electron-builder.json5). Unpackaged: wallpapers live at <appRoot>/public/,
|
|
// and __dirname in dist-electron resolves to <appRoot>/dist-electron/.
|
|
const isPackagedProcess = !process.defaultApp;
|
|
const assetBaseDir = isPackagedProcess
|
|
? process.resourcesPath
|
|
: path.join(__dirname, "..", "public");
|
|
const assetBaseUrl = pathToFileURL(`${assetBaseDir}${path.sep}`).toString();
|
|
|
|
contextBridge.exposeInMainWorld("electronAPI", {
|
|
assetBaseUrl,
|
|
hudOverlayHide: () => {
|
|
ipcRenderer.send("hud-overlay-hide");
|
|
},
|
|
hudOverlayClose: () => {
|
|
ipcRenderer.send("hud-overlay-close");
|
|
},
|
|
getSources: async (opts: Electron.SourcesOptions) => {
|
|
return await ipcRenderer.invoke("get-sources", opts);
|
|
},
|
|
switchToEditor: () => {
|
|
return ipcRenderer.invoke("switch-to-editor");
|
|
},
|
|
switchToHud: () => {
|
|
return ipcRenderer.invoke("switch-to-hud");
|
|
},
|
|
startNewRecording: () => {
|
|
return ipcRenderer.invoke("start-new-recording");
|
|
},
|
|
openSourceSelector: () => {
|
|
return ipcRenderer.invoke("open-source-selector");
|
|
},
|
|
selectSource: (source: ProcessedDesktopSource) => {
|
|
return ipcRenderer.invoke("select-source", source);
|
|
},
|
|
getSelectedSource: () => {
|
|
return ipcRenderer.invoke("get-selected-source");
|
|
},
|
|
requestCameraAccess: () => {
|
|
return ipcRenderer.invoke("request-camera-access");
|
|
},
|
|
|
|
storeRecordedVideo: (videoData: ArrayBuffer, fileName: string) => {
|
|
return ipcRenderer.invoke("store-recorded-video", videoData, fileName);
|
|
},
|
|
storeRecordedSession: (payload: StoreRecordedSessionInput) => {
|
|
return ipcRenderer.invoke("store-recorded-session", payload);
|
|
},
|
|
|
|
getRecordedVideoPath: () => {
|
|
return ipcRenderer.invoke("get-recorded-video-path");
|
|
},
|
|
setRecordingState: (recording: boolean) => {
|
|
return ipcRenderer.invoke("set-recording-state", recording);
|
|
},
|
|
getCursorTelemetry: (videoPath?: string) => {
|
|
return ipcRenderer.invoke("get-cursor-telemetry", videoPath);
|
|
},
|
|
onStopRecordingFromTray: (callback: () => void) => {
|
|
const listener = () => callback();
|
|
ipcRenderer.on("stop-recording-from-tray", listener);
|
|
return () => ipcRenderer.removeListener("stop-recording-from-tray", listener);
|
|
},
|
|
openExternalUrl: (url: string) => {
|
|
return ipcRenderer.invoke("open-external-url", url);
|
|
},
|
|
saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => {
|
|
return ipcRenderer.invoke("save-exported-video", videoData, fileName);
|
|
},
|
|
openVideoFilePicker: () => {
|
|
return ipcRenderer.invoke("open-video-file-picker");
|
|
},
|
|
setCurrentVideoPath: (path: string) => {
|
|
return ipcRenderer.invoke("set-current-video-path", path);
|
|
},
|
|
setCurrentRecordingSession: (session: RecordingSession | null) => {
|
|
return ipcRenderer.invoke("set-current-recording-session", session);
|
|
},
|
|
getCurrentVideoPath: () => {
|
|
return ipcRenderer.invoke("get-current-video-path");
|
|
},
|
|
getCurrentRecordingSession: () => {
|
|
return ipcRenderer.invoke("get-current-recording-session");
|
|
},
|
|
readBinaryFile: (filePath: string) => {
|
|
return ipcRenderer.invoke("read-binary-file", filePath);
|
|
},
|
|
clearCurrentVideoPath: () => {
|
|
return ipcRenderer.invoke("clear-current-video-path");
|
|
},
|
|
saveProjectFile: (projectData: unknown, suggestedName?: string, existingProjectPath?: string) => {
|
|
return ipcRenderer.invoke("save-project-file", projectData, suggestedName, existingProjectPath);
|
|
},
|
|
loadProjectFile: () => {
|
|
return ipcRenderer.invoke("load-project-file");
|
|
},
|
|
loadCurrentProjectFile: () => {
|
|
return ipcRenderer.invoke("load-current-project-file");
|
|
},
|
|
onMenuLoadProject: (callback: () => void) => {
|
|
const listener = () => callback();
|
|
ipcRenderer.on("menu-load-project", listener);
|
|
return () => ipcRenderer.removeListener("menu-load-project", listener);
|
|
},
|
|
onMenuSaveProject: (callback: () => void) => {
|
|
const listener = () => callback();
|
|
ipcRenderer.on("menu-save-project", listener);
|
|
return () => ipcRenderer.removeListener("menu-save-project", listener);
|
|
},
|
|
onMenuSaveProjectAs: (callback: () => void) => {
|
|
const listener = () => callback();
|
|
ipcRenderer.on("menu-save-project-as", listener);
|
|
return () => ipcRenderer.removeListener("menu-save-project-as", listener);
|
|
},
|
|
getPlatform: () => {
|
|
return ipcRenderer.invoke("get-platform");
|
|
},
|
|
revealInFolder: (filePath: string) => {
|
|
return ipcRenderer.invoke("reveal-in-folder", filePath);
|
|
},
|
|
getShortcuts: () => {
|
|
return ipcRenderer.invoke("get-shortcuts");
|
|
},
|
|
saveShortcuts: (shortcuts: unknown) => {
|
|
return ipcRenderer.invoke("save-shortcuts", shortcuts);
|
|
},
|
|
setLocale: (locale: string) => {
|
|
return ipcRenderer.invoke("set-locale", locale);
|
|
},
|
|
setMicrophoneExpanded: (expanded: boolean) => {
|
|
ipcRenderer.send("hud:setMicrophoneExpanded", expanded);
|
|
},
|
|
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 {
|
|
const shouldClose = await callback();
|
|
ipcRenderer.send("save-before-close-done", shouldClose);
|
|
} catch {
|
|
ipcRenderer.send("save-before-close-done", false);
|
|
}
|
|
};
|
|
ipcRenderer.on("request-save-before-close", listener);
|
|
return () => ipcRenderer.removeListener("request-save-before-close", listener);
|
|
},
|
|
});
|