import { X, Settings, Keyboard, Monitor, Shield, HardDrive, Info, RefreshCw, Grid3X3, Navigation, FileDown, FileUp, FileArchive, Check, Trash2 } from 'lucide-react'; import { useAppStore, type ThemeId } from '../store'; import { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; type Tab = 'general' | 'appearance' | 'shortcuts' | 'privacy' | 'storage' | 'about'; interface ShieldReport { blocked_requests: number; blocked_cosmetic: number; https_upgrades: number; engine_rules: number; } const THEMES: { id: ThemeId; name: string; description: string; colors: string[] }[] = [ { id: 'dark-canvas', name: 'Dark Canvas', description: 'Neutral dark background optimized for image reference work. Default.', colors: ['#1C1C1E', '#2A2A2E', '#3A3A3E', '#0A84FF'] }, { id: 'warm-studio', name: 'Warm Studio', description: 'Warm earthy tones inspired by traditional art studios.', colors: ['#221F1B', '#2E2A24', '#46403A', '#D4A373'] }, { id: 'midnight', name: 'Midnight', description: 'Deep blue-black for late night sessions with reduced eye strain.', colors: ['#0F1319', '#171D28', '#253040', '#6C89E8'] }, ]; export const SettingsPanel = () => { const { isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, setBgOpacity, showMinimap, setShowMinimap, showGrid, setShowGrid, theme, setTheme, images, textNotes, annotations, palettes, zoom, pan, boardTitle, setImages, setTextNotes, setAnnotations, setPalettes, setZoom, setPan, setBoardTitle, setCurrentScreen } = useAppStore(); const [activeTab, setActiveTab] = useState('general'); const [shieldReport, setShieldReport] = useState({ blocked_requests: 0, blocked_cosmetic: 0, https_upgrades: 0, engine_rules: 0 }); const [toastMsg, setToastMsg] = useState(''); const [isExporting, setIsExporting] = useState(false); useEffect(() => { if (isSettingsOpen) invoke('shield_get_report').then(setShieldReport).catch(() => {}); }, [isSettingsOpen]); useEffect(() => { if (toastMsg) { const t = setTimeout(() => setToastMsg(''), 4000); return () => clearTimeout(t); } }, [toastMsg]); // Estimate data size (images as base64 take ~1.37x original size) const dataSize = (() => { let bytes = 0; images.forEach(img => { bytes += (img.url?.length || 0); }); textNotes.forEach(n => { bytes += (n.text?.length || 0) * 2; }); annotations.forEach(a => { bytes += a.points.length * 16; }); if (bytes > 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`; if (bytes > 1024) return `${(bytes / 1024).toFixed(0)} KB`; return `${bytes} bytes`; })(); // Export as .refs (portable ZIP with all assets embedded) const handleExportRefs = async () => { if (isExporting) return; setIsExporting(true); try { const state = JSON.stringify({ title: boardTitle, images, textNotes, annotations, palettes, zoom, pan }); const bytes = await invoke('refs_export', { stateJson: state }); const blob = new Blob([new Uint8Array(bytes)], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); const filename = `${(boardTitle || 'board').replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, '-').toLowerCase() || 'board'}.refs`; const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setToastMsg(`✓ Exported "${filename}" — portable board with all images embedded`); } catch (e) { setToastMsg(`✗ Export failed: ${e}`); } setIsExporting(false); }; // Export as JSON (lightweight, URLs only) const handleExportJson = () => { try { const state = { title: boardTitle, images, textNotes, annotations, palettes, zoom, pan }; const json = JSON.stringify(state, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const filename = `${(boardTitle || 'board').replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, '-').toLowerCase() || 'board'}.json`; const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setToastMsg(`✓ Exported "${filename}" — lightweight JSON (images as URLs/base64)`); } catch (e) { setToastMsg(`✗ Export failed: ${e}`); } }; // Import .refs file const handleImportRefs = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.refs'; input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (!file) return; try { const buffer = await file.arrayBuffer(); const bytes = Array.from(new Uint8Array(buffer)); const stateJson = await invoke('refs_import', { data: bytes }); const state = JSON.parse(stateJson); if (state.images) setImages(state.images); if (state.textNotes) setTextNotes(state.textNotes); if (state.annotations) setAnnotations(state.annotations); if (state.palettes) setPalettes(state.palettes); if (state.zoom) setZoom(state.zoom); if (state.pan) setPan(state.pan); if (state.title) setBoardTitle(state.title); setCurrentScreen('board'); setToastMsg(`✓ Imported "${file.name}" — ${state.images?.length || 0} images loaded`); } catch (e) { setToastMsg(`✗ Import failed: ${e}`); } }; input.click(); }; // Import JSON const handleImportJson = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (!file) return; try { const text = await file.text(); const state = JSON.parse(text); if (state.images) setImages(state.images); if (state.textNotes) setTextNotes(state.textNotes); if (state.annotations) setAnnotations(state.annotations); if (state.palettes) setPalettes(state.palettes); if (state.zoom) setZoom(state.zoom); if (state.pan) setPan(state.pan); if (state.title) setBoardTitle(state.title); setCurrentScreen('board'); setToastMsg(`✓ Imported "${file.name}"`); } catch (e) { setToastMsg(`✗ Import failed: ${e}`); } }; input.click(); }; // Clear board const handleClearBoard = () => { if (!confirm('Clear all images, notes, and annotations from the current board? This cannot be undone.')) return; setImages([]); setTextNotes([]); setAnnotations([]); setPalettes([]); setToastMsg('Board cleared'); }; const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [ { id: 'general', label: 'General', icon: }, { id: 'appearance', label: 'Appearance', icon: }, { id: 'shortcuts', label: 'Shortcuts', icon: }, { id: 'privacy', label: 'Privacy', icon: }, { id: 'storage', label: 'Storage & Export', icon: }, { id: 'about', label: 'About', icon: }, ]; return (

Settings

{activeTab === 'general' && <>
setIsAlwaysOnTop(!isAlwaysOnTop)} />{isAlwaysOnTop && }
} label="Grid" description="Show dotted canvas grid." active={showGrid} onToggle={() => setShowGrid(!showGrid)} />} label="Minimap" description="Compact board overview in bottom-right." active={showMinimap} onToggle={() => setShowMinimap(!showMinimap)} />
} {activeTab === 'appearance' && <>
{THEMES.map(t => )}
} {activeTab === 'shortcuts' && <>
} {activeTab === 'privacy' && <>
} {activeTab === 'storage' && <>

Creates a standalone .refs ZIP archive containing all images, metadata, annotations, and layout. Fully portable — can be opened on any machine without network access.

Exports board as a JSON file. Images are stored as base64 data URLs or external URLs depending on how they were captured. Smaller file, requires embedded data to be preserved.

Load a previously exported board file. Replaces current board content.

} {activeTab === 'about' && <>

Refstudio

1.0.0-alpha

A local-first artist reference board with embedded browser capture, asset library, annotations, and privacy-respecting storage.

Built with Tauri v2 + Rust + React
{shieldReport.engine_rules.toLocaleString()} adblock filter rules loaded
}
{/* Toast notification */} {toastMsg &&
{toastMsg}
}
); }; function Header({ title, subtitle }: { title: string; subtitle: string }) { return

{title}

{subtitle}

; } function Section({ title, children }: { title: string; children: React.ReactNode }) { return

{title}

{children}
; } function ToggleRow({ label, description, active, onToggle, icon }: { label: string; description: string; active: boolean; onToggle: () => void; icon?: React.ReactNode }) { return ; } function RangeRow({ label, value, min, max, suffix, onChange }: { label: string; value: number; min: number; max: number; suffix: string; onChange: (v: number) => void }) { return
{label}
{value}{suffix} onChange(Number(e.target.value))} className="w-28 accent-[#0A84FF]" />
; } function InfoRow({ label, value, description }: { label: string; value: string; description: string }) { return
{label}
{description}
{value}
; } function SK({ combo, label }: { combo: string; label: string }) { return
{label}{combo}
; } function Stat({ value, label }: { value: string; label: string }) { return
{value}
{label}
; }