mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-27 16:25:35 +00:00
refactor(projects): simplify browser modal and thumbnail capture
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user