mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-14 18:00:49 +00:00
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:
1 parent
c1cb222995
commit
023a8a27a0
11 files changed
+164
-64
No files matched your search
Vendored
+5
@@ -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 }>;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || ""}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user