mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
refactor(projects): polish finder-style browser and previews
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user