fix: stabilize editor preview and wallpaper defaults

Follow-up to PR #69: fix: restore editor preview responsiveness after export (#68).
This commit is contained in:
webadderall committed 2026-03-20 23:51:52 +11:00
1 parent c1cb222995
commit 023a8a27a0
11 files changed
+164 -64

No files matched your search

+5
View File
@@ -78,6 +78,11 @@ interface Window {
fileName: string,
) => Promise<{ success: boolean; path?: string; message?: string }>;
getRecordedVideoPath: () => Promise<{ success: boolean; path?: string; message?: string }>;
listAssetDirectory: (relativeDir: string) => Promise<{
success: boolean;
files?: string[];
error?: string;
}>;
readLocalFile: (
filePath: string,
) => Promise<{ success: boolean; data?: Uint8Array; error?: string }>;
+34 -5
View File
@@ -27,6 +27,14 @@ const AUTO_RECORDING_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000
const ALLOW_RECORDLY_WINDOW_CAPTURE = Boolean(process.env['VITE_DEV_SERVER_URL'])
const RECORDING_SESSION_MANIFEST_SUFFIX = '.recordly-session.json'
function getAssetRootPath() {
if (app.isPackaged) {
return path.join(process.resourcesPath, 'assets')
}
return path.join(app.getAppPath(), 'public')
}
function getScreen() {
return nodeRequire('electron').screen as typeof import('electron').screen
}
@@ -2869,11 +2877,7 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
// Return base path for assets so renderer can resolve file:// paths in production
ipcMain.handle('get-asset-base-path', () => {
try {
if (app.isPackaged) {
const assetPath = path.join(process.resourcesPath, 'assets')
return pathToFileURL(`${assetPath}${path.sep}`).toString()
}
const assetPath = path.join(app.getAppPath(), 'public')
const assetPath = getAssetRootPath()
return pathToFileURL(`${assetPath}${path.sep}`).toString()
} catch (err) {
console.error('Failed to resolve asset base path:', err)
@@ -2881,6 +2885,31 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
}
})
ipcMain.handle('list-asset-directory', async (_, relativeDir: string) => {
try {
const normalizedRelativeDir = String(relativeDir ?? '')
.replace(/\\/g, '/')
.replace(/^\/+/, '')
const assetRootPath = path.resolve(getAssetRootPath())
const targetDirPath = path.resolve(assetRootPath, normalizedRelativeDir)
if (targetDirPath !== assetRootPath && !targetDirPath.startsWith(`${assetRootPath}${path.sep}`)) {
return { success: false, error: 'Invalid asset directory' }
}
const entries = await fs.readdir(targetDirPath, { withFileTypes: true })
const files = entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.sort(new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare)
return { success: true, files }
} catch (error) {
console.error('Failed to list asset directory:', error)
return { success: false, error: String(error) }
}
})
ipcMain.handle('read-local-file', async (_, filePath: string) => {
try {
const data = await fs.readFile(filePath)
+3
View File
@@ -25,6 +25,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
getAssetBasePath: async () => {
return await ipcRenderer.invoke("get-asset-base-path");
},
listAssetDirectory: (relativeDir: string) => {
return ipcRenderer.invoke("list-asset-directory", relativeDir);
},
readLocalFile: (filePath: string) => {
return ipcRenderer.invoke("read-local-file", filePath);
},
+24 -9
View File
@@ -12,7 +12,8 @@ import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { getAssetPath, getRenderableAssetUrl } from "@/lib/assetPath";
import { cn } from "@/lib/utils";
import { BUILT_IN_WALLPAPERS, WALLPAPER_PATHS, WALLPAPER_RELATIVE_PATHS } from "@/lib/wallpapers";
import { BUILT_IN_WALLPAPERS, getAvailableWallpapers } from "@/lib/wallpapers";
import type { BuiltInWallpaper } from "@/lib/wallpapers";
import { type AspectRatio } from "@/utils/aspectRatioUtils";
import { useI18n, useScopedT } from "../../contexts/I18nContext";
import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel";
@@ -253,6 +254,9 @@ export function SettingsPanel({
const { t } = useI18n();
const isBackgroundPanel = panelMode === "background";
const initialEditorPreferences = useMemo(() => loadEditorPreferences(), []);
const [builtInWallpapers, setBuiltInWallpapers] = useState<BuiltInWallpaper[]>(
BUILT_IN_WALLPAPERS,
);
const [wallpaperPreviewPaths, setWallpaperPreviewPaths] = useState<string[]>([]);
const [customImages, setCustomImages] = useState<string[]>(
initialEditorPreferences.customWallpapers,
@@ -262,19 +266,30 @@ export function SettingsPanel({
padding: number;
} | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const builtInWallpaperPaths = useMemo(
() => builtInWallpapers.map((wallpaper) => wallpaper.publicPath),
[builtInWallpapers],
);
useEffect(() => {
let mounted = true;
(async () => {
try {
const availableWallpapers = await getAvailableWallpapers();
const resolved = await Promise.all(
WALLPAPER_RELATIVE_PATHS.map(async (path) =>
getRenderableAssetUrl(await getAssetPath(path)),
availableWallpapers.map(async (wallpaper) =>
getRenderableAssetUrl(await getAssetPath(wallpaper.relativePath)),
),
);
if (mounted) setWallpaperPreviewPaths(resolved);
if (mounted) {
setBuiltInWallpapers(availableWallpapers);
setWallpaperPreviewPaths(resolved);
}
} catch {
if (mounted) setWallpaperPreviewPaths(WALLPAPER_PATHS);
if (mounted) {
setBuiltInWallpapers(BUILT_IN_WALLPAPERS);
setWallpaperPreviewPaths(BUILT_IN_WALLPAPERS.map((wallpaper) => wallpaper.publicPath));
}
}
})();
return () => {
@@ -575,7 +590,7 @@ export function SettingsPanel({
setCustomImages((prev) => prev.filter((img) => img !== imageUrl));
// If the removed image was selected, clear selection
if (selected === imageUrl) {
onWallpaperChange(WALLPAPER_PATHS[0]);
onWallpaperChange(builtInWallpaperPaths[0] ?? BUILT_IN_WALLPAPERS[0]?.publicPath ?? "");
}
};
@@ -686,10 +701,10 @@ export function SettingsPanel({
{(wallpaperPreviewPaths.length > 0
? wallpaperPreviewPaths
: WALLPAPER_PATHS
: builtInWallpaperPaths
).map((previewPath, index) => {
const wallpaper = BUILT_IN_WALLPAPERS[index];
const wallpaperValue = WALLPAPER_PATHS[index] ?? previewPath;
const wallpaper = builtInWallpapers[index] ?? BUILT_IN_WALLPAPERS[index];
const wallpaperValue = wallpaper?.publicPath ?? builtInWallpaperPaths[index] ?? previewPath;
const isSelected = getWallpaperTileState(wallpaperValue, previewPath);
return renderWallpaperImageTile(previewPath, isSelected, {
key: wallpaperValue,
+4 -27
View File
@@ -15,7 +15,6 @@ import { useI18n } from "@/contexts/I18nContext";
import { useShortcuts } from "@/contexts/ShortcutsContext";
import type { AppLocale } from "@/i18n/config";
import { SUPPORTED_LOCALES } from "@/i18n/config";
import { getAssetPath } from "@/lib/assetPath";
import {
calculateOutputDimensions,
type ExportFormat,
@@ -29,7 +28,6 @@ import {
VideoExporter,
} from "@/lib/exporter";
import { matchesShortcut } from "@/lib/shortcuts";
import { DEFAULT_WALLPAPER_RELATIVE_PATH } from "@/lib/wallpapers";
import { type AspectRatio, getAspectRatioValue } from "@/utils/aspectRatioUtils";
import { ExportSettingsMenu } from "./ExportSettingsMenu";
import { loadEditorPreferences, saveEditorPreferences } from "./editorPreferences";
@@ -196,6 +194,7 @@ export default function VideoEditor() {
const [hasPendingExportSave, setHasPendingExportSave] = useState(false);
const [lastSavedSnapshot, setLastSavedSnapshot] = useState<string | null>(null);
const [showCropModal, setShowCropModal] = useState(false);
const [previewVersion, setPreviewVersion] = useState(0);
const videoPlaybackRef = useRef<VideoPlaybackRef>(null);
const nextZoomIdRef = useRef(1);
@@ -1108,31 +1107,12 @@ export default function VideoEditor() {
autoSuggestedVideoPathRef.current = videoPath;
}, [videoPath, duration, effectiveCursorTelemetry, loopCursor, zoomRegions.length]);
// Initialize default wallpaper with resolved asset path
useEffect(() => {
let mounted = true;
(async () => {
try {
const resolvedPath = await getAssetPath(DEFAULT_WALLPAPER_RELATIVE_PATH);
if (mounted) {
setWallpaper(resolvedPath);
}
} catch (err) {
// If resolution fails, keep the fallback
console.warn("Failed to resolve default wallpaper path:", err);
}
})();
return () => {
mounted = false;
};
}, []);
function togglePlayPause() {
const playback = videoPlaybackRef.current;
const video = playback?.video;
if (!playback || !video) return;
if (isPlaying) {
if (!video.paused && !video.ended) {
playback.pause();
} else {
playback.play().catch((err) => console.error("Video play failed:", err));
@@ -1968,7 +1948,6 @@ export default function VideoEditor() {
videoPlaybackRef.current?.play();
} else {
video.currentTime = restoreTime;
await videoPlaybackRef.current?.refreshFrame();
}
} catch (error) {
console.error("Export error:", error);
@@ -1976,13 +1955,11 @@ export default function VideoEditor() {
setExportError(errorMessage);
toast.error(`Export failed: ${errorMessage}`);
} finally {
if (!isPlaying) {
await videoPlaybackRef.current?.refreshFrame().catch(() => undefined);
}
setIsExporting(false);
exporterRef.current = null;
setShowExportDropdown(keepExportDialogOpen);
setExportProgress(null);
setPreviewVersion((version) => version + 1);
}
},
[
@@ -2421,7 +2398,7 @@ export default function VideoEditor() {
}}
>
<VideoPlayback
key={videoPath || "no-video"}
key={`${videoPath || "no-video"}:${previewVersion}`}
aspectRatio={aspectRatio}
ref={videoPlaybackRef}
videoPath={videoPath || ""}
+10 -6
View File
@@ -652,6 +652,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
cursorSwayRef.current = cursorSway;
}, [cursorSway]);
useEffect(() => {
currentTimeRef.current = currentTime * 1000;
}, [currentTime]);
useEffect(() => {
if (!pixiReady || !videoReady) return;
@@ -672,11 +676,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
}
animationStateRef.current = createPlaybackAnimationState();
// Reset cursor overlay smoothing on layout change
cursorOverlayRef.current?.reset();
// Reset motion blur state for clean transitions
motionBlurStateRef.current = createMotionBlurState();
if (blurFilterRef.current) {
@@ -1248,10 +1248,14 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
) => {
const video = e.currentTarget;
onDurationChange(video.duration);
video.currentTime = 0;
const targetTime = clampMediaTimeToDuration(
currentTime,
Number.isFinite(video.duration) ? video.duration : null,
);
video.currentTime = targetTime;
video.pause();
allowPlaybackRef.current = false;
currentTimeRef.current = 0;
currentTimeRef.current = targetTime * 1000;
if (videoReadyRafRef.current) {
cancelAnimationFrame(videoReadyRafRef.current);
@@ -101,6 +101,19 @@ describe("editorPreferences", () => {
});
});
it("preserves a stored wallpaper choice on startup", () => {
vi.stubGlobal(
"localStorage",
createStorageMock({
[EDITOR_PREFERENCES_STORAGE_KEY]: JSON.stringify({
wallpaper: "/wallpapers/wallpaper1.jpg",
}),
}),
);
expect(loadEditorPreferences().wallpaper).toBe("/wallpapers/wallpaper1.jpg");
});
it("preserves the last valid custom aspect inputs while typing", () => {
const localStorage = createStorageMock({
[EDITOR_PREFERENCES_STORAGE_KEY]: JSON.stringify({
@@ -1,5 +1,5 @@
import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter";
import { WALLPAPER_PATHS } from "@/lib/wallpapers";
import { DEFAULT_WALLPAPER_PATH } from "@/lib/wallpapers";
import { ASPECT_RATIOS, type AspectRatio, isCustomAspectRatio } from "@/utils/aspectRatioUtils";
import {
type AnnotationRegion,
@@ -369,7 +369,7 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
: null;
return {
wallpaper: typeof editor.wallpaper === "string" ? editor.wallpaper : WALLPAPER_PATHS[0],
wallpaper: typeof editor.wallpaper === "string" ? editor.wallpaper : DEFAULT_WALLPAPER_PATH,
shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0.67,
backgroundBlur: normalizedBackgroundBlur,
zoomMotionBlur: normalizedZoomMotionBlur,
+1 -1
View File
@@ -79,7 +79,7 @@ export const DEFAULT_CURSOR_SWAY = 0.25;
export const DEFAULT_ZOOM_MOTION_BLUR = 0.35;
export const DEFAULT_WEBCAM_SIZE = 40;
export const DEFAULT_WEBCAM_REACT_TO_ZOOM = true;
export const DEFAULT_WEBCAM_CORNER_RADIUS = 82;
export const DEFAULT_WEBCAM_CORNER_RADIUS = 90;
export const DEFAULT_WEBCAM_SHADOW = 0.67;
export const DEFAULT_WEBCAM_MARGIN = 24;
export const DEFAULT_WEBCAM_POSITION_PRESET: WebcamPositionPreset = "bottom-right";
@@ -78,11 +78,6 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
}
const handlePlay = () => {
if (isSeekingRef.current) {
video.pause();
return;
}
if (!allowPlaybackRef.current) {
video.pause();
return;
@@ -123,19 +118,12 @@ export function createVideoEventHandlers(params: VideoEventHandlersParams) {
emitTime(skipToTime);
}
} else {
if (!isPlayingRef.current && !video.paused) {
video.pause();
}
emitTime(video.currentTime);
}
};
const handleSeeking = () => {
isSeekingRef.current = true;
if (!isPlayingRef.current && !video.paused) {
video.pause();
}
emitTime(video.currentTime);
};
+68 -2
View File
@@ -5,6 +5,8 @@ export interface BuiltInWallpaper {
publicPath: string;
}
const IMAGE_FILE_PATTERN = /\.(avif|gif|jpe?g|png|svg|webp)$/i;
export const BUILT_IN_WALLPAPERS: BuiltInWallpaper[] = [
{ id: 'wallpaper-1', label: 'Wallpaper 1', relativePath: 'wallpapers/wallpaper1.jpg', publicPath: '/wallpapers/wallpaper1.jpg' },
{ id: 'wallpaper-2', label: 'Wallpaper 2', relativePath: 'wallpapers/wallpaper2.jpg', publicPath: '/wallpapers/wallpaper2.jpg' },
@@ -33,5 +35,69 @@ export const BUILT_IN_WALLPAPERS: BuiltInWallpaper[] = [
export const WALLPAPER_PATHS = BUILT_IN_WALLPAPERS.map((wallpaper) => wallpaper.publicPath);
export const WALLPAPER_RELATIVE_PATHS = BUILT_IN_WALLPAPERS.map((wallpaper) => wallpaper.relativePath);
export const DEFAULT_WALLPAPER_PATH = WALLPAPER_PATHS[0];
export const DEFAULT_WALLPAPER_RELATIVE_PATH = WALLPAPER_RELATIVE_PATHS[0];
export const DEFAULT_WALLPAPER_PATH = '/wallpapers/wallpaper2.jpg';
export const DEFAULT_WALLPAPER_RELATIVE_PATH = 'wallpapers/wallpaper2.jpg';
function toWallpaperId(fileName: string) {
return fileName
.replace(/\.[^.]+$/, "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function toWallpaperLabel(fileName: string) {
const baseName = fileName.replace(/\.[^.]+$/, "").trim();
if (!baseName) {
return "Wallpaper";
}
return baseName
.replace(/[_-]+/g, " ")
.replace(/\s+/g, " ")
.replace(/\b\w/g, (match) => match.toUpperCase());
}
function createWallpaperEntry(fileName: string): BuiltInWallpaper {
const encodedFileName = encodeURIComponent(fileName);
return {
id: toWallpaperId(fileName) || `wallpaper-${encodedFileName.toLowerCase()}`,
label: toWallpaperLabel(fileName),
relativePath: `wallpapers/${fileName}`,
publicPath: `/wallpapers/${encodedFileName}`,
};
}
function sortWallpaperFiles(fileNames: string[]) {
return [...fileNames].sort(
new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare,
);
}
export async function getAvailableWallpapers(): Promise<BuiltInWallpaper[]> {
const fallbackWallpapers = BUILT_IN_WALLPAPERS;
if (typeof window === "undefined" || !window.electronAPI?.listAssetDirectory) {
return fallbackWallpapers;
}
try {
const result = await window.electronAPI.listAssetDirectory("wallpapers");
if (!result.success || !result.files?.length) {
return fallbackWallpapers;
}
const discoveredFiles = sortWallpaperFiles(
result.files.filter((fileName) => IMAGE_FILE_PATTERN.test(fileName)),
);
if (discoveredFiles.length === 0) {
return fallbackWallpapers;
}
return discoveredFiles.map(createWallpaperEntry);
} catch {
return fallbackWallpapers;
}
}