Skip to content

Commit c53dd2d

Browse files
Merge pull request #496 from Enriquefft/fix/wallpaper-export-376
Fix wallpaper backgrounds exporting as black (#376)
2 parents 67ec577 + e06e40d commit c53dd2d

27 files changed

Lines changed: 721 additions & 383 deletions

electron-builder.json5

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
"!CONTRIBUTING.md",
2121
"!LICENSE"
2222
],
23+
// Asset layout contract: "wallpapers/" under resourcesPath must align with
24+
// assetBaseDir in electron/preload.ts (packaged branch).
2325
"extraResources": [
2426
{
2527
"from": "public/wallpapers",
26-
"to": "assets/wallpapers"
28+
"to": "wallpapers"
2729
}
2830
],
2931

electron/electron-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface Window {
3737
status: string;
3838
error?: string;
3939
}>;
40-
getAssetBasePath: () => Promise<string | null>;
40+
assetBaseUrl: string;
4141
storeRecordedVideo: (
4242
videoData: ArrayBuffer,
4343
fileName: string,

electron/ipc/handlers.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
3-
import { fileURLToPath, pathToFileURL } from "node:url";
3+
import { fileURLToPath } from "node:url";
44
import {
55
app,
66
BrowserWindow,
@@ -801,21 +801,6 @@ export function registerIpcHandlers(
801801
}
802802
});
803803

