import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, Settings as SettingsIcon, Moon, Sun, Type, Eye, Bell, Trash2, Key, Check, Loader2, HardDrive, RefreshCw, Palette } from 'lucide-react'; import { useSettingsStore, PRESET_THEMES } from '../../stores/settingsStore'; import { validatePassToken } from '../../utils/apiClient'; import { getCacheStats, clearThreadCache, runCacheCleanup, formatBytes, type CacheStats } from '../../services/cacheService'; interface SettingsProps { isOpen: boolean; onClose: () => void; } export default function Settings({ isOpen, onClose }: SettingsProps) { const settings = useSettingsStore(); const [activeTab, setActiveTab] = useState<'appearance' | 'behavior' | 'privacy' | 'notifications' | 'account' | 'storage'>('appearance'); const [passToken, setPassToken] = useState(settings.chanPassToken || ''); const [isValidating, setIsValidating] = useState(false); const [passStatus, setPassStatus] = useState<'idle' | 'valid' | 'invalid'>('idle'); const [cacheStats, setCacheStats] = useState(null); const [isCacheLoading, setIsCacheLoading] = useState(false); // Load cache stats when storage tab is active useEffect(() => { if (isOpen && activeTab === 'storage') { loadCacheStats(); } }, [isOpen, activeTab]); const loadCacheStats = async () => { setIsCacheLoading(true); try { const stats = await getCacheStats(); setCacheStats(stats); } catch (e) { console.error('Failed to load cache stats:', e); } setIsCacheLoading(false); }; const handleClearCache = async () => { if (!confirm('Are you sure you want to clear all cached threads?')) return; setIsCacheLoading(true); try { await clearThreadCache(); await loadCacheStats(); } catch (e) { console.error('Failed to clear cache:', e); } setIsCacheLoading(false); }; const handleCleanupCache = async () => { setIsCacheLoading(true); try { await runCacheCleanup(settings.maxCacheAgeDays, settings.maxCacheSizeMB); await loadCacheStats(); } catch (e) { console.error('Failed to cleanup cache:', e); } setIsCacheLoading(false); }; if (!isOpen) return null; return (
{/* Backdrop */} {/* Modal */} {/* Header */}

Settings

{/* Content */}
{/* Sidebar */}
{/* Settings Panel */}
{activeTab === 'appearance' && (

Appearance

{/* Theme */}
{(['dark', 'light', 'auto'] as const).map((theme) => ( ))}
{/* Color Scheme */}
{Object.entries(PRESET_THEMES).map(([name, colors]) => ( ))}
{/* Custom Accent Color */}
settings.updateSettings({ customColors: { ...settings.customColors, accent: e.target.value } })} className="w-10 h-10 rounded cursor-pointer border-0" /> settings.updateSettings({ customColors: { ...settings.customColors, accent: e.target.value } })} className="input flex-1 font-mono text-sm" placeholder="#6366f1" />
{/* Font Size */}
settings.updateSettings({ fontSize: parseInt(e.target.value) })} className="w-full" />
{/* View Density */}
{(['compact', 'comfortable', 'spacious'] as const).map((density) => ( ))}
{/* Board Themes */}

Use different colors for each board

settings.updateSettings({ boardThemesEnabled: e.target.checked })} className="w-5 h-5" />
)} {activeTab === 'behavior' && (

Behavior

{/* Auto-refresh */}
settings.updateSettings({ autoRefreshInterval: parseInt(e.target.value) })} className="w-full" />

Set to 0 to disable auto-refresh

{/* Thread Auto-scroll */}

Automatically scroll to new posts

settings.updateSettings({ threadAutoScroll: e.target.checked })} className="w-5 h-5" />
{/* Media Auto-play */}

Automatically play videos and GIFs

settings.updateSettings({ mediaAutoPlay: e.target.checked })} className="w-5 h-5" />
)} {activeTab === 'privacy' && (

Privacy

{/* NSFW Blur */}

Blur potentially NSFW images by default

settings.updateSettings({ nsfwBlur: e.target.checked })} className="w-5 h-5" />
{/* Disable Image Loading */}

Save bandwidth by not loading images

settings.updateSettings({ disableImageLoading: e.target.checked })} className="w-5 h-5" />
{/* Clear Cache on Exit */}

Automatically clear cached data when closing

settings.updateSettings({ clearCacheOnExit: e.target.checked })} className="w-5 h-5" />
{/* Proxy */}
settings.updateSettings({ proxyUrl: e.target.value })} className="input w-full" />

Leave empty to connect directly

)} {activeTab === 'notifications' && (

Notifications

{/* Desktop Notifications */}

Show notifications for watched threads

settings.updateSettings({ desktopNotifications: e.target.checked })} className="w-5 h-5" />
{/* Notification Sound */}

Play sound when new replies are detected

settings.updateSettings({ notificationSound: e.target.checked })} className="w-5 h-5" />
)} {activeTab === 'account' && (

4chan Pass

Enter your 4chan Pass token to post without solving captchas. You can get a Pass at 4chan.org/pass

{ setPassToken(e.target.value); setPassStatus('idle'); }} className="input flex-1" />
{passStatus === 'valid' && (

Pass token saved and validated!

)} {passStatus === 'invalid' && (

Invalid token. Make sure you copied the pass_id cookie correctly.

)} {settings.chanPassToken && ( )}

Your token is stored locally and never sent to any server except 4chan.

)} {activeTab === 'storage' && (

Storage & Cache

{/* Cache Stats */}

Thread Cache

{cacheStats ? (

Cached Threads

{cacheStats.thread_count}

Cached Posts

{cacheStats.post_count}

Cache Size

{formatBytes(cacheStats.db_size_bytes)}

Limit

{settings.maxCacheSizeMB} MB

) : (

Loading cache stats...

)}
{/* Cache Settings */}
{/* Enable Cache */}

Cache threads for faster loading

settings.updateSettings({ threadCacheEnabled: e.target.checked })} className="w-5 h-5" />
{/* Max Cache Age */}
settings.updateSettings({ maxCacheAgeDays: parseInt(e.target.value) })} className="w-full" />

Older cache entries are automatically deleted

{/* Max Cache Size */}
settings.updateSettings({ maxCacheSizeMB: parseInt(e.target.value) })} className="w-full" />

Least recently used threads are deleted when limit is reached

{/* Cache Actions */}

Cache is stored locally in your app data folder.

)}
{/* Footer */}
); }