asdf98 commited on
Commit
e696850
·
verified ·
1 Parent(s): 43d57e8

fix: listen for browser hover ADD events and add captured image to active canvas

Browse files
Files changed (1) hide show
  1. src/store.tsx +156 -145
src/store.tsx CHANGED
@@ -1,145 +1,156 @@
1
- import React, { createContext, useContext, useState, ReactNode, useEffect, useCallback } from 'react';
2
- import { invoke } from '@tauri-apps/api/core';
3
- import { RefImage, ContextMenuState, AnnotationPath, Palette, TextNote } from './types';
4
-
5
- interface AppState {
6
- textNotes: TextNote[]; setTextNotes: React.Dispatch<React.SetStateAction<TextNote[]>>;
7
- images: RefImage[]; setImages: React.Dispatch<React.SetStateAction<RefImage[]>>;
8
- annotations: AnnotationPath[]; setAnnotations: React.Dispatch<React.SetStateAction<AnnotationPath[]>>;
9
- palettes: Palette[]; setPalettes: React.Dispatch<React.SetStateAction<Palette[]>>;
10
- zoom: number; setZoom: React.Dispatch<React.SetStateAction<number>>;
11
- pan: { x: number; y: number }; setPan: React.Dispatch<React.SetStateAction<{ x: number; y: number }>>;
12
- isSettingsOpen: boolean; setIsSettingsOpen: React.Dispatch<React.SetStateAction<boolean>>;
13
- isBrowserOpen: boolean; setIsBrowserOpen: React.Dispatch<React.SetStateAction<boolean>>;
14
- isLibraryOpen: boolean; setIsLibraryOpen: React.Dispatch<React.SetStateAction<boolean>>;
15
- selectedNodeIds: string[]; setSelectedNodeIds: React.Dispatch<React.SetStateAction<string[]>>;
16
- globalDesaturate: boolean; setGlobalDesaturate: React.Dispatch<React.SetStateAction<boolean>>;
17
- contextMenu: ContextMenuState; setContextMenu: React.Dispatch<React.SetStateAction<ContextMenuState>>;
18
- isAlwaysOnTop: boolean; setIsAlwaysOnTop: React.Dispatch<React.SetStateAction<boolean>>;
19
- bgOpacity: number; setBgOpacity: React.Dispatch<React.SetStateAction<number>>;
20
- isClickThrough: boolean; setIsClickThrough: React.Dispatch<React.SetStateAction<boolean>>;
21
- isAnnotationMode: boolean; setIsAnnotationMode: React.Dispatch<React.SetStateAction<boolean>>;
22
- annotationColor: string; setAnnotationColor: React.Dispatch<React.SetStateAction<string>>;
23
- annotationSize: number; setAnnotationSize: React.Dispatch<React.SetStateAction<number>>;
24
- isEraser: boolean; setIsEraser: React.Dispatch<React.SetStateAction<boolean>>;
25
- isHighlighter: boolean; setIsHighlighter: React.Dispatch<React.SetStateAction<boolean>>;
26
- undo: () => void; redo: () => void;
27
- currentScreen: 'hub' | 'board'; setCurrentScreen: React.Dispatch<React.SetStateAction<'hub' | 'board'>>;
28
- updateSelectedNodes: (dx: number, dy: number, explicitTargetId: string) => void;
29
- focusedImageId: string | null; setFocusedImageId: React.Dispatch<React.SetStateAction<string | null>>;
30
- valueMirrorIds: string[]; setValueMirrorIds: React.Dispatch<React.SetStateAction<string[]>>;
31
- isZoomLensActive: boolean; setIsZoomLensActive: React.Dispatch<React.SetStateAction<boolean>>;
32
- isWhisperBrowser: boolean; setIsWhisperBrowser: React.Dispatch<React.SetStateAction<boolean>>;
33
- activeProjectId: string | null; setActiveProjectId: React.Dispatch<React.SetStateAction<string | null>>;
34
- boardTitle: string; setBoardTitle: React.Dispatch<React.SetStateAction<string>>;
35
- }
36
-
37
- const AppContext = createContext<AppState | undefined>(undefined);
38
-
39
- // Emit error events for the toast system to pick up (avoids circular dep with toast.tsx)
40
- function emitError(msg: string) {
41
- window.dispatchEvent(new CustomEvent('muse:error', { detail: msg }));
42
- }
43
-
44
- export const AppProvider = ({ children }: { children: ReactNode }) => {
45
- const [textNotes, setTextNotes] = useState<TextNote[]>([]);
46
- const [images, setImages] = useState<RefImage[]>([]);
47
- const [annotations, setAnnotations] = useState<AnnotationPath[]>([]);
48
- const [palettes, setPalettes] = useState<Palette[]>([]);
49
- const [zoom, setZoom] = useState(1);
50
- const [pan, setPan] = useState({ x: 0, y: 0 });
51
- const [isSettingsOpen, setIsSettingsOpen] = useState(false);
52
- const [isBrowserOpen, setIsBrowserOpen] = useState(false);
53
- const [isLibraryOpen, setIsLibraryOpen] = useState(false);
54
- const [selectedNodeIds, setSelectedNodeIds] = useState<string[]>([]);
55
- const [globalDesaturate, setGlobalDesaturate] = useState(false);
56
- const [contextMenu, setContextMenu] = useState<ContextMenuState>(null);
57
- const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false);
58
- const [bgOpacity, setBgOpacity] = useState(50);
59
- const [isClickThrough, setIsClickThrough] = useState(false);
60
- const [isAnnotationMode, setIsAnnotationMode] = useState(false);
61
- const [annotationColor, setAnnotationColor] = useState('#FF453A');
62
- const [annotationSize, setAnnotationSize] = useState(4);
63
- const [isEraser, setIsEraser] = useState(false);
64
- const [isHighlighter, setIsHighlighter] = useState(false);
65
- const [history, setHistory] = useState<any[]>([]);
66
- const [historyIndex, setHistoryIndex] = useState(-1);
67
- const [isUndoing, setIsUndoing] = useState(false);
68
- const [currentScreen, setCurrentScreen] = useState<'hub' | 'board'>('hub');
69
- const [focusedImageId, setFocusedImageId] = useState<string | null>(null);
70
- const [valueMirrorIds, setValueMirrorIds] = useState<string[]>([]);
71
- const [isZoomLensActive, setIsZoomLensActive] = useState(false);
72
- const [isWhisperBrowser, setIsWhisperBrowser] = useState(false);
73
- const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
74
- const [boardTitle, setBoardTitle] = useState('Untitled Board');
75
-
76
- // Load active project on mount
77
- useEffect(() => {
78
- invoke<string | null>('projects_get_active_id').then(id => {
79
- if (!id) return;
80
- setActiveProjectId(id);
81
- invoke<string>('project_load', { id }).then(json => {
82
- try {
83
- const saved = JSON.parse(json);
84
- if (saved.images) setImages(saved.images);
85
- if (saved.textNotes) setTextNotes(saved.textNotes);
86
- if (saved.annotations) setAnnotations(saved.annotations);
87
- if (saved.palettes) setPalettes(saved.palettes);
88
- if (saved.zoom) setZoom(saved.zoom);
89
- if (saved.pan) setPan(saved.pan);
90
- if (saved.title) setBoardTitle(saved.title);
91
- if (saved.valueMirrorIds) setValueMirrorIds(saved.valueMirrorIds);
92
- setCurrentScreen('board');
93
- } catch (e) {
94
- console.error('Failed to parse project state:', e);
95
- emitError('Failed to load project — file may be corrupted');
96
- }
97
- }).catch(e => {
98
- console.error('project_load failed:', e);
99
- emitError(`Failed to load project: ${e}`);
100
- });
101
- }).catch(() => { /* no active project — stay on hub */ });
102
- }, []);
103
-
104
- // Auto-save with error reporting (only first failure shown, then silent until success)
105
- const saveFailedRef = React.useRef(false);
106
- useEffect(() => {
107
- if (isUndoing) { setIsUndoing(false); return; }
108
- if (!activeProjectId) return;
109
- const timer = setTimeout(() => {
110
- const stateToSave = { textNotes, images, annotations, palettes, zoom, pan, valueMirrorIds, title: boardTitle };
111
- invoke('project_save', { id: activeProjectId, state: JSON.stringify(stateToSave), title: boardTitle })
112
- .then(() => { saveFailedRef.current = false; })
113
- .catch(e => {
114
- if (!saveFailedRef.current) {
115
- emitError(`Auto-save failed: ${e}`);
116
- saveFailedRef.current = true;
117
- }
118
- console.error('Auto-save failed:', e);
119
- });
120
- setHistory(prev => {
121
- const nh = prev.slice(0, historyIndex + 1 > 0 ? historyIndex + 1 : undefined);
122
- if (JSON.stringify(nh[nh.length - 1]) !== JSON.stringify(stateToSave)) { nh.push(stateToSave); if (nh.length > 200) nh.shift(); setHistoryIndex(nh.length - 1); }
123
- return nh;
124
- });
125
- }, 800);
126
- return () => clearTimeout(timer);
127
- }, [textNotes, images, annotations, palettes, zoom, pan, valueMirrorIds, activeProjectId, boardTitle]);
128
-
129
- const undo = useCallback(() => { if (historyIndex > 0) { 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 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]);
131
-
132
- const updateSelectedNodes = useCallback((dx: number, dy: number, explicitTargetId: string) => {
133
- const ids = new Set([...selectedNodeIds, explicitTargetId]);
134
- 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); });
135
- 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); });
136
- }, [selectedNodeIds]);
137
-
138
- return (
139
- <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, undo, redo, currentScreen, setCurrentScreen, updateSelectedNodes, focusedImageId, setFocusedImageId, valueMirrorIds, setValueMirrorIds, isZoomLensActive, setIsZoomLensActive, isWhisperBrowser, setIsWhisperBrowser, activeProjectId, setActiveProjectId, boardTitle, setBoardTitle }}>
140
- {children}
141
- </AppContext.Provider>
142
- );
143
- };
144
-
145
- export const useAppStore = () => { const ctx = useContext(AppContext); if (!ctx) throw new Error('useAppStore must be used within AppProvider'); return ctx; };
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useContext, useState, ReactNode, useEffect, useCallback } from 'react';
2
+ 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[]>>;
9
+ annotations: AnnotationPath[]; setAnnotations: React.Dispatch<React.SetStateAction<AnnotationPath[]>>;
10
+ palettes: Palette[]; setPalettes: React.Dispatch<React.SetStateAction<Palette[]>>;
11
+ zoom: number; setZoom: React.Dispatch<React.SetStateAction<number>>;
12
+ pan: { x: number; y: number }; setPan: React.Dispatch<React.SetStateAction<{ x: number; y: number }>>;
13
+ isSettingsOpen: boolean; setIsSettingsOpen: React.Dispatch<React.SetStateAction<boolean>>;
14
+ isBrowserOpen: boolean; setIsBrowserOpen: React.Dispatch<React.SetStateAction<boolean>>;
15
+ isLibraryOpen: boolean; setIsLibraryOpen: React.Dispatch<React.SetStateAction<boolean>>;
16
+ selectedNodeIds: string[]; setSelectedNodeIds: React.Dispatch<React.SetStateAction<string[]>>;
17
+ globalDesaturate: boolean; setGlobalDesaturate: React.Dispatch<React.SetStateAction<boolean>>;
18
+ contextMenu: ContextMenuState; setContextMenu: React.Dispatch<React.SetStateAction<ContextMenuState>>;
19
+ isAlwaysOnTop: boolean; setIsAlwaysOnTop: React.Dispatch<React.SetStateAction<boolean>>;
20
+ bgOpacity: number; setBgOpacity: React.Dispatch<React.SetStateAction<number>>;
21
+ isClickThrough: boolean; setIsClickThrough: React.Dispatch<React.SetStateAction<boolean>>;
22
+ isAnnotationMode: boolean; setIsAnnotationMode: React.Dispatch<React.SetStateAction<boolean>>;
23
+ annotationColor: string; setAnnotationColor: React.Dispatch<React.SetStateAction<string>>;
24
+ annotationSize: number; setAnnotationSize: React.Dispatch<React.SetStateAction<number>>;
25
+ isEraser: boolean; setIsEraser: React.Dispatch<React.SetStateAction<boolean>>;
26
+ isHighlighter: boolean; setIsHighlighter: React.Dispatch<React.SetStateAction<boolean>>;
27
+ undo: () => void; redo: () => void;
28
+ currentScreen: 'hub' | 'board'; setCurrentScreen: React.Dispatch<React.SetStateAction<'hub' | 'board'>>;
29
+ updateSelectedNodes: (dx: number, dy: number, explicitTargetId: string) => void;
30
+ focusedImageId: string | null; setFocusedImageId: React.Dispatch<React.SetStateAction<string | null>>;
31
+ valueMirrorIds: string[]; setValueMirrorIds: React.Dispatch<React.SetStateAction<string[]>>;
32
+ isZoomLensActive: boolean; setIsZoomLensActive: React.Dispatch<React.SetStateAction<boolean>>;
33
+ isWhisperBrowser: boolean; setIsWhisperBrowser: React.Dispatch<React.SetStateAction<boolean>>;
34
+ activeProjectId: string | null; setActiveProjectId: React.Dispatch<React.SetStateAction<string | null>>;
35
+ boardTitle: string; setBoardTitle: React.Dispatch<React.SetStateAction<string>>;
36
+ }
37
+
38
+ const AppContext = createContext<AppState | undefined>(undefined);
39
+ function emitError(msg: string) { window.dispatchEvent(new CustomEvent('muse:error', { detail: msg })); }
40
+
41
+ export const AppProvider = ({ children }: { children: ReactNode }) => {
42
+ const [textNotes, setTextNotes] = useState<TextNote[]>([]);
43
+ const [images, setImages] = useState<RefImage[]>([]);
44
+ const [annotations, setAnnotations] = useState<AnnotationPath[]>([]);
45
+ const [palettes, setPalettes] = useState<Palette[]>([]);
46
+ const [zoom, setZoom] = useState(1);
47
+ const [pan, setPan] = useState({ x: 0, y: 0 });
48
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
49
+ const [isBrowserOpen, setIsBrowserOpen] = useState(false);
50
+ const [isLibraryOpen, setIsLibraryOpen] = useState(false);
51
+ const [selectedNodeIds, setSelectedNodeIds] = useState<string[]>([]);
52
+ const [globalDesaturate, setGlobalDesaturate] = useState(false);
53
+ const [contextMenu, setContextMenu] = useState<ContextMenuState>(null);
54
+ const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false);
55
+ const [bgOpacity, setBgOpacity] = useState(50);
56
+ const [isClickThrough, setIsClickThrough] = useState(false);
57
+ const [isAnnotationMode, setIsAnnotationMode] = useState(false);
58
+ const [annotationColor, setAnnotationColor] = useState('#FF453A');
59
+ const [annotationSize, setAnnotationSize] = useState(4);
60
+ const [isEraser, setIsEraser] = useState(false);
61
+ const [isHighlighter, setIsHighlighter] = useState(false);
62
+ const [history, setHistory] = useState<any[]>([]);
63
+ const [historyIndex, setHistoryIndex] = useState(-1);
64
+ const [isUndoing, setIsUndoing] = useState(false);
65
+ const [currentScreen, setCurrentScreen] = useState<'hub' | 'board'>('hub');
66
+ const [focusedImageId, setFocusedImageId] = useState<string | null>(null);
67
+ const [valueMirrorIds, setValueMirrorIds] = useState<string[]>([]);
68
+ const [isZoomLensActive, setIsZoomLensActive] = useState(false);
69
+ const [isWhisperBrowser, setIsWhisperBrowser] = useState(false);
70
+ const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
71
+ const [boardTitle, setBoardTitle] = useState('Untitled Board');
72
+
73
+ // Browser hover overlay -> Rust -> board://image_added -> active canvas state.
74
+ // The native browser capture path cannot directly mutate this React store, so this is the bridge.
75
+ useEffect(() => {
76
+ const un = listen<any>('board://image_added', (event) => {
77
+ const p = event.payload || {};
78
+ const width = Number(p.width || 300);
79
+ const height = Number(p.height || 200);
80
+ const ratio = width > 0 && height > 0 ? width / height : 1.5;
81
+ const targetW = Math.min(480, Math.max(160, width > 0 ? width / 3 : 300));
82
+ const targetH = targetW / ratio;
83
+ setImages(prev => {
84
+ if (prev.some(img => img.url === p.url || img.url === p.dataUrl || img.id === p.id)) return prev;
85
+ return [...prev, {
86
+ id: crypto.randomUUID(),
87
+ url: p.dataUrl || p.data_url || p.url,
88
+ sourceUrl: p.sourceUrl || p.source_url || p.source || p.originalUrl || p.original_url,
89
+ x: (-pan.x + window.innerWidth / 2 - targetW / 2) / zoom,
90
+ y: (-pan.y + window.innerHeight / 2 - targetH / 2) / zoom,
91
+ width: Math.round(targetW),
92
+ height: Math.round(targetH),
93
+ aspectRatio: ratio,
94
+ }];
95
+ });
96
+ setCurrentScreen('board');
97
+ });
98
+ return () => { un.then(fn => fn()); };
99
+ }, [pan, zoom]);
100
+
101
+ useEffect(() => {
102
+ invoke<string | null>('projects_get_active_id').then(id => {
103
+ if (!id) return;
104
+ setActiveProjectId(id);
105
+ invoke<string>('project_load', { id }).then(json => {
106
+ try {
107
+ const saved = JSON.parse(json);
108
+ if (saved.images) setImages(saved.images);
109
+ if (saved.textNotes) setTextNotes(saved.textNotes);
110
+ if (saved.annotations) setAnnotations(saved.annotations);
111
+ if (saved.palettes) setPalettes(saved.palettes);
112
+ if (saved.zoom) setZoom(saved.zoom);
113
+ if (saved.pan) setPan(saved.pan);
114
+ if (saved.title) setBoardTitle(saved.title);
115
+ if (saved.valueMirrorIds) setValueMirrorIds(saved.valueMirrorIds);
116
+ setCurrentScreen('board');
117
+ } catch (e) { console.error('Failed to parse project state:', e); emitError('Failed to load project — file may be corrupted'); }
118
+ }).catch(e => { console.error('project_load failed:', e); emitError(`Failed to load project: ${e}`); });
119
+ }).catch(() => {});
120
+ }, []);
121
+
122
+ const saveFailedRef = React.useRef(false);
123
+ useEffect(() => {
124
+ if (isUndoing) { setIsUndoing(false); return; }
125
+ if (!activeProjectId) return;
126
+ const timer = setTimeout(() => {
127
+ const stateToSave = { textNotes, images, annotations, palettes, zoom, pan, valueMirrorIds, title: boardTitle };
128
+ invoke('project_save', { id: activeProjectId, state: JSON.stringify(stateToSave), title: boardTitle })
129
+ .then(() => { saveFailedRef.current = false; })
130
+ .catch(e => { if (!saveFailedRef.current) { emitError(`Auto-save failed: ${e}`); saveFailedRef.current = true; } console.error('Auto-save failed:', e); });
131
+ setHistory(prev => {
132
+ const nh = prev.slice(0, historyIndex + 1 > 0 ? historyIndex + 1 : undefined);
133
+ if (JSON.stringify(nh[nh.length - 1]) !== JSON.stringify(stateToSave)) { nh.push(stateToSave); if (nh.length > 200) nh.shift(); setHistoryIndex(nh.length - 1); }
134
+ return nh;
135
+ });
136
+ }, 800);
137
+ return () => clearTimeout(timer);
138
+ }, [textNotes, images, annotations, palettes, zoom, pan, valueMirrorIds, activeProjectId, boardTitle]);
139
+
140
+ const undo = useCallback(() => { if (historyIndex > 0) { 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]);
141
+ 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]);
142
+
143
+ const updateSelectedNodes = useCallback((dx: number, dy: number, explicitTargetId: string) => {
144
+ const ids = new Set([...selectedNodeIds, explicitTargetId]);
145
+ 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); });
146
+ 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); });
147
+ }, [selectedNodeIds]);
148
+
149
+ return (
150
+ <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, undo, redo, currentScreen, setCurrentScreen, updateSelectedNodes, focusedImageId, setFocusedImageId, valueMirrorIds, setValueMirrorIds, isZoomLensActive, setIsZoomLensActive, isWhisperBrowser, setIsWhisperBrowser, activeProjectId, setActiveProjectId, boardTitle, setBoardTitle }}>
151
+ {children}
152
+ </AppContext.Provider>
153
+ );
154
+ };
155
+
156
+ export const useAppStore = () => { const ctx = useContext(AppContext); if (!ctx) throw new Error('useAppStore must be used within AppProvider'); return ctx; };