refactor(projects): simplify browser modal and thumbnail capture

This commit is contained in:
webadderall
2026-03-23 19:42:50 +11:00
parent ddf229d797
commit 5b0bf63749
2 changed files with 24 additions and 133 deletions
@@ -1,6 +1,4 @@
import { FolderOpen, Save } from "lucide-react";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -23,11 +21,7 @@ type ProjectBrowserDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
entries: ProjectLibraryEntry[];
projectsDirectoryPath: string | null;
onOpenProject: (projectPath: string) => void;
onBrowseProjectFiles: () => void;
onOpenProjectsFolder: () => void;
onSaveProjectAs: () => void;
};
function formatUpdatedAt(updatedAt: number) {
@@ -47,65 +41,22 @@ export default function ProjectBrowserDialog({
open,
onOpenChange,
entries,
projectsDirectoryPath,
onOpenProject,
onBrowseProjectFiles,
onOpenProjectsFolder,
onSaveProjectAs,
}: ProjectBrowserDialogProps) {
const visibleEntries = useMemo(() => entries.slice(0, 16), [entries]);
const visibleEntries = useMemo(() => entries.slice(0, 24), [entries]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl border-white/10 bg-[#131317] p-0 text-slate-200 shadow-2xl">
<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. New projects are saved to the dedicated
projects folder by default.
Open recent Recordly projects from one place.
</DialogDescription>
</DialogHeader>
<div className="flex items-center justify-between gap-3 border-b border-white/10 px-6 py-4">
<div className="min-w-0">
<div className="text-xs font-medium uppercase tracking-[0.18em] text-slate-500">
Projects Folder
</div>
<div className="truncate text-sm text-slate-300">
{projectsDirectoryPath ?? "Loading projects folder..."}
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={onOpenProjectsFolder}
className="border-white/10 bg-white/5 text-slate-200 hover:bg-white/10 hover:text-white"
>
<FolderOpen className="h-4 w-4" />
Open Folder
</Button>
<Button
type="button"
variant="outline"
onClick={onBrowseProjectFiles}
className="border-white/10 bg-white/5 text-slate-200 hover:bg-white/10 hover:text-white"
>
<FolderOpen className="h-4 w-4" />
Browse Files
</Button>
<Button
type="button"
onClick={onSaveProjectAs}
className="bg-[#2563EB] text-white hover:bg-[#2563EB]/90"
>
<Save className="h-4 w-4" />
Save As
</Button>
</div>
</div>
<div className="max-h-[70vh] overflow-y-auto px-6 py-5">
{visibleEntries.length > 0 ? (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{visibleEntries.map((entry) => {
const thumbnailSrc = entry.thumbnailPath ? toFileUrl(entry.thumbnailPath) : null;
return (
@@ -113,7 +64,7 @@ export default function ProjectBrowserDialog({
key={entry.path}
type="button"
onClick={() => onOpenProject(entry.path)}
className="group flex flex-col overflow-hidden rounded-2xl 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-xl border border-white/10 bg-white/[0.03] text-left transition hover:border-[#2563EB]/60 hover:bg-white/[0.05]"
>
<div className="relative aspect-video w-full overflow-hidden bg-[#0d0d11]">
{thumbnailSrc ? (
@@ -139,12 +90,10 @@ export default function ProjectBrowserDialog({
) : null}
</div>
</div>
<div className="flex flex-1 flex-col gap-2 px-4 py-4">
<div className="truncate text-base font-semibold text-white">
{entry.name}
</div>
<div className="truncate text-xs text-slate-500">{entry.path}</div>
<div className="text-xs text-slate-400">
<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="truncate text-[11px] text-slate-500">{entry.path}</div>
<div className="text-[11px] text-slate-400">
Updated {formatUpdatedAt(entry.updatedAt)}
</div>
</div>
@@ -156,27 +105,7 @@ 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 Recordly will keep it in the projects folder with a preview
thumbnail so it is easy to reopen later.
</div>
<div className="flex items-center gap-2">
<Button
type="button"
onClick={onSaveProjectAs}
className="bg-[#2563EB] text-white hover:bg-[#2563EB]/90"
>
<Save className="h-4 w-4" />
Save Project
</Button>
<Button
type="button"
variant="outline"
onClick={onBrowseProjectFiles}
className="border-white/10 bg-white/5 text-slate-200 hover:bg-white/10 hover:text-white"
>
<FolderOpen className="h-4 w-4" />
Browse Existing
</Button>
Save a project and it will appear here with a preview thumbnail.
</div>
</div>
)}
+14 -52
View File
@@ -305,7 +305,6 @@ export default function VideoEditor() {
const [videoSourcePath, setVideoSourcePath] = useState<string | null>(null);
const [currentProjectPath, setCurrentProjectPath] = useState<string | null>(null);
const [projectLibraryEntries, setProjectLibraryEntries] = useState<ProjectLibraryEntry[]>([]);
const [projectsDirectoryPath, setProjectsDirectoryPath] = useState<string | null>(null);
const [projectBrowserOpen, setProjectBrowserOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -460,7 +459,6 @@ export default function VideoEditor() {
throw new Error(result.error || "Failed to load project library");
}
setProjectsDirectoryPath(result.projectsDir ?? null);
setProjectLibraryEntries(result.entries);
} catch (projectLibraryError) {
console.warn("Unable to refresh project library:", projectLibraryError);
@@ -469,12 +467,13 @@ export default function VideoEditor() {
const captureProjectThumbnail = useCallback(async () => {
const previewHandle = videoPlaybackRef.current;
const previewCanvas = previewHandle?.app?.canvas ?? null;
const previewVideo = previewHandle?.video ?? null;
const previewCanvas = previewHandle?.app?.canvas ?? null;
if (previewHandle && previewVideo && previewVideo.paused) {
try {
await previewHandle.refreshFrame();
await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
} catch (thumbnailRefreshError) {
console.warn(
"Unable to refresh preview frame before thumbnail capture:",
@@ -484,8 +483,8 @@ export default function VideoEditor() {
}
const canvas = document.createElement("canvas");
const targetWidth = 1280;
const targetHeight = 720;
const targetWidth = 360;
const targetHeight = 203;
canvas.width = targetWidth;
canvas.height = targetHeight;
@@ -493,15 +492,17 @@ export default function VideoEditor() {
if (!context) {
return null;
}
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = "high";
context.fillStyle = "#111113";
context.fillRect(0, 0, targetWidth, targetHeight);
const drawableSource =
previewCanvas && previewCanvas.width > 0 && previewCanvas.height > 0
? previewCanvas
: previewVideo && previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0
? previewVideo
previewVideo && previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0
? previewVideo
: previewCanvas && previewCanvas.width > 0 && previewCanvas.height > 0
? previewCanvas
: null;
if (!drawableSource) {
@@ -1559,30 +1560,6 @@ export default function VideoEditor() {
}
}, [saveProject]);
const handleLoadProject = useCallback(async () => {
const result = await window.electronAPI.loadProjectFile();
if (result.canceled) {
return;
}
if (!result.success) {
toast.error(result.message || "Failed to load project");
return;
}
const restored = await applyLoadedProject(result.project, result.path ?? null);
if (!restored) {
toast.error("Invalid project file format");
return;
}
setProjectBrowserOpen(false);
await refreshProjectLibrary();
toast.success(`Project loaded from ${result.path}`);
}, [applyLoadedProject, refreshProjectLibrary]);
const handleOpenProjectFromLibrary = useCallback(
async (projectPath: string) => {
const result = await window.electronAPI.openProjectFileAtPath(projectPath);
@@ -1609,20 +1586,15 @@ export default function VideoEditor() {
[applyLoadedProject, refreshProjectLibrary],
);
const handleOpenProjectsFolder = useCallback(async () => {
const result = await window.electronAPI.openProjectsDirectory();
if (!result.success) {
toast.error(result.message || result.error || "Failed to open projects folder");
}
}, []);
const handleOpenProjectBrowser = useCallback(async () => {
await refreshProjectLibrary();
setProjectBrowserOpen(true);
}, [refreshProjectLibrary]);
useEffect(() => {
const removeLoadListener = window.electronAPI.onMenuLoadProject(handleLoadProject);
const removeLoadListener = window.electronAPI.onMenuLoadProject(() => {
void handleOpenProjectBrowser();
});
const removeSaveListener = window.electronAPI.onMenuSaveProject(handleSaveProject);
const removeSaveAsListener = window.electronAPI.onMenuSaveProjectAs(handleSaveProjectAs);
@@ -1631,7 +1603,7 @@ export default function VideoEditor() {
removeSaveListener?.();
removeSaveAsListener?.();
};
}, [handleLoadProject, handleSaveProject, handleSaveProjectAs]);
}, [handleOpenProjectBrowser, handleSaveProject, handleSaveProjectAs]);
useEffect(() => {
let mounted = true;
@@ -3418,19 +3390,9 @@ export default function VideoEditor() {
open={projectBrowserOpen}
onOpenChange={setProjectBrowserOpen}
entries={projectLibraryEntries}
projectsDirectoryPath={projectsDirectoryPath}
onOpenProject={(projectPath) => {
void handleOpenProjectFromLibrary(projectPath);
}}
onBrowseProjectFiles={() => {
void handleLoadProject();
}}
onOpenProjectsFolder={() => {
void handleOpenProjectsFolder();
}}
onSaveProjectAs={() => {
void handleSaveProjectAs();
}}
/>
<Toaster theme="dark" className="pointer-events-auto" />