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.
53 lines
1.3 KiB
TypeScript
53 lines
1.3 KiB
TypeScript
export class UnsafeAssetPathError extends Error {
|
|
constructor(segment: string) {
|
|
super(`Unsafe asset path segment: ${segment}`);
|
|
this.name = "UnsafeAssetPathError";
|
|
}
|
|
}
|
|
|
|
export class AssetBaseUnavailableError extends Error {
|
|
constructor() {
|
|
super("electronAPI.assetBaseUrl is not available; preload did not load correctly");
|
|
this.name = "AssetBaseUnavailableError";
|
|
}
|
|
}
|
|
|
|
function encodeRelativeAssetPath(relativePath: string): string {
|
|
return relativePath
|
|
.replace(/^\/+/, "")
|
|
.split("/")
|
|
.filter(Boolean)
|
|
.map((part) => {
|
|
const decoded = decodeURIComponent(part);
|
|
if (decoded === "." || decoded === "..") {
|
|
throw new UnsafeAssetPathError(decoded);
|
|
}
|
|
return encodeURIComponent(decoded);
|
|
})
|
|
.join("/");
|
|
}
|
|
|
|
function ensureTrailingSlash(value: string): string {
|
|
return value.endsWith("/") ? value : `${value}/`;
|
|
}
|
|
|
|
export function getAssetPath(relativePath: string): string {
|
|
const encoded = encodeRelativeAssetPath(relativePath);
|
|
|
|
if (typeof window === "undefined") {
|
|
return `/${encoded}`;
|
|
}
|
|
|
|
if (window.location?.protocol?.startsWith("http")) {
|
|
return `/${encoded}`;
|
|
}
|
|
|
|
const base = window.electronAPI?.assetBaseUrl;
|
|
if (!base) {
|
|
throw new AssetBaseUnavailableError();
|
|
}
|
|
return new URL(encoded, ensureTrailingSlash(base)).toString();
|
|
}
|
|
|
|
export default getAssetPath;
|