asdf98 commited on
Commit
73ff76e
·
verified ·
1 Parent(s): f5c97e5

fix: dedupe board image add events by original/source/data URL

Browse files
Files changed (1) hide show
  1. src/store.tsx +14 -52
src/store.tsx CHANGED
@@ -38,6 +38,8 @@ interface AppState {
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[]>([]);
@@ -70,22 +72,25 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
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),
@@ -98,59 +103,16 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
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; };
 
38
  const AppContext = createContext<AppState | undefined>(undefined);
39
  function emitError(msg: string) { window.dispatchEvent(new CustomEvent('muse:error', { detail: msg })); }
40
 
41
+ function normalizeUrl(v: any): string { return String(v || '').trim(); }
42
+
43
  export const AppProvider = ({ children }: { children: ReactNode }) => {
44
  const [textNotes, setTextNotes] = useState<TextNote[]>([]);
45
  const [images, setImages] = useState<RefImage[]>([]);
 
72
  const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
73
  const [boardTitle, setBoardTitle] = useState('Untitled Board');
74
 
 
 
75
  useEffect(() => {
76
  const un = listen<any>('board://image_added', (event) => {
77
  const p = event.payload || {};
78
+ const dataUrl = normalizeUrl(p.dataUrl || p.data_url || p.url);
79
+ const originalUrl = normalizeUrl(p.url || p.originalUrl || p.original_url || dataUrl);
80
+ const sourceUrl = normalizeUrl(p.sourceUrl || p.source_url || p.source || p.pageUrl || p.page_url);
81
+ if (!dataUrl) return;
82
  const width = Number(p.width || 300);
83
  const height = Number(p.height || 200);
84
  const ratio = width > 0 && height > 0 ? width / height : 1.5;
85
  const targetW = Math.min(480, Math.max(160, width > 0 ? width / 3 : 300));
86
  const targetH = targetW / ratio;
87
  setImages(prev => {
88
+ // Strong dedupe: same source/original/data URL within board means same capture.
89
+ if (prev.some(img => normalizeUrl(img.url) === dataUrl || normalizeUrl(img.url) === originalUrl || normalizeUrl(img.sourceUrl) === sourceUrl || normalizeUrl(img.sourceUrl) === originalUrl)) return prev;
90
  return [...prev, {
91
  id: crypto.randomUUID(),
92
+ url: dataUrl,
93
+ sourceUrl: sourceUrl || originalUrl,
94
  x: (-pan.x + window.innerWidth / 2 - targetW / 2) / zoom,
95
  y: (-pan.y + window.innerHeight / 2 - targetH / 2) / zoom,
96
  width: Math.round(targetW),
 
103
  return () => { un.then(fn => fn()); };
104
  }, [pan, zoom]);
105
 
106
+ useEffect(() => { invoke<string | null>('projects_get_active_id').then(id => { if (!id) return; setActiveProjectId(id); invoke<string>('project_load', { id }).then(json => { try { const saved = JSON.parse(json); if (saved.images) setImages(saved.images); if (saved.textNotes) setTextNotes(saved.textNotes); if (saved.annotations) setAnnotations(saved.annotations); if (saved.palettes) setPalettes(saved.palettes); if (saved.zoom) setZoom(saved.zoom); if (saved.pan) setPan(saved.pan); if (saved.title) setBoardTitle(saved.title); if (saved.valueMirrorIds) setValueMirrorIds(saved.valueMirrorIds); setCurrentScreen('board'); } catch (e) { console.error('Failed to parse project state:', e); emitError('Failed to load project — file may be corrupted'); } }).catch(e => { console.error('project_load failed:', e); emitError(`Failed to load project: ${e}`); }); }).catch(() => {}); }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  const saveFailedRef = React.useRef(false);
109
+ useEffect(() => { if (isUndoing) { setIsUndoing(false); return; } if (!activeProjectId) return; const timer = setTimeout(() => { const stateToSave = { textNotes, images, annotations, palettes, zoom, pan, valueMirrorIds, title: boardTitle }; invoke('project_save', { id: activeProjectId, state: JSON.stringify(stateToSave), title: boardTitle }).then(() => { saveFailedRef.current = false; }).catch(e => { if (!saveFailedRef.current) { emitError(`Auto-save failed: ${e}`); saveFailedRef.current = true; } console.error('Auto-save failed:', e); }); setHistory(prev => { const nh = prev.slice(0, historyIndex + 1 > 0 ? historyIndex + 1 : undefined); if (JSON.stringify(nh[nh.length - 1]) !== JSON.stringify(stateToSave)) { nh.push(stateToSave); if (nh.length > 200) nh.shift(); setHistoryIndex(nh.length - 1); } return nh; }); }, 800); return () => clearTimeout(timer); }, [textNotes, images, annotations, palettes, zoom, pan, valueMirrorIds, activeProjectId, boardTitle]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  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]);
112
  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]);
113
+ 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]);
114
 
115
+ 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, undo, redo, currentScreen, setCurrentScreen, updateSelectedNodes, focusedImageId, setFocusedImageId, valueMirrorIds, setValueMirrorIds, isZoomLensActive, setIsZoomLensActive, isWhisperBrowser, setIsWhisperBrowser, activeProjectId, setActiveProjectId, boardTitle, setBoardTitle }}>{children}</AppContext.Provider>;
 
 
 
 
 
 
 
 
 
 
116
  };
117
 
118
  export const useAppStore = () => { const ctx = useContext(AppContext); if (!ctx) throw new Error('useAppStore must be used within AppProvider'); return ctx; };