feat: add theme state to store; apply data-theme to document; persist in localStorage
Browse files- src/store.tsx +9 -1
src/store.tsx
CHANGED
|
@@ -3,6 +3,8 @@ import { invoke } from '@tauri-apps/api/core';
|
|
| 3 |
import { listen } from '@tauri-apps/api/event';
|
| 4 |
import { RefImage, ContextMenuState, AnnotationPath, Palette, TextNote } from './types';
|
| 5 |
|
|
|
|
|
|
|
| 6 |
interface AppState {
|
| 7 |
textNotes: TextNote[]; setTextNotes: React.Dispatch<React.SetStateAction<TextNote[]>>;
|
| 8 |
images: RefImage[]; setImages: React.Dispatch<React.SetStateAction<RefImage[]>>;
|
|
@@ -26,6 +28,7 @@ interface AppState {
|
|
| 26 |
isHighlighter: boolean; setIsHighlighter: React.Dispatch<React.SetStateAction<boolean>>;
|
| 27 |
showMinimap: boolean; setShowMinimap: React.Dispatch<React.SetStateAction<boolean>>;
|
| 28 |
showGrid: boolean; setShowGrid: React.Dispatch<React.SetStateAction<boolean>>;
|
|
|
|
| 29 |
undo: () => void; redo: () => void;
|
| 30 |
currentScreen: 'hub' | 'board'; setCurrentScreen: React.Dispatch<React.SetStateAction<'hub' | 'board'>>;
|
| 31 |
updateSelectedNodes: (dx: number, dy: number, explicitTargetId: string) => void;
|
|
@@ -64,6 +67,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
|
|
| 64 |
const [isHighlighter, setIsHighlighter] = useState(false);
|
| 65 |
const [showMinimap, setShowMinimap] = useState(false);
|
| 66 |
const [showGrid, setShowGrid] = useState(true);
|
|
|
|
| 67 |
const [history, setHistory] = useState<any[]>([]);
|
| 68 |
const [historyIndex, setHistoryIndex] = useState(-1);
|
| 69 |
const [isUndoing, setIsUndoing] = useState(false);
|
|
@@ -80,13 +84,17 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
|
|
| 80 |
useEffect(() => { panRef.current = pan; }, [pan]);
|
| 81 |
useEffect(() => { zoomRef.current = zoom; }, [zoom]);
|
| 82 |
|
|
|
|
| 83 |
useEffect(() => {
|
| 84 |
if (localStorage.getItem('refstudio.showMinimap') === 'true') setShowMinimap(true);
|
| 85 |
const grid = localStorage.getItem('refstudio.showGrid');
|
| 86 |
if (grid !== null) setShowGrid(grid === 'true');
|
|
|
|
|
|
|
| 87 |
}, []);
|
| 88 |
useEffect(() => { localStorage.setItem('refstudio.showMinimap', String(showMinimap)); }, [showMinimap]);
|
| 89 |
useEffect(() => { localStorage.setItem('refstudio.showGrid', String(showGrid)); }, [showGrid]);
|
|
|
|
| 90 |
|
| 91 |
useEffect(() => {
|
| 92 |
const un = listen<any>('board://image_added', (event) => {
|
|
@@ -121,7 +129,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
|
|
| 121 |
const redo = useCallback(() => { if (historyIndex < history.length - 1) { setIsUndoing(true); const s = history[historyIndex + 1]; setTextNotes(s.textNotes); setImages(s.images); setAnnotations(s.annotations); setPalettes(s.palettes); setZoom(s.zoom); setPan(s.pan); if (s.valueMirrorIds) setValueMirrorIds(s.valueMirrorIds); setHistoryIndex(historyIndex + 1); } }, [history, historyIndex]);
|
| 122 |
const updateSelectedNodes = useCallback((dx: number, dy: number, explicitTargetId: string) => { const ids = new Set([...selectedNodeIds, explicitTargetId]); setImages(prev => { const gids = new Set<string>(); prev.forEach(i => { if (ids.has(i.id) && i.groupId) gids.add(i.groupId); }); return prev.map(i => (ids.has(i.id) || (i.groupId && gids.has(i.groupId))) ? { ...i, x: i.x + dx, y: i.y + dy } : i); }); setTextNotes(prev => { const gids = new Set<string>(); prev.forEach(i => { if (ids.has(i.id) && i.groupId) gids.add(i.groupId); }); return prev.map(i => (ids.has(i.id) || (i.groupId && gids.has(i.groupId))) ? { ...i, x: i.x + dx, y: i.y + dy } : i); }); }, [selectedNodeIds]);
|
| 123 |
|
| 124 |
-
return <AppContext.Provider value={{ textNotes, setTextNotes, images, setImages, annotations, setAnnotations, palettes, setPalettes, zoom, setZoom, pan, setPan, isSettingsOpen, setIsSettingsOpen, isBrowserOpen, setIsBrowserOpen, isLibraryOpen, setIsLibraryOpen, selectedNodeIds, setSelectedNodeIds, globalDesaturate, setGlobalDesaturate, contextMenu, setContextMenu, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, setBgOpacity, isClickThrough, setIsClickThrough, isAnnotationMode, setIsAnnotationMode, annotationColor, setAnnotationColor, annotationSize, setAnnotationSize, isEraser, setIsEraser, isHighlighter, setIsHighlighter, showMinimap, setShowMinimap, showGrid, setShowGrid, undo, redo, currentScreen, setCurrentScreen, updateSelectedNodes, focusedImageId, setFocusedImageId, valueMirrorIds, setValueMirrorIds, isZoomLensActive, setIsZoomLensActive, isWhisperBrowser, setIsWhisperBrowser, activeProjectId, setActiveProjectId, boardTitle, setBoardTitle }}>{children}</AppContext.Provider>;
|
| 125 |
};
|
| 126 |
|
| 127 |
export const useAppStore = () => { const ctx = useContext(AppContext); if (!ctx) throw new Error('useAppStore must be used within AppProvider'); return ctx; };
|
|
|
|
| 3 |
import { listen } from '@tauri-apps/api/event';
|
| 4 |
import { RefImage, ContextMenuState, AnnotationPath, Palette, TextNote } from './types';
|
| 5 |
|
| 6 |
+
export type ThemeId = 'dark-canvas' | 'warm-studio' | 'midnight';
|
| 7 |
+
|
| 8 |
interface AppState {
|
| 9 |
textNotes: TextNote[]; setTextNotes: React.Dispatch<React.SetStateAction<TextNote[]>>;
|
| 10 |
images: RefImage[]; setImages: React.Dispatch<React.SetStateAction<RefImage[]>>;
|
|
|
|
| 28 |
isHighlighter: boolean; setIsHighlighter: React.Dispatch<React.SetStateAction<boolean>>;
|
| 29 |
showMinimap: boolean; setShowMinimap: React.Dispatch<React.SetStateAction<boolean>>;
|
| 30 |
showGrid: boolean; setShowGrid: React.Dispatch<React.SetStateAction<boolean>>;
|
| 31 |
+
theme: ThemeId; setTheme: React.Dispatch<React.SetStateAction<ThemeId>>;
|
| 32 |
undo: () => void; redo: () => void;
|
| 33 |
currentScreen: 'hub' | 'board'; setCurrentScreen: React.Dispatch<React.SetStateAction<'hub' | 'board'>>;
|
| 34 |
updateSelectedNodes: (dx: number, dy: number, explicitTargetId: string) => void;
|
|
|
|
| 67 |
const [isHighlighter, setIsHighlighter] = useState(false);
|
| 68 |
const [showMinimap, setShowMinimap] = useState(false);
|
| 69 |
const [showGrid, setShowGrid] = useState(true);
|
| 70 |
+
const [theme, setTheme] = useState<ThemeId>('dark-canvas');
|
| 71 |
const [history, setHistory] = useState<any[]>([]);
|
| 72 |
const [historyIndex, setHistoryIndex] = useState(-1);
|
| 73 |
const [isUndoing, setIsUndoing] = useState(false);
|
|
|
|
| 84 |
useEffect(() => { panRef.current = pan; }, [pan]);
|
| 85 |
useEffect(() => { zoomRef.current = zoom; }, [zoom]);
|
| 86 |
|
| 87 |
+
// Persist preferences
|
| 88 |
useEffect(() => {
|
| 89 |
if (localStorage.getItem('refstudio.showMinimap') === 'true') setShowMinimap(true);
|
| 90 |
const grid = localStorage.getItem('refstudio.showGrid');
|
| 91 |
if (grid !== null) setShowGrid(grid === 'true');
|
| 92 |
+
const savedTheme = localStorage.getItem('refstudio.theme') as ThemeId | null;
|
| 93 |
+
if (savedTheme && ['dark-canvas', 'warm-studio', 'midnight'].includes(savedTheme)) setTheme(savedTheme);
|
| 94 |
}, []);
|
| 95 |
useEffect(() => { localStorage.setItem('refstudio.showMinimap', String(showMinimap)); }, [showMinimap]);
|
| 96 |
useEffect(() => { localStorage.setItem('refstudio.showGrid', String(showGrid)); }, [showGrid]);
|
| 97 |
+
useEffect(() => { localStorage.setItem('refstudio.theme', theme); document.documentElement.setAttribute('data-theme', theme); }, [theme]);
|
| 98 |
|
| 99 |
useEffect(() => {
|
| 100 |
const un = listen<any>('board://image_added', (event) => {
|
|
|
|
| 129 |
const redo = useCallback(() => { if (historyIndex < history.length - 1) { setIsUndoing(true); const s = history[historyIndex + 1]; setTextNotes(s.textNotes); setImages(s.images); setAnnotations(s.annotations); setPalettes(s.palettes); setZoom(s.zoom); setPan(s.pan); if (s.valueMirrorIds) setValueMirrorIds(s.valueMirrorIds); setHistoryIndex(historyIndex + 1); } }, [history, historyIndex]);
|
| 130 |
const updateSelectedNodes = useCallback((dx: number, dy: number, explicitTargetId: string) => { const ids = new Set([...selectedNodeIds, explicitTargetId]); setImages(prev => { const gids = new Set<string>(); prev.forEach(i => { if (ids.has(i.id) && i.groupId) gids.add(i.groupId); }); return prev.map(i => (ids.has(i.id) || (i.groupId && gids.has(i.groupId))) ? { ...i, x: i.x + dx, y: i.y + dy } : i); }); setTextNotes(prev => { const gids = new Set<string>(); prev.forEach(i => { if (ids.has(i.id) && i.groupId) gids.add(i.groupId); }); return prev.map(i => (ids.has(i.id) || (i.groupId && gids.has(i.groupId))) ? { ...i, x: i.x + dx, y: i.y + dy } : i); }); }, [selectedNodeIds]);
|
| 131 |
|
| 132 |
+
return <AppContext.Provider value={{ textNotes, setTextNotes, images, setImages, annotations, setAnnotations, palettes, setPalettes, zoom, setZoom, pan, setPan, isSettingsOpen, setIsSettingsOpen, isBrowserOpen, setIsBrowserOpen, isLibraryOpen, setIsLibraryOpen, selectedNodeIds, setSelectedNodeIds, globalDesaturate, setGlobalDesaturate, contextMenu, setContextMenu, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, setBgOpacity, isClickThrough, setIsClickThrough, isAnnotationMode, setIsAnnotationMode, annotationColor, setAnnotationColor, annotationSize, setAnnotationSize, isEraser, setIsEraser, isHighlighter, setIsHighlighter, showMinimap, setShowMinimap, showGrid, setShowGrid, theme, setTheme, undo, redo, currentScreen, setCurrentScreen, updateSelectedNodes, focusedImageId, setFocusedImageId, valueMirrorIds, setValueMirrorIds, isZoomLensActive, setIsZoomLensActive, isWhisperBrowser, setIsWhisperBrowser, activeProjectId, setActiveProjectId, boardTitle, setBoardTitle }}>{children}</AppContext.Provider>;
|
| 133 |
};
|
| 134 |
|
| 135 |
export const useAppStore = () => { const ctx = useContext(AppContext); if (!ctx) throw new Error('useAppStore must be used within AppProvider'); return ctx; };
|