From 397e6b69dc4d1f504cea6b44a5ccd8da00efe68a Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:37:06 +1100 Subject: [PATCH] refactor(projects): polish finder-style browser and previews --- .../video-editor/ProjectBrowserDialog.tsx | 42 +++---- src/components/video-editor/VideoEditor.tsx | 111 ++++++++++++++++-- 2 files changed, 120 insertions(+), 33 deletions(-) diff --git a/src/components/video-editor/ProjectBrowserDialog.tsx b/src/components/video-editor/ProjectBrowserDialog.tsx index ff3109ee..390158fd 100644 --- a/src/components/video-editor/ProjectBrowserDialog.tsx +++ b/src/components/video-editor/ProjectBrowserDialog.tsx @@ -1,11 +1,5 @@ import { useMemo } from "react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { toFileUrl } from "./projectPersistence"; export type ProjectLibraryEntry = { @@ -48,15 +42,14 @@ export default function ProjectBrowserDialog({ return ( - - Projects - - Open recent Recordly projects from one place. - + + + Projects + -
+
{visibleEntries.length > 0 ? ( -
+
{visibleEntries.map((entry) => { const thumbnailSrc = entry.thumbnailPath ? toFileUrl(entry.thumbnailPath) : null; return ( @@ -64,23 +57,23 @@ export default function ProjectBrowserDialog({ key={entry.path} type="button" onClick={() => onOpenProject(entry.path)} - className="group flex flex-col overflow-hidden rounded-xl border border-white/10 bg-white/[0.03] text-left transition hover:border-[#2563EB]/60 hover:bg-white/[0.05]" + className="group flex flex-col overflow-hidden rounded-2xl border border-white/10 bg-[#1a1a1f] text-left outline-none transition hover:border-white/20 hover:bg-[#202028] focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0" > -
+
{thumbnailSrc ? ( ) : ( -
+
No preview yet
)}
- + {entry.isInProjectsDirectory ? "Library" : "Recent"} {entry.isCurrent ? ( @@ -90,10 +83,12 @@ export default function ProjectBrowserDialog({ ) : null}
-
-
{entry.name}
+
+
+ {entry.name} +
{entry.path}
-
+
Updated {formatUpdatedAt(entry.updatedAt)}
@@ -104,9 +99,6 @@ export default function ProjectBrowserDialog({ ) : (
No saved projects yet
-
- Save a project and it will appear here with a preview thumbnail. -
)}
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 0de8be18..8f8f35a3 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -34,6 +34,7 @@ import { type ExportProgress, type ExportQuality, type ExportSettings, + FrameRenderer, GIF_SIZE_PRESETS, GifExporter, type GifFrameRate, @@ -483,8 +484,8 @@ export default function VideoEditor() { } const canvas = document.createElement("canvas"); - const targetWidth = 360; - const targetHeight = 203; + const targetWidth = 320; + const targetHeight = 180; canvas.width = targetWidth; canvas.height = targetHeight; @@ -494,15 +495,76 @@ export default function VideoEditor() { } context.imageSmoothingEnabled = true; context.imageSmoothingQuality = "high"; - context.fillStyle = "#111113"; context.fillRect(0, 0, targetWidth, targetHeight); + const previewWidth = previewHandle?.containerRef.current?.clientWidth || 1920; + const previewHeight = previewHandle?.containerRef.current?.clientHeight || 1080; + const frameTimestampUs = Math.max(0, Math.round(currentTime * 1_000_000)); + + if (previewVideo && previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0) { + let videoFrame: VideoFrame | null = null; + let frameRenderer: FrameRenderer | null = null; + + try { + videoFrame = new VideoFrame(previewVideo, { timestamp: frameTimestampUs }); + frameRenderer = new FrameRenderer({ + width: targetWidth, + height: targetHeight, + wallpaper, + zoomRegions, + showShadow: shadowIntensity > 0, + shadowIntensity, + backgroundBlur, + zoomMotionBlur, + connectZooms, + zoomInDurationMs, + zoomInOverlapMs, + zoomOutDurationMs, + connectedZoomGapMs, + connectedZoomDurationMs, + zoomInEasing, + zoomOutEasing, + connectedZoomEasing, + borderRadius, + padding, + cropRegion, + webcam, + webcamUrl: webcam.sourcePath ? toFileUrl(webcam.sourcePath) : null, + videoWidth: previewVideo.videoWidth, + videoHeight: previewVideo.videoHeight, + annotationRegions, + autoCaptions, + autoCaptionSettings, + speedRegions, + previewWidth, + previewHeight, + cursorTelemetry, + showCursor, + cursorStyle, + cursorSize, + cursorSmoothing, + cursorMotionBlur, + cursorClickBounce, + cursorClickBounceDuration, + cursorSway, + }); + await frameRenderer.initialize(); + await frameRenderer.renderFrame(videoFrame, frameTimestampUs); + return frameRenderer.getCanvas().toDataURL("image/png"); + } catch (thumbnailRenderError) { + console.warn("Unable to render thumbnail from composed frame:", thumbnailRenderError); + } finally { + videoFrame?.close(); + frameRenderer?.destroy(); + } + } + const drawableSource = - previewVideo && previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0 - ? previewVideo - : previewCanvas && previewCanvas.width > 0 && previewCanvas.height > 0 - ? previewCanvas + previewCanvas && previewCanvas.width > 0 && previewCanvas.height > 0 + ? previewCanvas + : previewVideo && previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0 + ? previewVideo : null; if (!drawableSource) { @@ -533,7 +595,40 @@ export default function VideoEditor() { console.warn("Unable to capture project thumbnail:", thumbnailError); return null; } - }, []); + }, [ + annotationRegions, + autoCaptionSettings, + autoCaptions, + backgroundBlur, + borderRadius, + connectZooms, + connectedZoomDurationMs, + connectedZoomEasing, + connectedZoomGapMs, + cropRegion, + currentTime, + cursorClickBounce, + cursorClickBounceDuration, + cursorMotionBlur, + cursorSize, + cursorSmoothing, + cursorStyle, + cursorSway, + cursorTelemetry, + padding, + shadowIntensity, + showCursor, + speedRegions, + wallpaper, + webcam, + zoomInDurationMs, + zoomInEasing, + zoomInOverlapMs, + zoomMotionBlur, + zoomOutDurationMs, + zoomOutEasing, + zoomRegions, + ]); const markExportAsSaving = useCallback(() => { setExportProgress((previous) => ({