File size: 8,491 Bytes
3d7d9b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | import { Plus, FolderOpen, Layout, Layers, Settings, Trash2, Minus, Square, X } from 'lucide-react';
import { useAppStore } from '../store';
import { useRef, useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
interface ProjectEntry { id: string; title: string; element_count: number; saved_at: number; }
export const StarterHub = () => {
const { setCurrentScreen, setIsSettingsOpen, setImages, setTextNotes, setAnnotations, setPalettes, setZoom, setPan, setActiveProjectId, setBoardTitle } = useAppStore();
const [projects, setProjects] = useState<ProjectEntry[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);
const loadProjects = () => invoke<ProjectEntry[]>('projects_list').then(setProjects).catch(() => {});
useEffect(() => { loadProjects(); }, []);
const handleOpenFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const d = JSON.parse(ev.target?.result as string);
if (d.images) setImages(d.images);
if (d.textNotes) setTextNotes(d.textNotes);
if (d.annotations) setAnnotations(d.annotations);
if (d.palettes) setPalettes(d.palettes);
if (d.zoom) setZoom(d.zoom);
if (d.pan) setPan(d.pan);
if (d.title) setBoardTitle(d.title);
setCurrentScreen('board');
} catch {}
};
reader.readAsText(file);
if (e.target) e.target.value = '';
};
const handleNewBoard = async () => {
try {
const entry = await invoke<any>('project_create', { title: null });
setActiveProjectId(entry.id);
setBoardTitle(entry.title);
} catch {}
setImages([]); setTextNotes([]); setAnnotations([]); setPalettes([]);
setZoom(1); setPan({ x: 0, y: 0 });
setCurrentScreen('board');
};
const handleOpenProject = async (project: ProjectEntry) => {
try {
const json = await invoke<string>('project_load', { id: project.id });
const d = JSON.parse(json);
setImages(d.images || []); setTextNotes(d.textNotes || []); setAnnotations(d.annotations || []); setPalettes(d.palettes || []);
setZoom(d.zoom || 1); setPan(d.pan || { x: 0, y: 0 });
setBoardTitle(d.title || project.title);
setActiveProjectId(project.id);
} catch {
setImages([]); setTextNotes([]); setAnnotations([]); setPalettes([]); setZoom(1); setPan({ x: 0, y: 0 });
setBoardTitle(project.title); setActiveProjectId(project.id);
}
setCurrentScreen('board');
};
const handleDeleteProject = async (id: string, e: React.MouseEvent) => {
e.stopPropagation();
try { await invoke('project_delete', { id }); } catch {}
loadProjects();
};
const timeAgo = (ts: number) => {
const diff = Math.floor(Date.now() / 1000) - ts;
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
return new Date(ts * 1000).toLocaleDateString();
};
return (
<div className="w-screen h-screen bg-[#0A0A0B] flex flex-col font-sans text-white select-none overflow-hidden">
{/* Top bar */}
<div className="w-full h-12 flex items-center justify-between px-5 bg-[#0A0A0B] shrink-0 z-10 border-b border-white/[0.04]" data-tauri-drag-region>
{/* Left: Logo */}
<div className="flex items-center gap-2.5">
<div className="w-[22px] h-[22px] bg-white rounded-[5px] flex items-center justify-center text-black">
<Layout size={11} />
</div>
<span className="font-semibold text-[12px] text-white/80 tracking-tight">Refstudio</span>
</div>
{/* Right: Settings + Window controls */}
<div className="flex items-center gap-1">
<button onClick={() => setIsSettingsOpen(true)} className="w-7 h-7 rounded-md flex items-center justify-center text-white/30 hover:text-white/70 hover:bg-white/5 transition-colors" title="Settings">
<Settings size={14} />
</button>
<div className="w-px h-4 bg-white/[0.06] mx-1" />
<button onClick={() => appWindow.minimize()} className="w-7 h-7 rounded-md flex items-center justify-center text-white/30 hover:text-white/70 hover:bg-white/5 transition-colors">
<Minus size={14} />
</button>
<button onClick={() => appWindow.toggleMaximize()} className="w-7 h-7 rounded-md flex items-center justify-center text-white/30 hover:text-white/70 hover:bg-white/5 transition-colors">
<Square size={11} />
</button>
<button onClick={() => appWindow.close()} className="w-7 h-7 rounded-md flex items-center justify-center text-white/30 hover:text-[#FF453A] hover:bg-[#FF453A]/10 transition-colors">
<X size={14} />
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 flex flex-col items-center pt-14 pb-12 px-8 overflow-y-auto">
{/* Action buttons */}
<div className="flex gap-3 mb-10 w-full max-w-md">
<button onClick={handleNewBoard} className="flex-1 h-[72px] rounded-xl bg-[#141415] border border-white/[0.06] hover:border-[#0A84FF]/30 flex items-center gap-3 px-5 transition-all group">
<div className="w-8 h-8 rounded-full bg-white/[0.04] group-hover:bg-[#0A84FF] flex items-center justify-center text-white/50 group-hover:text-white transition-colors shrink-0">
<Plus size={15} />
</div>
<span className="text-[13px] text-white/70 font-medium">New Board</span>
</button>
<button onClick={() => fileInputRef.current?.click()} className="flex-1 h-[72px] rounded-xl bg-[#141415] border border-white/[0.06] hover:border-white/15 flex items-center gap-3 px-5 transition-all group">
<div className="w-8 h-8 rounded-full bg-white/[0.04] group-hover:bg-white group-hover:text-black flex items-center justify-center text-white/50 transition-colors shrink-0">
<FolderOpen size={15} />
</div>
<span className="text-[13px] text-white/70 font-medium">Open File</span>
</button>
<input type="file" accept=".json" className="hidden" ref={fileInputRef} onChange={handleOpenFile} />
</div>
{/* Recent projects */}
<div className="w-full max-w-md">
<div className="flex items-center justify-between mb-3 px-1">
<span className="text-[11px] font-medium text-white/25 uppercase tracking-wider">Recent</span>
</div>
{projects.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-white/20">
<Layers size={22} className="mb-2.5 opacity-50" />
<p className="text-[12px] font-medium">No projects yet</p>
</div>
) : (
<div className="flex flex-col gap-1">
{projects.map(p => (
<button key={p.id} onClick={() => handleOpenProject(p)} className="group w-full flex items-center gap-3 px-3.5 py-3 rounded-lg hover:bg-white/[0.03] transition-colors text-left">
<div className="w-8 h-8 rounded-lg bg-white/[0.04] flex items-center justify-center text-white/20 shrink-0">
<Layers size={13} />
</div>
<div className="flex-1 min-w-0">
<div className="text-[13px] font-medium text-white/80 truncate">{p.title}</div>
<div className="text-[10px] text-white/25 mt-0.5">{p.element_count} elements · {timeAgo(p.saved_at)}</div>
</div>
<button onClick={(e) => handleDeleteProject(p.id, e)} className="w-6 h-6 rounded flex items-center justify-center text-white/10 hover:text-[#FF453A] hover:bg-[#FF453A]/10 opacity-0 group-hover:opacity-100 transition-all shrink-0">
<Trash2 size={12} />
</button>
</button>
))}
</div>
)}
</div>
</div>
</div>
);
};
|