feat: fully functional Storage settings - live usage stats, clear library/projects, reveal folder, export .refs"
Browse files- src/components/SettingsPanel.tsx +63 -114
src/components/SettingsPanel.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import type React from 'react';
|
| 2 |
-
import { X, Settings, Keyboard, Monitor, Shield, HardDrive, Info, RefreshCw, KeyRound, Lock, Unlock, Trash2, Plus, Navigation, Grid3X3, Database, Palette, Clock, MousePointer2 } from 'lucide-react';
|
| 3 |
import { useAppStore } from '../store';
|
| 4 |
import { useState, useEffect } from 'react';
|
| 5 |
import { invoke } from '@tauri-apps/api/core';
|
|
@@ -7,9 +7,12 @@ import { unlockVault, lockVault, isVaultUnlocked, saveCredential, listCredential
|
|
| 7 |
|
| 8 |
type Tab = 'general' | 'appearance' | 'shortcuts' | 'privacy' | 'vault' | 'storage' | 'about';
|
| 9 |
interface ShieldReport { blocked_requests: number; blocked_cosmetic: number; https_upgrades: number; engine_rules: number; }
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
export const SettingsPanel = () => {
|
| 12 |
-
const { isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, setBgOpacity, showMinimap, setShowMinimap, showGrid, setShowGrid } = useAppStore();
|
| 13 |
const [activeTab, setActiveTab] = useState<Tab>('general');
|
| 14 |
const [shieldReport, setShieldReport] = useState<ShieldReport>({ blocked_requests: 0, blocked_cosmetic: 0, https_upgrades: 0, engine_rules: 0 });
|
| 15 |
const [vaultUnlocked, setVaultUnlocked] = useState(isVaultUnlocked());
|
|
@@ -19,9 +22,14 @@ export const SettingsPanel = () => {
|
|
| 19 |
const [username, setUsername] = useState('');
|
| 20 |
const [password, setPassword] = useState('');
|
| 21 |
const [vaultMsg, setVaultMsg] = useState('');
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
useEffect(() => { if (isSettingsOpen) invoke<ShieldReport>('shield_get_report').then(setShieldReport).catch(() => {}); }, [isSettingsOpen]);
|
|
|
|
| 24 |
const reloadCreds = () => listCredentials().then(setCreds).catch(e => setVaultMsg(String(e)));
|
|
|
|
| 25 |
|
| 26 |
const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
| 27 |
{ id: 'general', label: 'General', icon: <Settings size={16} /> },
|
|
@@ -36,123 +44,64 @@ export const SettingsPanel = () => {
|
|
| 36 |
const unlock = async () => { try { await unlockVault(master); setMaster(''); setVaultUnlocked(true); setVaultMsg('Vault unlocked'); await reloadCreds(); } catch (e) { setVaultMsg(`Unlock failed: ${String(e)}`); } };
|
| 37 |
const saveCred = async () => { try { await saveCredential({ origin, username, password }); setOrigin(''); setUsername(''); setPassword(''); setVaultMsg('Credential saved'); await reloadCreds(); } catch (e) { setVaultMsg(`Save failed: ${String(e)}`); } };
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
return (
|
| 40 |
<div className={`absolute right-4 top-4 bottom-4 w-[min(720px,calc(100vw-32px))] z-[80] bg-[#1C1C1E]/98 shadow-2xl border border-[#3A3A3E] rounded-2xl flex flex-col transform transition-transform duration-500 ease-[cubic-bezier(0.19,1,0.22,1)] overflow-hidden ${isSettingsOpen ? 'translate-x-0' : 'translate-x-[calc(100%+32px)]'}`}>
|
| 41 |
-
<div className="h-14 border-b border-white/5 flex items-center justify-between px-5 bg-black/20 shrink-0">
|
| 42 |
-
<h2 className="text-[#E0E0E0] font-medium flex items-center gap-2"><Settings size={16} className="text-[#808080]" /> Settings</h2>
|
| 43 |
-
<button onClick={() => setIsSettingsOpen(false)} className="text-[#808080] hover:text-[#E0E0E0] p-1 rounded-md hover:bg-white/5"><X size={18} /></button>
|
| 44 |
-
</div>
|
| 45 |
-
|
| 46 |
<div className="flex flex-1 min-h-0 overflow-hidden">
|
| 47 |
-
<nav className="w-[190px] bg-black/10 border-r border-white/5 p-3 flex flex-col gap-1 shrink-0 overflow-y-auto custom-scrollbar">
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
))}
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
<main className="flex-1 min-w-0 overflow-y-auto custom-scrollbar">
|
| 56 |
-
<div className="p-6 pb-10 text-[#E0E0E0] text-sm max-w-[520px] mx-auto">
|
| 57 |
-
{activeTab === 'general' && <Page title="General" subtitle="Core behavior for the canvas, window, and workspace.">
|
| 58 |
-
<SettingsGroup title="Startup & Window">
|
| 59 |
-
<SettingRow label="Open last board on startup" description="Load the most recent project automatically when Refstudio starts." active={true} onToggle={() => {}} disabled />
|
| 60 |
-
<SettingRow label="Always on Top" description="Keep Refstudio above other apps while drawing in Photoshop, Krita, Blender, or ZBrush." active={isAlwaysOnTop} onToggle={() => setIsAlwaysOnTop(!isAlwaysOnTop)} />
|
| 61 |
-
{isAlwaysOnTop && <RangeRow label="Overlay opacity" description="Adjust transparency while Refstudio is pinned above another app." value={bgOpacity} min={10} max={100} suffix="%" onChange={setBgOpacity} />}
|
| 62 |
-
</SettingsGroup>
|
| 63 |
-
<SettingsGroup title="Canvas">
|
| 64 |
-
<SettingRow icon={<Grid3X3 size={16} />} label="Grid" description="Show the subtle dotted canvas grid. This is intentionally kept in Settings, not the floating toolbar." active={showGrid} onToggle={() => setShowGrid(!showGrid)} />
|
| 65 |
-
<SettingRow icon={<Navigation size={16} />} label="Navigator / Minimap" description="Show a compact board overview in the bottom-right corner. Off by default to keep the canvas clean." active={showMinimap} onToggle={() => setShowMinimap(!showMinimap)} />
|
| 66 |
-
</SettingsGroup>
|
| 67 |
-
</Page>}
|
| 68 |
-
|
| 69 |
-
{activeTab === 'appearance' && <Page title="Appearance" subtitle="Visual defaults for artist-friendly, low-noise reference work.">
|
| 70 |
-
<SettingsGroup title="Theme">
|
| 71 |
-
<div className="p-4 grid grid-cols-2 gap-3">
|
| 72 |
-
<ThemeCard active name="Dark Canvas" colors={['#1C1C1E', '#2A2A2E', '#3A3A3E', '#0A84FF']} />
|
| 73 |
-
<ThemeCard name="Warm Studio" colors={['#221F1B', '#342D26', '#6B5A45', '#D4A373']} disabled />
|
| 74 |
-
</div>
|
| 75 |
-
</SettingsGroup>
|
| 76 |
-
<SettingsGroup title="Canvas Display">
|
| 77 |
-
<InfoRow icon={<Palette size={16} />} label="Canvas background" value="#1C1C1E" description="Neutral dark background from the Refstudio SRS visual language." />
|
| 78 |
-
<InfoRow icon={<MousePointer2 size={16} />} label="Interface behavior" value="Auto-hide chrome" description="The toolbar remains hidden until you move toward it, keeping the board dominant." />
|
| 79 |
-
</SettingsGroup>
|
| 80 |
-
</Page>}
|
| 81 |
-
|
| 82 |
-
{activeTab === 'shortcuts' && <Page title="Keyboard Shortcuts" subtitle="Hotkey-first controls for one-hand artist workflow.">
|
| 83 |
-
<SettingsGroup title="Panels">
|
| 84 |
-
<ShortcutRow combo="B" label="Toggle Browser Panel" />
|
| 85 |
-
<ShortcutRow combo="L" label="Toggle Library Panel" />
|
| 86 |
-
<ShortcutRow combo="Esc" label="Close panels / clear selection" />
|
| 87 |
-
<ShortcutRow combo="Ctrl + ," label="Open Settings" />
|
| 88 |
-
</SettingsGroup>
|
| 89 |
-
<SettingsGroup title="Canvas">
|
| 90 |
-
<ShortcutRow combo="Space + Drag" label="Pan canvas" />
|
| 91 |
-
<ShortcutRow combo="Scroll" label="Zoom to cursor" />
|
| 92 |
-
<ShortcutRow combo="Ctrl + 0" label="Fit all images" />
|
| 93 |
-
<ShortcutRow combo="Ctrl + 1" label="100% zoom" />
|
| 94 |
-
<ShortcutRow combo="A" label="Toggle annotation mode" />
|
| 95 |
-
<ShortcutRow combo="D" label="Desaturate selection" />
|
| 96 |
-
<ShortcutRow combo="Shift + D" label="Desaturate all" />
|
| 97 |
-
<ShortcutRow combo="Delete" label="Delete selection" />
|
| 98 |
-
</SettingsGroup>
|
| 99 |
-
<SettingsGroup title="Editing">
|
| 100 |
-
<ShortcutRow combo="Ctrl + Z" label="Undo" />
|
| 101 |
-
<ShortcutRow combo="Ctrl + Shift + Z" label="Redo" />
|
| 102 |
-
<ShortcutRow combo="Ctrl + A" label="Select all images" />
|
| 103 |
-
<ShortcutRow combo="Ctrl + G" label="Group selection" />
|
| 104 |
-
<ShortcutRow combo="Ctrl + Shift + G" label="Ungroup selection" />
|
| 105 |
-
</SettingsGroup>
|
| 106 |
-
</Page>}
|
| 107 |
-
|
| 108 |
-
{activeTab === 'privacy' && <Page title="Privacy" subtitle="Local-first browsing and capture controls.">
|
| 109 |
-
<SettingsGroup title="Muse Shield">
|
| 110 |
-
<div className="p-4 grid grid-cols-2 gap-3">
|
| 111 |
-
<StatBox value={shieldReport.engine_rules.toLocaleString()} label="Rules" />
|
| 112 |
-
<StatBox value={shieldReport.blocked_requests.toLocaleString()} label="Requests blocked" />
|
| 113 |
-
<StatBox value={shieldReport.blocked_cosmetic.toLocaleString()} label="Elements hidden" />
|
| 114 |
-
<StatBox value={shieldReport.https_upgrades.toLocaleString()} label="HTTPS upgrades" />
|
| 115 |
-
</div>
|
| 116 |
-
<div className="px-4 pb-4"><button onClick={() => invoke('shield_update_lists').catch(() => {})} className="w-full py-2.5 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-sm flex items-center justify-center gap-2"><RefreshCw size={14} /> Update filters</button></div>
|
| 117 |
-
</SettingsGroup>
|
| 118 |
-
<SettingsGroup title="Local Data">
|
| 119 |
-
<InfoRow icon={<Shield size={16} />} label="Privacy model" value="Local-first" description="Boards, library metadata, and browsing history stay on your machine." />
|
| 120 |
-
<ActionRow label="App navigation history" description="Clear Refstudio's app-managed URL history. This does not inspect system WebView cookies." actionLabel="Clear History" danger onClick={() => invoke('history_clear').catch(() => {})} />
|
| 121 |
-
</SettingsGroup>
|
| 122 |
-
</Page>}
|
| 123 |
-
|
| 124 |
-
{activeTab === 'vault' && <Page title="Password Vault" subtitle="Stronghold-backed local credential storage.">
|
| 125 |
-
<SettingsGroup title="Vault Status">
|
| 126 |
-
<div className="p-4 space-y-3">
|
| 127 |
-
<p className="text-[#909090] text-xs leading-relaxed">Passwords are stored using the Tauri Stronghold frontend plugin. The vault unlocks only with your master password.</p>
|
| 128 |
-
{!vaultUnlocked ? <div className="flex gap-2"><input type="password" value={master} onChange={e => setMaster(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') unlock(); }} placeholder="Master password" className="flex-1 bg-black/30 border border-white/10 rounded-xl px-3 py-2 outline-none focus:border-[#0A84FF]" /><button onClick={unlock} className="px-4 py-2 bg-[#0A84FF] rounded-xl text-white flex items-center gap-2"><Unlock size={14} /> Unlock</button></div> : <button onClick={() => { lockVault(); setVaultUnlocked(false); setCreds([]); }} className="px-4 py-2 bg-white/5 border border-white/10 rounded-xl text-white flex items-center gap-2"><Lock size={14} /> Lock vault</button>}
|
| 129 |
-
{vaultMsg && <div className="text-xs text-[#FFD60A]">{vaultMsg}</div>}
|
| 130 |
-
</div>
|
| 131 |
-
</SettingsGroup>
|
| 132 |
-
{vaultUnlocked && <SettingsGroup title="Credentials">
|
| 133 |
-
<div className="p-4 space-y-2">
|
| 134 |
-
<input value={origin} onChange={e => setOrigin(e.target.value)} placeholder="Site / origin (example.com)" className="w-full bg-black/30 border border-white/10 rounded-xl px-3 py-2 outline-none focus:border-[#0A84FF]" />
|
| 135 |
-
<input value={username} onChange={e => setUsername(e.target.value)} placeholder="Username / email" className="w-full bg-black/30 border border-white/10 rounded-xl px-3 py-2 outline-none focus:border-[#0A84FF]" />
|
| 136 |
-
<input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" className="w-full bg-black/30 border border-white/10 rounded-xl px-3 py-2 outline-none focus:border-[#0A84FF]" />
|
| 137 |
-
<button onClick={saveCred} className="w-full px-3 py-2.5 bg-[#0A84FF] rounded-xl text-white flex items-center justify-center gap-2"><Plus size={14} /> Save credential</button>
|
| 138 |
-
</div>
|
| 139 |
-
<div className="px-4 pb-4 space-y-2">{creds.map(c => <div key={c.id} className="flex items-center justify-between bg-black/20 border border-white/5 rounded-xl p-3"><div><div className="text-white text-sm">{c.username}</div><div className="text-[#808080] text-xs">{c.origin}</div></div><button onClick={() => deleteCredential(c.id).then(reloadCreds)} className="text-red-300 hover:bg-red-500/10 rounded-lg p-2"><Trash2 size={14} /></button></div>)}</div>
|
| 140 |
-
</SettingsGroup>}
|
| 141 |
-
</Page>}
|
| 142 |
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
<
|
| 147 |
-
<
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
</Page>}
|
| 154 |
-
</div>
|
| 155 |
-
</main>
|
| 156 |
</div>
|
| 157 |
</div>
|
| 158 |
);
|
|
|
|
| 1 |
import type React from 'react';
|
| 2 |
+
import { X, Settings, Keyboard, Monitor, Shield, HardDrive, Info, RefreshCw, KeyRound, Lock, Unlock, Trash2, Plus, Navigation, Grid3X3, Database, Palette, Clock, MousePointer2, FolderOpen, FileDown, Image as ImageIcon, LayoutGrid } from 'lucide-react';
|
| 3 |
import { useAppStore } from '../store';
|
| 4 |
import { useState, useEffect } from 'react';
|
| 5 |
import { invoke } from '@tauri-apps/api/core';
|
|
|
|
| 7 |
|
| 8 |
type Tab = 'general' | 'appearance' | 'shortcuts' | 'privacy' | 'vault' | 'storage' | 'about';
|
| 9 |
interface ShieldReport { blocked_requests: number; blocked_cosmetic: number; https_upgrades: number; engine_rules: number; }
|
| 10 |
+
interface StorageInfo { data_path: string; total_size_bytes: number; file_count: number; project_count: number; library_count: number; library_size_bytes: number; projects_size_bytes: number; }
|
| 11 |
+
|
| 12 |
+
function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(i > 0 ? 1 : 0) + ' ' + sizes[i]; }
|
| 13 |
|
| 14 |
export const SettingsPanel = () => {
|
| 15 |
+
const { isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, setBgOpacity, showMinimap, setShowMinimap, showGrid, setShowGrid, images, textNotes, annotations, palettes, zoom, pan, boardTitle } = useAppStore();
|
| 16 |
const [activeTab, setActiveTab] = useState<Tab>('general');
|
| 17 |
const [shieldReport, setShieldReport] = useState<ShieldReport>({ blocked_requests: 0, blocked_cosmetic: 0, https_upgrades: 0, engine_rules: 0 });
|
| 18 |
const [vaultUnlocked, setVaultUnlocked] = useState(isVaultUnlocked());
|
|
|
|
| 22 |
const [username, setUsername] = useState('');
|
| 23 |
const [password, setPassword] = useState('');
|
| 24 |
const [vaultMsg, setVaultMsg] = useState('');
|
| 25 |
+
const [storageInfo, setStorageInfo] = useState<StorageInfo | null>(null);
|
| 26 |
+
const [storageLoading, setStorageLoading] = useState(false);
|
| 27 |
+
const [storageMsg, setStorageMsg] = useState('');
|
| 28 |
|
| 29 |
useEffect(() => { if (isSettingsOpen) invoke<ShieldReport>('shield_get_report').then(setShieldReport).catch(() => {}); }, [isSettingsOpen]);
|
| 30 |
+
useEffect(() => { if (isSettingsOpen && activeTab === 'storage') loadStorageInfo(); }, [isSettingsOpen, activeTab]);
|
| 31 |
const reloadCreds = () => listCredentials().then(setCreds).catch(e => setVaultMsg(String(e)));
|
| 32 |
+
const loadStorageInfo = () => { setStorageLoading(true); invoke<StorageInfo>('storage_info').then(setStorageInfo).catch(() => {}).finally(() => setStorageLoading(false)); };
|
| 33 |
|
| 34 |
const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
| 35 |
{ id: 'general', label: 'General', icon: <Settings size={16} /> },
|
|
|
|
| 44 |
const unlock = async () => { try { await unlockVault(master); setMaster(''); setVaultUnlocked(true); setVaultMsg('Vault unlocked'); await reloadCreds(); } catch (e) { setVaultMsg(`Unlock failed: ${String(e)}`); } };
|
| 45 |
const saveCred = async () => { try { await saveCredential({ origin, username, password }); setOrigin(''); setUsername(''); setPassword(''); setVaultMsg('Credential saved'); await reloadCreds(); } catch (e) { setVaultMsg(`Save failed: ${String(e)}`); } };
|
| 46 |
|
| 47 |
+
const handleClearLibrary = async () => { if (!confirm('Clear all images from the asset library? This cannot be undone.')) return; try { await invoke('storage_clear_library'); setStorageMsg('Library cleared'); loadStorageInfo(); } catch (e) { setStorageMsg(`Failed: ${e}`); } };
|
| 48 |
+
const handleClearProjects = async () => { if (!confirm('Delete ALL saved projects? This cannot be undone. The current board will remain in memory but will not be recoverable after closing.')) return; try { await invoke('storage_clear_projects'); setStorageMsg('Projects cleared'); loadStorageInfo(); } catch (e) { setStorageMsg(`Failed: ${e}`); } };
|
| 49 |
+
const handleRevealFolder = () => invoke('storage_reveal_folder').catch(e => setStorageMsg(`Failed: ${e}`));
|
| 50 |
+
const handleExportRefs = async () => {
|
| 51 |
+
const state = JSON.stringify({ textNotes: [], images, annotations: [], palettes: [], zoom, pan, title: boardTitle });
|
| 52 |
+
try {
|
| 53 |
+
const bytes = await invoke<number[]>('refs_export', { stateJson: state });
|
| 54 |
+
const blob = new Blob([new Uint8Array(bytes)], { type: 'application/octet-stream' });
|
| 55 |
+
const url = URL.createObjectURL(blob);
|
| 56 |
+
const a = document.createElement('a'); a.href = url; a.download = `${boardTitle.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() || 'board'}.refs`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
|
| 57 |
+
setStorageMsg('Board exported as .refs file');
|
| 58 |
+
} catch (e) { setStorageMsg(`Export failed: ${e}`); }
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
return (
|
| 62 |
<div className={`absolute right-4 top-4 bottom-4 w-[min(720px,calc(100vw-32px))] z-[80] bg-[#1C1C1E]/98 shadow-2xl border border-[#3A3A3E] rounded-2xl flex flex-col transform transition-transform duration-500 ease-[cubic-bezier(0.19,1,0.22,1)] overflow-hidden ${isSettingsOpen ? 'translate-x-0' : 'translate-x-[calc(100%+32px)]'}`}>
|
| 63 |
+
<div className="h-14 border-b border-white/5 flex items-center justify-between px-5 bg-black/20 shrink-0"><h2 className="text-[#E0E0E0] font-medium flex items-center gap-2"><Settings size={16} className="text-[#808080]" /> Settings</h2><button onClick={() => setIsSettingsOpen(false)} className="text-[#808080] hover:text-[#E0E0E0] p-1 rounded-md hover:bg-white/5"><X size={18} /></button></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
<div className="flex flex-1 min-h-0 overflow-hidden">
|
| 65 |
+
<nav className="w-[190px] bg-black/10 border-r border-white/5 p-3 flex flex-col gap-1 shrink-0 overflow-y-auto custom-scrollbar">{tabs.map(tab => <button key={tab.id} onClick={() => setActiveTab(tab.id)} className={`flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-[13px] text-left transition-colors ${activeTab === tab.id ? 'bg-[#0A84FF] text-white font-medium shadow-lg shadow-[#0A84FF]/10' : 'text-[#808080] hover:text-[#E0E0E0] hover:bg-white/5'}`}>{tab.icon}{tab.label}</button>)}</nav>
|
| 66 |
+
<main className="flex-1 min-w-0 overflow-y-auto custom-scrollbar"><div className="p-6 pb-10 text-[#E0E0E0] text-sm max-w-[520px] mx-auto">
|
| 67 |
+
{activeTab === 'general' && <Page title="General" subtitle="Core behavior for the canvas, window, and workspace."><SettingsGroup title="Startup & Window"><SettingRow label="Open last board on startup" description="Load the most recent project automatically when Refstudio starts." active={true} onToggle={() => {}} disabled /><SettingRow label="Always on Top" description="Keep Refstudio above other apps while drawing in Photoshop, Krita, Blender, or ZBrush." active={isAlwaysOnTop} onToggle={() => setIsAlwaysOnTop(!isAlwaysOnTop)} />{isAlwaysOnTop && <RangeRow label="Overlay opacity" description="Adjust transparency while Refstudio is pinned above another app." value={bgOpacity} min={10} max={100} suffix="%" onChange={setBgOpacity} />}</SettingsGroup><SettingsGroup title="Canvas"><SettingRow icon={<Grid3X3 size={16} />} label="Grid" description="Show the subtle dotted canvas grid. This is intentionally kept in Settings, not the floating toolbar." active={showGrid} onToggle={() => setShowGrid(!showGrid)} /><SettingRow icon={<Navigation size={16} />} label="Navigator / Minimap" description="Show a compact board overview in the bottom-right corner. Off by default to keep the canvas clean." active={showMinimap} onToggle={() => setShowMinimap(!showMinimap)} /></SettingsGroup></Page>}
|
| 68 |
+
{activeTab === 'appearance' && <Page title="Appearance" subtitle="Visual defaults for artist-friendly, low-noise reference work."><SettingsGroup title="Theme"><div className="p-4 grid grid-cols-2 gap-3"><ThemeCard active name="Dark Canvas" colors={['#1C1C1E', '#2A2A2E', '#3A3A3E', '#0A84FF']} /><ThemeCard name="Warm Studio" colors={['#221F1B', '#342D26', '#6B5A45', '#D4A373']} disabled /></div></SettingsGroup><SettingsGroup title="Canvas Display"><InfoRow icon={<Palette size={16} />} label="Canvas background" value="#1C1C1E" description="Neutral dark background from the Refstudio SRS visual language." /><InfoRow icon={<MousePointer2 size={16} />} label="Interface behavior" value="Auto-hide chrome" description="The toolbar remains hidden until you move toward it, keeping the board dominant." /></SettingsGroup></Page>}
|
| 69 |
+
{activeTab === 'shortcuts' && <Page title="Keyboard Shortcuts" subtitle="Hotkey-first controls for one-hand artist workflow."><SettingsGroup title="Panels"><ShortcutRow combo="B" label="Toggle Browser Panel" /><ShortcutRow combo="L" label="Toggle Library Panel" /><ShortcutRow combo="Esc" label="Close panels / clear selection" /><ShortcutRow combo="Ctrl + ," label="Open Settings" /></SettingsGroup><SettingsGroup title="Canvas"><ShortcutRow combo="Space + Drag" label="Pan canvas" /><ShortcutRow combo="Scroll" label="Zoom to cursor" /><ShortcutRow combo="Ctrl + 0" label="Fit all images" /><ShortcutRow combo="Ctrl + 1" label="100% zoom" /><ShortcutRow combo="A" label="Toggle annotation mode" /><ShortcutRow combo="D" label="Desaturate selection" /><ShortcutRow combo="Shift + D" label="Desaturate all" /><ShortcutRow combo="Delete" label="Delete selection" /></SettingsGroup><SettingsGroup title="Editing"><ShortcutRow combo="Ctrl + Z" label="Undo" /><ShortcutRow combo="Ctrl + Shift + Z" label="Redo" /><ShortcutRow combo="Ctrl + A" label="Select all images" /><ShortcutRow combo="Ctrl + G" label="Group selection" /><ShortcutRow combo="Ctrl + Shift + G" label="Ungroup selection" /></SettingsGroup></Page>}
|
| 70 |
+
{activeTab === 'privacy' && <Page title="Privacy" subtitle="Local-first browsing and capture controls."><SettingsGroup title="Muse Shield"><div className="p-4 grid grid-cols-2 gap-3"><StatBox value={shieldReport.engine_rules.toLocaleString()} label="Rules" /><StatBox value={shieldReport.blocked_requests.toLocaleString()} label="Requests blocked" /><StatBox value={shieldReport.blocked_cosmetic.toLocaleString()} label="Elements hidden" /><StatBox value={shieldReport.https_upgrades.toLocaleString()} label="HTTPS upgrades" /></div><div className="px-4 pb-4"><button onClick={() => invoke('shield_update_lists').catch(() => {})} className="w-full py-2.5 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-sm flex items-center justify-center gap-2"><RefreshCw size={14} /> Update filters</button></div></SettingsGroup><SettingsGroup title="Local Data"><InfoRow icon={<Shield size={16} />} label="Privacy model" value="Local-first" description="Boards, library metadata, and browsing history stay on your machine." /><ActionRow label="App navigation history" description="Clear Refstudio's app-managed URL history." actionLabel="Clear History" danger onClick={() => invoke('history_clear').catch(() => {})} /></SettingsGroup></Page>}
|
| 71 |
+
{activeTab === 'vault' && <Page title="Password Vault" subtitle="Stronghold-backed local credential storage."><SettingsGroup title="Vault Status"><div className="p-4 space-y-3"><p className="text-[#909090] text-xs leading-relaxed">Passwords are stored using the Tauri Stronghold frontend plugin. The vault unlocks only with your master password.</p>{!vaultUnlocked ? <div className="flex gap-2"><input type="password" value={master} onChange={e => setMaster(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') unlock(); }} placeholder="Master password" className="flex-1 bg-black/30 border border-white/10 rounded-xl px-3 py-2 outline-none focus:border-[#0A84FF]" /><button onClick={unlock} className="px-4 py-2 bg-[#0A84FF] rounded-xl text-white flex items-center gap-2"><Unlock size={14} /> Unlock</button></div> : <button onClick={() => { lockVault(); setVaultUnlocked(false); setCreds([]); }} className="px-4 py-2 bg-white/5 border border-white/10 rounded-xl text-white flex items-center gap-2"><Lock size={14} /> Lock vault</button>}{vaultMsg && <div className="text-xs text-[#FFD60A]">{vaultMsg}</div>}</div></SettingsGroup>{vaultUnlocked && <SettingsGroup title="Credentials"><div className="p-4 space-y-2"><input value={origin} onChange={e => setOrigin(e.target.value)} placeholder="Site / origin (example.com)" className="w-full bg-black/30 border border-white/10 rounded-xl px-3 py-2 outline-none focus:border-[#0A84FF]" /><input value={username} onChange={e => setUsername(e.target.value)} placeholder="Username / email" className="w-full bg-black/30 border border-white/10 rounded-xl px-3 py-2 outline-none focus:border-[#0A84FF]" /><input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" className="w-full bg-black/30 border border-white/10 rounded-xl px-3 py-2 outline-none focus:border-[#0A84FF]" /><button onClick={saveCred} className="w-full px-3 py-2.5 bg-[#0A84FF] rounded-xl text-white flex items-center justify-center gap-2"><Plus size={14} /> Save credential</button></div><div className="px-4 pb-4 space-y-2">{creds.map(c => <div key={c.id} className="flex items-center justify-between bg-black/20 border border-white/5 rounded-xl p-3"><div><div className="text-white text-sm">{c.username}</div><div className="text-[#808080] text-xs">{c.origin}</div></div><button onClick={() => deleteCredential(c.id).then(reloadCreds)} className="text-red-300 hover:bg-red-500/10 rounded-lg p-2"><Trash2 size={14} /></button></div>)}</div></SettingsGroup>}</Page>}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
+
{activeTab === 'storage' && <Page title="Storage" subtitle="Manage local data, export boards, and inspect disk usage.">
|
| 74 |
+
<SettingsGroup title="Usage">
|
| 75 |
+
{storageLoading ? <div className="px-4 py-6 text-center text-[#808080]"><RefreshCw size={16} className="animate-spin mx-auto mb-2" />Loading...</div> : storageInfo ? <div className="p-4 grid grid-cols-2 gap-3">
|
| 76 |
+
<StatBox value={formatBytes(storageInfo.total_size_bytes)} label="Total storage" />
|
| 77 |
+
<StatBox value={String(storageInfo.file_count)} label="Files on disk" />
|
| 78 |
+
<StatBox value={String(storageInfo.project_count)} label="Saved projects" />
|
| 79 |
+
<StatBox value={String(storageInfo.library_count)} label="Library images" />
|
| 80 |
+
</div> : <div className="px-4 py-4 text-[#808080] text-xs">Could not load storage info.</div>}
|
| 81 |
+
{storageInfo && <div className="px-4 pb-4 space-y-2">
|
| 82 |
+
<InfoRow icon={<Database size={16} />} label="Projects data" value={formatBytes(storageInfo.projects_size_bytes)} description={`${storageInfo.project_count} board files saved locally.`} />
|
| 83 |
+
<InfoRow icon={<ImageIcon size={16} />} label="Asset library" value={formatBytes(storageInfo.library_size_bytes)} description={`${storageInfo.library_count} images with embedded data URLs and extracted palettes.`} />
|
| 84 |
+
<InfoRow icon={<Clock size={16} />} label="Auto-save interval" value="800ms" description="Project state is debounced then written to disk after edits." />
|
| 85 |
+
</div>}
|
| 86 |
+
</SettingsGroup>
|
| 87 |
+
<SettingsGroup title="Data Location">
|
| 88 |
+
<div className="px-4 py-3 flex items-center justify-between gap-4">
|
| 89 |
+
<div><div className="text-[14px] font-medium text-white">App data folder</div><div className="text-[11px] text-[#8A8A8C] mt-1 font-mono break-all leading-5">{storageInfo?.data_path || '...'}</div></div>
|
| 90 |
+
<button onClick={handleRevealFolder} className="shrink-0 px-3 py-2 rounded-xl text-[12px] font-semibold border bg-white/5 text-white border-white/10 hover:bg-white/10 flex items-center gap-1.5"><FolderOpen size={13} /> Open</button>
|
| 91 |
+
</div>
|
| 92 |
+
</SettingsGroup>
|
| 93 |
+
<SettingsGroup title="Export">
|
| 94 |
+
<ActionRow label="Export current board as .refs" description="Creates a portable .refs archive with all images embedded. Can be shared or backed up." actionLabel="Export .refs" onClick={handleExportRefs} />
|
| 95 |
+
</SettingsGroup>
|
| 96 |
+
<SettingsGroup title="Danger Zone">
|
| 97 |
+
<ActionRow label="Clear asset library" description="Remove all imported images from the library. Does not affect boards already open." actionLabel="Clear Library" danger onClick={handleClearLibrary} />
|
| 98 |
+
<ActionRow label="Delete all projects" description="Remove every saved board file from disk. The current board stays in memory until you close the app." actionLabel="Delete Projects" danger onClick={handleClearProjects} />
|
| 99 |
+
</SettingsGroup>
|
| 100 |
+
{storageMsg && <div className="mt-3 text-xs text-[#FFD60A] bg-[#FFD60A]/10 border border-[#FFD60A]/20 rounded-xl px-3 py-2">{storageMsg}</div>}
|
| 101 |
+
</Page>}
|
| 102 |
|
| 103 |
+
{activeTab === 'about' && <Page title="About" subtitle="Refstudio alpha build information."><div className="flex flex-col items-center text-center mt-8 gap-3"><div className="w-20 h-20 bg-[#2A2A2E] rounded-2xl flex items-center justify-center"><Monitor size={32} className="text-[#0A84FF]" /></div><h1 className="text-2xl font-semibold">Refstudio</h1><p className="text-[#808080]">1.0.0-alpha</p><p className="max-w-sm text-[#909090] text-sm leading-relaxed">A local-first artist reference board with embedded browser capture, asset library, annotations, and privacy-respecting storage.</p></div></Page>}
|
| 104 |
+
</div></main>
|
|
|
|
|
|
|
|
|
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
);
|