refactor(projects): polish finder-style browser and previews

This commit is contained in:
webadderall
2026-03-23 20:37:06 +11:00
parent 5b0bf63749
commit 397e6b69dc
2 changed files with 120 additions and 33 deletions
@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-7xl border-white/10 bg-[#131317] p-0 text-slate-200 shadow-2xl">
<DialogHeader className="border-b border-white/10 px-6 py-5">
<DialogTitle className="text-xl font-semibold text-white">Projects</DialogTitle>
<DialogDescription className="text-sm text-slate-400">
Open recent Recordly projects from one place.
</DialogDescription>
<DialogHeader className="border-b border-white/10 px-6 py-4">
<DialogTitle className="text-lg font-semibold tracking-tight text-white">
Projects
</DialogTitle>
</DialogHeader>
<div className="max-h-[70vh] overflow-y-auto px-6 py-5">
<div className="max-h-[70vh] overflow-y-auto px-5 py-5">
{visibleEntries.length > 0 ? (
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4 xl:grid-cols-4">
{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"
>
<div className="relative aspect-video w-full overflow-hidden bg-[#0d0d11]">
<div className="relative aspect-[16/10] w-full overflow-hidden bg-[#0d0d11]">
{thumbnailSrc ? (
<img
src={thumbnailSrc}
alt=""
className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.02]"
className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.015]"
draggable={false}
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-[radial-gradient(circle_at_top,_rgba(37,99,235,0.3),_transparent_55%),linear-gradient(180deg,_rgba(255,255,255,0.06),_rgba(255,255,255,0.02))] text-sm font-medium text-slate-400">
<div className="flex h-full w-full items-center justify-center bg-[radial-gradient(circle_at_top,_rgba(37,99,235,0.18),_transparent_55%),linear-gradient(180deg,_rgba(255,255,255,0.05),_rgba(255,255,255,0.02))] text-sm font-medium text-slate-400">
No preview yet
</div>
)}
<div className="absolute inset-x-0 top-0 flex items-center justify-between px-3 py-3">
<span className="rounded-full bg-black/50 px-2 py-1 text-[10px] font-medium uppercase tracking-[0.18em] text-slate-200 backdrop-blur">
<span className="rounded-full bg-black/45 px-2 py-1 text-[10px] font-medium uppercase tracking-[0.18em] text-slate-200 backdrop-blur">
{entry.isInProjectsDirectory ? "Library" : "Recent"}
</span>
{entry.isCurrent ? (
@@ -90,10 +83,12 @@ export default function ProjectBrowserDialog({
) : null}
</div>
</div>
<div className="flex flex-1 flex-col gap-1.5 px-3 py-3">
<div className="truncate text-sm font-semibold text-white">{entry.name}</div>
<div className="flex flex-1 flex-col gap-1 px-3 py-3">
<div className="truncate text-[15px] font-semibold tracking-tight text-white">
{entry.name}
</div>
<div className="truncate text-[11px] text-slate-500">{entry.path}</div>
<div className="text-[11px] text-slate-400">
<div className="pt-1 text-[11px] text-slate-400">
Updated {formatUpdatedAt(entry.updatedAt)}
</div>
</div>
@@ -104,9 +99,6 @@ export default function ProjectBrowserDialog({
) : (
<div className="flex min-h-[280px] flex-col items-center justify-center gap-4 rounded-2xl border border-dashed border-white/10 bg-white/[0.02] px-8 text-center">
<div className="text-lg font-semibold text-white">No saved projects yet</div>
<div className="max-w-md text-sm text-slate-400">
Save a project and it will appear here with a preview thumbnail.
</div>
</div>
)}
</div>
+103 -8
View File
@@ -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) => ({