804-
// Return base path for assets so renderer can resolve file:// paths in production
805-
ipcMain.handle("get-asset-base-path", () => {
806-
try {
807-
if (app.isPackaged) {
808-
const assetPath = path.join(process.resourcesPath, "assets");
809-
return pathToFileURL(`${assetPath}${path.sep}`).toString();
810-
}
811-
const assetPath = path.join(app.getAppPath(), "public", "assets");
812-
return pathToFileURL(`${assetPath}${path.sep}`).toString();
813-
} catch (err) {
814-
console.error("Failed to resolve asset base path:", err);
815-
return null;
816-
}
817-
});
818-
819804
/**
820805
* Handles saving an exported video file.
821806
* Shows a save dialog, normalizes the file path for the current OS,

electron/preload.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1+
import path from "node:path";
2+
import { pathToFileURL } from "node:url";
13
import { contextBridge, ipcRenderer } from "electron";
24
import type { RecordingSession, StoreRecordedSessionInput } from "../src/lib/recordingSession";
35

6+
// Asset base URL is a build-time constant per process; resolve once here so
7+
// the renderer can consume it synchronously. Packaged: electron-builder
8+
// extraResources copies public/wallpapers -> resources/wallpapers (see
9+
// electron-builder.json5). Unpackaged: wallpapers live at <appRoot>/public/,
10+
// and __dirname in dist-electron resolves to <appRoot>/dist-electron/.
11+
const isPackagedProcess = !process.defaultApp;
12+
const assetBaseDir = isPackagedProcess
13+
? process.resourcesPath
14+
: path.join(__dirname, "..", "public");
15+
const assetBaseUrl = pathToFileURL(`${assetBaseDir}${path.sep}`).toString();
16+
417
contextBridge.exposeInMainWorld("electronAPI", {
18+
assetBaseUrl,
519
hudOverlayHide: () => {
620
ipcRenderer.send("hud-overlay-hide");
721
},
822
hudOverlayClose: () => {
923
ipcRenderer.send("hud-overlay-close");
1024
},
11-
getAssetBasePath: async () => {
12-
// ask main process for the correct base path (production vs dev)
13-
return await ipcRenderer.invoke("get-asset-base-path");
14-
},
1525
getSources: async (opts: Electron.SourcesOptions) => {
1626
return await ipcRenderer.invoke("get-sources", opts);
1727
},

nix/package.nix

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ buildNpmPackage {
6868
cp -r node_modules "$out/lib/openscreen/"
6969
7070
# Asset resolution: when app.isPackaged is false, the main process resolves
71-
# assets at <appPath>/public/assets/. Mirror the electron-builder
72-
# extraResources layout so wallpapers load correctly.
73-
mkdir -p "$out/lib/openscreen/public/assets"
74-
cp -r public/wallpapers "$out/lib/openscreen/public/assets/wallpapers"
71+
# assets at <appPath>/public/. Place wallpapers at that root to match the
72+
# packaged layout (electron-builder extraResources -> resources/wallpapers).
73+
mkdir -p "$out/lib/openscreen/public"
74+
cp -r public/wallpapers "$out/lib/openscreen/public/wallpapers"
7575
7676
# Wrap system electron with the app directory
7777
mkdir -p "$out/bin"

src/components/video-editor/SettingsPanel.tsx

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
Upload,
1515
X,
1616
} from "lucide-react";
17-
import { useCallback, useEffect, useRef, useState } from "react";
17+
import { useCallback, useMemo, useRef, useState } from "react";
1818
import { toast } from "sonner";
1919
import {
2020
Accordion,
@@ -34,11 +34,11 @@ import { Slider } from "@/components/ui/slider";
3434
import { Switch } from "@/components/ui/switch";
3535
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
3636
import { useScopedT } from "@/contexts/I18nContext";
37-
import { getAssetPath } from "@/lib/assetPath";
3837
import { WEBCAM_LAYOUT_PRESETS } from "@/lib/compositeLayout";
3938
import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter";
4039
import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter";
4140
import { cn } from "@/lib/utils";
41+
import { resolveImageWallpaperUrl, WALLPAPER_PATHS } from "@/lib/wallpaper";
4242
import { type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils";
4343
import { getTestId } from "@/utils/getTestId";
4444
import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel";
@@ -123,11 +123,6 @@ function CustomSpeedInput({
123123
);
124124
}
125125

126-
const WALLPAPER_COUNT = 18;
127-
const WALLPAPER_RELATIVE = Array.from(
128-
{ length: WALLPAPER_COUNT },
129-
(_, i) => `wallpapers/wallpaper${i + 1}.jpg`,
130-
);
131126
const GRADIENTS = [
132127
"linear-gradient( 111.6deg, rgba(114,167,232,1) 9.4%, rgba(253,129,82,1) 43.9%, rgba(253,129,82,1) 54.8%, rgba(249,202,86,1) 86.3% )",
133128
"linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%)",
@@ -326,24 +321,12 @@ export function SettingsPanel({
326321
onWebcamSizePresetCommit,
327322
}: SettingsPanelProps) {
328323
const t = useScopedT("settings");
329-
const [wallpaperPaths, setWallpaperPaths] = useState<string[]>([]);
324+
// Resolved URLs are for DOM rendering only (backgroundImage). The canonical
325+
// `/wallpapers/wallpaperN.jpg` form in WALLPAPER_PATHS is what gets persisted
326+
// on click — never the machine-specific file:// URL.
327+
const wallpaperPreviewUrls = useMemo(() => WALLPAPER_PATHS.map(resolveImageWallpaperUrl), []);
330328
const [customImages, setCustomImages] = useState<string[]>([]);
331329
const fileInputRef = useRef<HTMLInputElement>(null);
332-
333-
useEffect(() => {
334-
let mounted = true;
335-
(async () => {
336-
try {
337-
const resolved = await Promise.all(WALLPAPER_RELATIVE.map((p) => getAssetPath(p)));
338-
if (mounted) setWallpaperPaths(resolved);
339-
} catch (_err) {
340-
if (mounted) setWallpaperPaths(WALLPAPER_RELATIVE.map((p) => `/${p}`));
341-
}
342-
})();
343-
return () => {
344-
mounted = false;
345-
};
346-
}, []);
347330
const colorPalette = [
348331
"#FF0000",
349332
"#FFD700",
@@ -526,7 +509,7 @@ export function SettingsPanel({
526509
setCustomImages((prev) => prev.filter((img) => img !== imageUrl));
527510
// If the removed image was selected, clear selection
528511
if (selected === imageUrl) {
529-
onWallpaperChange(wallpaperPaths[0] || WALLPAPER_RELATIVE[0]);
512+
onWallpaperChange(WALLPAPER_PATHS[0]);
530513
}
531514
};
532515

@@ -1146,38 +1129,24 @@ export function SettingsPanel({
11461129
);
11471130
})}
11481131

1149-
{(wallpaperPaths.length > 0
1150-
? wallpaperPaths
1151-
: WALLPAPER_RELATIVE.map((p) => `/${p}`)
1152-
).map((path) => {
1153-
const isSelected = (() => {
1154-
if (!selected) return false;
1155-
if (selected === path) return true;
1156-
try {
1157-
const clean = (s: string) =>
1158-
s.replace(/^file:\/\//, "").replace(/^\//, "");
1159-
if (clean(selected).endsWith(clean(path))) return true;
1160-
if (clean(path).endsWith(clean(selected))) return true;
1161-
} catch {
1162-
// Best-effort comparison; fallback to strict match.
1163-
}
1164-
return false;
1165-
})();
1132+
{WALLPAPER_PATHS.map((canonicalPath, i) => {
1133+
const previewUrl = wallpaperPreviewUrls[i] ?? canonicalPath;
1134+
const isSelected = selected === canonicalPath;
11661135
return (
11671136
<div
1168-
key={path}
1137+
key={canonicalPath}
11691138
className={cn(
11701139
"aspect-square w-9 h-9 rounded-md border-2 overflow-hidden cursor-pointer transition-all duration-200 shadow-sm",
11711140
isSelected
11721141
? "border-[#34B27B] ring-1 ring-[#34B27B]/30"
11731142
: "border-white/10 hover:border-[#34B27B]/40 opacity-80 hover:opacity-100 bg-white/5",
11741143
)}
11751144
style={{
1176-
backgroundImage: `url(${path})`,
1145+
backgroundImage: `url(${previewUrl})`,
11771146
backgroundSize: "cover",
11781147
backgroundPosition: "center",
11791148
}}
1180-
onClick={() => onWallpaperChange(path)}
1149+
onClick={() => onWallpaperChange(canonicalPath)}
11811150
role="button"
11821151
/>
11831152
);

src/components/video-editor/VideoEditor.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { computeFrameStepTime } from "@/lib/frameStep";
3232
import type { ProjectMedia } from "@/lib/recordingSession";
3333
import { matchesShortcut } from "@/lib/shortcuts";
3434
import { loadUserPreferences, saveUserPreferences } from "@/lib/userPreferences";
35+
import { BackgroundLoadError } from "@/lib/wallpaper";
3536
import {
3637
getAspectRatioValue,
3738
getNativeAspectRatioValue,
@@ -1566,9 +1567,15 @@ export default function VideoEditor() {
15661567
}
15671568
} catch (error) {
15681569
console.error("Export error:", error);
1569-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
1570-
setExportError(errorMessage);
1571-
toast.error(`Export failed: ${errorMessage}`);
1570+
if (error instanceof BackgroundLoadError) {
1571+
const message = t("errors.exportBackgroundLoadFailed", { url: error.displayUrl });
1572+
setExportError(message);
1573+
toast.error(message);
1574+
} else {
1575+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
1576+
setExportError(errorMessage);
1577+
toast.error(t("errors.exportFailedWithError", { error: errorMessage }));
1578+
}
15721579
} finally {
15731580
setIsExporting(false);
15741581
exporterRef.current = null;
@@ -1601,6 +1608,7 @@ export default function VideoEditor() {
16011608
exportQuality,
16021609
handleExportSaved,
16031610
cursorTelemetry,
1611+
t,
16041612
],
16051613
);
16061614

src/components/video-editor/VideoPlayback.tsx

Lines changed: 12 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import {
1818
useRef,
1919
useState,
2020
} from "react";
21-
import { getAssetPath } from "@/lib/assetPath";
2221
import {
2322
getWebcamLayoutCssBoxShadow,
2423
type Size,
2524
type StyledRenderRect,
2625
type WebcamLayoutPreset,
2726
type WebcamSizePreset,
2827
} from "@/lib/compositeLayout";
28+
import { classifyWallpaper, DEFAULT_WALLPAPER, resolveImageWallpaperUrl } from "@/lib/wallpaper";
2929
import { getCssClipPath } from "@/lib/webcamMaskShapes";
3030
import {
3131
type AspectRatio,
@@ -1108,7 +1108,17 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
11081108
videoReadyRafRef.current = requestAnimationFrame(waitForRenderableFrame);
11091109
};
11101110

1111-
const [resolvedWallpaper, setResolvedWallpaper] = useState<string | null>(null);
1111+
const resolvedWallpaper = useMemo<string | null>(() => {
1112+
const source = wallpaper || DEFAULT_WALLPAPER;
1113+
const classified = classifyWallpaper(source);
1114+
if (classified.kind !== "image") return classified.value;
1115+
try {
1116+
return resolveImageWallpaperUrl(classified.path);
1117+
} catch (err) {
1118+
console.warn("[VideoPlayback] wallpaper resolve failed:", err);
1119+
return null;
1120+
}
1121+
}, [wallpaper]);
11121122
const webcamCssBoxShadow = useMemo(
11131123
() => getWebcamLayoutCssBoxShadow(webcamLayoutPreset),
11141124
[webcamLayoutPreset],
@@ -1176,58 +1186,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
11761186
webcamVideo.currentTime = 0;
11771187
}, [webcamVideoPath]);
11781188

1179-
useEffect(() => {
1180-
let mounted = true;
1181-
(async () => {
1182-
try {
1183-
if (!wallpaper) {
1184-
const def = await getAssetPath("wallpapers/wallpaper1.jpg");
1185-
if (mounted) setResolvedWallpaper(def);
1186-
return;
1187-
}
1188-
1189-
if (
1190-
wallpaper.startsWith("#") ||
1191-
wallpaper.startsWith("linear-gradient") ||
1192-
wallpaper.startsWith("radial-gradient")
1193-
) {
1194-
if (mounted) setResolvedWallpaper(wallpaper);
1195-
return;
1196-
}
1197-
1198-
// If it's a data URL (custom uploaded image), use as-is
1199-
if (wallpaper.startsWith("data:")) {
1200-
if (mounted) setResolvedWallpaper(wallpaper);
1201-
return;
1202-
}
1203-
1204-
// If it's an absolute web/http or file path, use as-is
1205-
if (
1206-
wallpaper.startsWith("http") ||
1207-
wallpaper.startsWith("file://") ||
1208-
wallpaper.startsWith("/")
1209-
) {
1210-
// If it's an absolute server path (starts with '/'), resolve via getAssetPath as well
1211-
if (wallpaper.startsWith("/")) {
1212-
const rel = wallpaper.replace(/^\//, "");
1213-
const p = await getAssetPath(rel);
1214-
if (mounted) setResolvedWallpaper(p);
1215-
return;
1216-
}
1217-
if (mounted) setResolvedWallpaper(wallpaper);
1218-
return;
1219-
}
1220-
const p = await getAssetPath(wallpaper.replace(/^\//, ""));
1221-
if (mounted) setResolvedWallpaper(p);
1222-
} catch (_err) {
1223-
if (mounted) setResolvedWallpaper(wallpaper || "/wallpapers/wallpaper1.jpg");
1224-
}
1225-
})();
1226-
return () => {
1227-
mounted = false;
1228-
};
1229-
}, [wallpaper]);
1230-
12311189
useEffect(() => {
12321190
return () => {
12331191
if (videoReadyRafRef.current) {

0 commit comments

Comments
 (0)