resolve asset base path synchronously from preload
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.
This commit is contained in:
Vendored
+1
-1
@@ -37,7 +37,7 @@ interface Window {
|
|||||||
status: string;
|
status: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
getAssetBasePath: () => Promise<string | null>;
|
assetBaseUrl: string;
|
||||||
storeRecordedVideo: (
|
storeRecordedVideo: (
|
||||||
videoData: ArrayBuffer,
|
videoData: ArrayBuffer,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
@@ -801,22 +801,6 @@ export function registerIpcHandlers(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return base path for assets so renderer can resolve file:// paths in production.
|
|
||||||
// Packaged: electron-builder extraResources copies public/wallpapers -> resources/wallpapers.
|
|
||||||
// Unpackaged: wallpapers live at <appPath>/public/wallpapers.
|
|
||||||
// Single convention: "<base>/wallpapers/x.jpg" resolves in both modes.
|
|
||||||
ipcMain.handle("get-asset-base-path", () => {
|
|
||||||
try {
|
|
||||||
const baseDir = app.isPackaged
|
|
||||||
? process.resourcesPath
|
|
||||||
: path.join(app.getAppPath(), "public");
|
|
||||||
return pathToFileURL(`${baseDir}${path.sep}`).toString();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to resolve asset base path:", err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles saving an exported video file.
|
* Handles saving an exported video file.
|
||||||
* Shows a save dialog, normalizes the file path for the current OS,
|
* Shows a save dialog, normalizes the file path for the current OS,
|
||||||
|
|||||||
+14
-4
@@ -1,17 +1,27 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
import type { RecordingSession, StoreRecordedSessionInput } from "../src/lib/recordingSession";
|
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", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
assetBaseUrl,
|
||||||
hudOverlayHide: () => {
|
hudOverlayHide: () => {
|
||||||
ipcRenderer.send("hud-overlay-hide");
|
ipcRenderer.send("hud-overlay-hide");
|
||||||
},
|
},
|
||||||
hudOverlayClose: () => {
|
hudOverlayClose: () => {
|
||||||
ipcRenderer.send("hud-overlay-close");
|
ipcRenderer.send("hud-overlay-close");
|
||||||
},
|
},
|
||||||
getAssetBasePath: async () => {
|
|
||||||
// ask main process for the correct base path (production vs dev)
|
|
||||||
return await ipcRenderer.invoke("get-asset-base-path");
|
|
||||||
},
|
|
||||||
getSources: async (opts: Electron.SourcesOptions) => {
|
getSources: async (opts: Electron.SourcesOptions) => {
|
||||||
return await ipcRenderer.invoke("get-sources", opts);
|
return await ipcRenderer.invoke("get-sources", opts);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -321,24 +321,9 @@ export function SettingsPanel({
|
|||||||
onWebcamSizePresetCommit,
|
onWebcamSizePresetCommit,
|
||||||
}: SettingsPanelProps) {
|
}: SettingsPanelProps) {
|
||||||
const t = useScopedT("settings");
|
const t = useScopedT("settings");
|
||||||
const [wallpaperPaths, setWallpaperPaths] = useState<string[]>([]);
|
const wallpaperPaths = useMemo(() => WALLPAPER_PATHS.map(resolveImageWallpaperUrl), []);
|
||||||
const [customImages, setCustomImages] = useState<string[]>([]);
|
const [customImages, setCustomImages] = useState<string[]>([]);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let mounted = true;
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const resolved = await Promise.all(WALLPAPER_PATHS.map((p) => resolveImageWallpaperUrl(p)));
|
|
||||||
if (mounted) setWallpaperPaths(resolved);
|
|
||||||
} catch (_err) {
|
|
||||||
if (mounted) setWallpaperPaths([...WALLPAPER_PATHS]);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
const colorPalette = [
|
const colorPalette = [
|
||||||
"#FF0000",
|
"#FF0000",
|
||||||
"#FFD700",
|
"#FFD700",
|
||||||
|
|||||||
@@ -1108,7 +1108,17 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
|||||||
videoReadyRafRef.current = requestAnimationFrame(waitForRenderableFrame);
|
videoReadyRafRef.current = requestAnimationFrame(waitForRenderableFrame);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [resolvedWallpaper, setResolvedWallpaper] = useState<string | null>(null);
|
const resolvedWallpaper = useMemo<string | null>(() => {
|
||||||
|
const source = wallpaper || DEFAULT_WALLPAPER;
|
||||||
|
const classified = classifyWallpaper(source);
|
||||||
|
if (classified.kind !== "image") return classified.value;
|
||||||
|
try {
|
||||||
|
return resolveImageWallpaperUrl(classified.path);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[VideoPlayback] wallpaper resolve failed:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [wallpaper]);
|
||||||
const webcamCssBoxShadow = useMemo(
|
const webcamCssBoxShadow = useMemo(
|
||||||
() => getWebcamLayoutCssBoxShadow(webcamLayoutPreset),
|
() => getWebcamLayoutCssBoxShadow(webcamLayoutPreset),
|
||||||
[webcamLayoutPreset],
|
[webcamLayoutPreset],
|
||||||
@@ -1176,28 +1186,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
|||||||
webcamVideo.currentTime = 0;
|
webcamVideo.currentTime = 0;
|
||||||
}, [webcamVideoPath]);
|
}, [webcamVideoPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let mounted = true;
|
|
||||||
(async () => {
|
|
||||||
const source = wallpaper || DEFAULT_WALLPAPER;
|
|
||||||
const classified = classifyWallpaper(source);
|
|
||||||
if (classified.kind !== "image") {
|
|
||||||
if (mounted) setResolvedWallpaper(classified.value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const resolved = await resolveImageWallpaperUrl(classified.path);
|
|
||||||
if (mounted) setResolvedWallpaper(resolved);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("[VideoPlayback] wallpaper resolve failed:", err);
|
|
||||||
if (mounted) setResolvedWallpaper(null);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, [wallpaper]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (videoReadyRafRef.current) {
|
if (videoReadyRafRef.current) {
|
||||||
|
|||||||
+20
-24
@@ -5,6 +5,13 @@ export class UnsafeAssetPathError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
function encodeRelativeAssetPath(relativePath: string): string {
|
||||||
return relativePath
|
return relativePath
|
||||||
.replace(/^\/+/, "")
|
.replace(/^\/+/, "")
|
||||||
@@ -24,33 +31,22 @@ function ensureTrailingSlash(value: string): string {
|
|||||||
return value.endsWith("/") ? value : `${value}/`;
|
return value.endsWith("/") ? value : `${value}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAssetPath(relativePath: string): Promise<string> {
|
export function getAssetPath(relativePath: string): string {
|
||||||
const encodedRelativePath = encodeRelativeAssetPath(relativePath);
|
const encoded = encodeRelativeAssetPath(relativePath);
|
||||||
|
|
||||||
try {
|
if (typeof window === "undefined") {
|
||||||
if (typeof window !== "undefined") {
|
return `/${encoded}`;
|
||||||
if (
|
|
||||||
window.location &&
|
|
||||||
window.location.protocol &&
|
|
||||||
window.location.protocol.startsWith("http")
|
|
||||||
) {
|
|
||||||
return `/${encodedRelativePath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.electronAPI && typeof window.electronAPI.getAssetBasePath === "function") {
|
|
||||||
const base = await window.electronAPI.getAssetBasePath();
|
|
||||||
if (base) {
|
|
||||||
return new URL(encodedRelativePath, ensureTrailingSlash(base)).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof UnsafeAssetPathError) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `/${encodedRelativePath}`;
|
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;
|
export default getAssetPath;
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ export class FrameRenderer {
|
|||||||
bgCtx.fillStyle = gradient;
|
bgCtx.fillStyle = gradient;
|
||||||
bgCtx.fillRect(0, 0, this.config.width, this.config.height);
|
bgCtx.fillRect(0, 0, this.config.width, this.config.height);
|
||||||
} else {
|
} else {
|
||||||
const imageUrl = await resolveImageWallpaperUrl(classified.path);
|
const imageUrl = resolveImageWallpaperUrl(classified.path);
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
if (imageUrl.startsWith("http") && !imageUrl.startsWith(window.location.origin)) {
|
if (imageUrl.startsWith("http") && !imageUrl.startsWith(window.location.origin)) {
|
||||||
img.crossOrigin = "anonymous";
|
img.crossOrigin = "anonymous";
|
||||||
|
|||||||
+31
-43
@@ -1,5 +1,5 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { UnsafeAssetPathError } from "./assetPath";
|
import { AssetBaseUnavailableError, UnsafeAssetPathError } from "./assetPath";
|
||||||
import {
|
import {
|
||||||
BackgroundLoadError,
|
BackgroundLoadError,
|
||||||
classifyWallpaper,
|
classifyWallpaper,
|
||||||
@@ -125,99 +125,87 @@ describe("resolveImageWallpaperUrl", () => {
|
|||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes through http URL", async () => {
|
it("passes through http URL", () => {
|
||||||
expect(await resolveImageWallpaperUrl("http://example.com/bg.jpg")).toBe(
|
expect(resolveImageWallpaperUrl("http://example.com/bg.jpg")).toBe("http://example.com/bg.jpg");
|
||||||
"http://example.com/bg.jpg",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes through https URL", async () => {
|
it("passes through https URL", () => {
|
||||||
expect(await resolveImageWallpaperUrl("https://example.com/bg.jpg")).toBe(
|
expect(resolveImageWallpaperUrl("https://example.com/bg.jpg")).toBe(
|
||||||
"https://example.com/bg.jpg",
|
"https://example.com/bg.jpg",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes through file:// URL", async () => {
|
it("passes through file:// URL", () => {
|
||||||
expect(await resolveImageWallpaperUrl("file:///tmp/bg.jpg")).toBe("file:///tmp/bg.jpg");
|
expect(resolveImageWallpaperUrl("file:///tmp/bg.jpg")).toBe("file:///tmp/bg.jpg");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes through data URI", async () => {
|
it("passes through data URI", () => {
|
||||||
const uri = "data:image/png;base64,AAAA";
|
const uri = "data:image/png;base64,AAAA";
|
||||||
expect(await resolveImageWallpaperUrl(uri)).toBe(uri);
|
expect(resolveImageWallpaperUrl(uri)).toBe(uri);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves leading-slash wallpaper path via http fallback", async () => {
|
it("resolves leading-slash wallpaper path via http fallback", () => {
|
||||||
expect(await resolveImageWallpaperUrl("/wallpapers/wallpaper1.jpg")).toBe(
|
expect(resolveImageWallpaperUrl("/wallpapers/wallpaper1.jpg")).toBe(
|
||||||
"/wallpapers/wallpaper1.jpg",
|
"/wallpapers/wallpaper1.jpg",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves bare relative wallpaper path", async () => {
|
it("resolves bare relative wallpaper path", () => {
|
||||||
expect(await resolveImageWallpaperUrl("wallpapers/wallpaper1.jpg")).toBe(
|
expect(resolveImageWallpaperUrl("wallpapers/wallpaper1.jpg")).toBe(
|
||||||
"/wallpapers/wallpaper1.jpg",
|
"/wallpapers/wallpaper1.jpg",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("encodes special characters in path segments", async () => {
|
it("encodes special characters in path segments", () => {
|
||||||
expect(await resolveImageWallpaperUrl("/wallpapers/my image.jpg")).toBe(
|
expect(resolveImageWallpaperUrl("/wallpapers/my image.jpg")).toBe("/wallpapers/my%20image.jpg");
|
||||||
"/wallpapers/my%20image.jpg",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects image paths outside /wallpapers/", async () => {
|
it("rejects image paths outside /wallpapers/", () => {
|
||||||
await expect(resolveImageWallpaperUrl("/etc/passwd")).rejects.toBeInstanceOf(
|
expect(() => resolveImageWallpaperUrl("/etc/passwd")).toThrow(BackgroundLoadError);
|
||||||
BackgroundLoadError,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects traversal attempts", async () => {
|
it("rejects traversal attempts", () => {
|
||||||
await expect(resolveImageWallpaperUrl("/wallpapers/../etc/passwd")).rejects.toBeInstanceOf(
|
expect(() => resolveImageWallpaperUrl("/wallpapers/../etc/passwd")).toThrow(
|
||||||
UnsafeAssetPathError,
|
UnsafeAssetPathError,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects percent-encoded traversal", async () => {
|
it("rejects percent-encoded traversal", () => {
|
||||||
await expect(resolveImageWallpaperUrl("/wallpapers/%2e%2e/app.asar")).rejects.toBeInstanceOf(
|
expect(() => resolveImageWallpaperUrl("/wallpapers/%2e%2e/app.asar")).toThrow(
|
||||||
UnsafeAssetPathError,
|
UnsafeAssetPathError,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves via electronAPI when not http", async () => {
|
it("resolves via electronAPI.assetBaseUrl when not http", () => {
|
||||||
vi.stubGlobal("window", {
|
vi.stubGlobal("window", {
|
||||||
...globalThis.window,
|
...globalThis.window,
|
||||||
location: { protocol: "file:" },
|
location: { protocol: "file:" },
|
||||||
electronAPI: {
|
electronAPI: { assetBaseUrl: "file:///opt/app/public/" },
|
||||||
getAssetBasePath: vi.fn().mockResolvedValue("file:///opt/app/public/"),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(await resolveImageWallpaperUrl("/wallpapers/wallpaper1.jpg")).toBe(
|
expect(resolveImageWallpaperUrl("/wallpapers/wallpaper1.jpg")).toBe(
|
||||||
"file:///opt/app/public/wallpapers/wallpaper1.jpg",
|
"file:///opt/app/public/wallpapers/wallpaper1.jpg",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("electronAPI branch appends trailing slash if missing", async () => {
|
it("appends trailing slash to assetBaseUrl if missing", () => {
|
||||||
vi.stubGlobal("window", {
|
vi.stubGlobal("window", {
|
||||||
...globalThis.window,
|
...globalThis.window,
|
||||||
location: { protocol: "file:" },
|
location: { protocol: "file:" },
|
||||||
electronAPI: {
|
electronAPI: { assetBaseUrl: "file:///opt/app/public" },
|
||||||
getAssetBasePath: vi.fn().mockResolvedValue("file:///opt/app/public"),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(await resolveImageWallpaperUrl("/wallpapers/wallpaper1.jpg")).toBe(
|
expect(resolveImageWallpaperUrl("/wallpapers/wallpaper1.jpg")).toBe(
|
||||||
"file:///opt/app/public/wallpapers/wallpaper1.jpg",
|
"file:///opt/app/public/wallpapers/wallpaper1.jpg",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to leading-slash relative when electronAPI returns null", async () => {
|
it("throws loudly when assetBaseUrl is empty (no silent fallback)", () => {
|
||||||
vi.stubGlobal("window", {
|
vi.stubGlobal("window", {
|
||||||
...globalThis.window,
|
...globalThis.window,
|
||||||
location: { protocol: "file:" },
|
location: { protocol: "file:" },
|
||||||
electronAPI: {
|
electronAPI: { assetBaseUrl: "" },
|
||||||
getAssetBasePath: vi.fn().mockResolvedValue(null),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(await resolveImageWallpaperUrl("/wallpapers/wallpaper1.jpg")).toBe(
|
expect(() => resolveImageWallpaperUrl("/wallpapers/wallpaper1.jpg")).toThrow(
|
||||||
"/wallpapers/wallpaper1.jpg",
|
AssetBaseUnavailableError,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function classifyWallpaper(value: string): WallpaperClassification {
|
|||||||
|
|
||||||
const ALLOWED_IMAGE_PREFIX = "/wallpapers/";
|
const ALLOWED_IMAGE_PREFIX = "/wallpapers/";
|
||||||
|
|
||||||
export async function resolveImageWallpaperUrl(imagePath: string): Promise<string> {
|
export function resolveImageWallpaperUrl(imagePath: string): string {
|
||||||
if (
|
if (
|
||||||
imagePath.startsWith("http://") ||
|
imagePath.startsWith("http://") ||
|
||||||
imagePath.startsWith("https://") ||
|
imagePath.startsWith("https://") ||
|
||||||
|
|||||||
Vendored
+1
-1
@@ -55,7 +55,7 @@ interface Window {
|
|||||||
message?: string;
|
message?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
getAssetBasePath: () => Promise<string | null>;
|
assetBaseUrl: string;
|
||||||
setRecordingState: (recording: boolean) => Promise<void>;
|
setRecordingState: (recording: boolean) => Promise<void>;
|
||||||
getCursorTelemetry: (videoPath?: string) => Promise<{
|
getCursorTelemetry: (videoPath?: string) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user