| 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> | |
| ); | |
| }; | |