asdf98 commited on
Commit
da47a90
·
verified ·
1 Parent(s): 9665ef1

feat: route app keyboard handling through customizable shortcut registry

Browse files
Files changed (1) hide show
  1. src/App.tsx +30 -40
src/App.tsx CHANGED
@@ -8,6 +8,7 @@ import { StarterHub } from './components/StarterHub';
8
  import { getCurrentWindow } from '@tauri-apps/api/window';
9
  import { invoke } from '@tauri-apps/api/core';
10
  import { listen } from '@tauri-apps/api/event';
 
11
 
12
  const BrowserPanel = lazy(() => import('./components/BrowserPanel').then(m => ({ default: m.BrowserPanel })));
13
  const LibraryPanel = lazy(() => import('./components/LibraryPanel').then(m => ({ default: m.LibraryPanel })));
@@ -23,52 +24,41 @@ const MainApp = () => {
23
  useEffect(() => { if (toastMsg) { const t = setTimeout(() => setToastMsg(null), 3000); return () => clearTimeout(t); } }, [toastMsg]);
24
  useEffect(() => { appWindow.setAlwaysOnTop(isAlwaysOnTop).catch(() => {}); }, [isAlwaysOnTop]);
25
 
26
- useEffect(() => {
27
- const unlisten = listen<any>('board://image_added', (event) => {
28
- const data = event.payload;
29
- if (!data || !data.url) return;
30
- const img = new Image();
31
- const add = (w0: number, h0: number) => { const ratio = w0 / h0; const w = Math.min(400, w0); const h = w / ratio; setImages(prev => [...prev, { id: data.id || crypto.randomUUID(), url: data.url, x: (-pan.x + window.innerWidth / 3 + Math.random() * 80) / zoom, y: (-pan.y + window.innerHeight / 3 + Math.random() * 80) / zoom, width: w, height: h, aspectRatio: ratio }]); setToastMsg('✓ Image added'); };
32
- img.onload = () => add(img.width, img.height);
33
- img.onerror = () => add(data.width || 300, data.height || 200);
34
- img.src = data.url;
35
- });
36
- return () => { unlisten.then(fn => fn()); };
37
- }, [pan, zoom, setImages]);
38
 
39
- const doScreenCapture = async () => {
40
- setIsCapturing(true); setToastMsg('Capturing...');
41
- try { await appWindow.minimize(); await new Promise(r => setTimeout(r, 300)); const dataUrl = await invoke<string>('screen_capture_full'); await appWindow.unminimize(); await appWindow.setFocus(); if (dataUrl) { const img = new Image(); img.onload = () => { const ratio = img.width / img.height; const w = Math.min(800, img.width); const h = w / ratio; setImages(prev => [...prev, { id: crypto.randomUUID(), url: dataUrl, x: (-pan.x + window.innerWidth/2 - w/2)/zoom, y: (-pan.y + window.innerHeight/2 - h/2)/zoom, width: w, height: h, aspectRatio: ratio }]); setToastMsg('Screenshot captured!'); }; img.src = dataUrl; } } catch { setToastMsg('Capture failed'); await appWindow.unminimize().catch(() => {}); }
42
- setIsCapturing(false);
43
- };
44
 
45
  useEffect(() => {
46
  const onKeyDown = (e: KeyboardEvent) => {
47
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || (e.target as HTMLElement)?.isContentEditable) return;
48
- const key = e.key.toLowerCase(); const ctrl = e.ctrlKey || e.metaKey;
49
- if (ctrl && key === 'z') { e.preventDefault(); if (e.shiftKey) redo(); else undo(); return; }
50
- if (ctrl && key === 's') { e.preventDefault(); setToastMsg('Saved'); return; }
51
- if (e.shiftKey && key === 's' && !ctrl) { e.preventDefault(); doScreenCapture(); return; }
52
- if (key === 'f' && !ctrl && !e.shiftKey) { if (focusedImageId) setFocusedImageId(null); else if (selectedNodeIds.length > 0) setFocusedImageId(selectedNodeIds[0]); return; }
53
- if (key === 'v' && !ctrl && !e.shiftKey && selectedNodeIds.length > 0) { setValueMirrorIds(prev => { const all = selectedNodeIds.every(id => prev.includes(id)); return all ? prev.filter(id => !selectedNodeIds.includes(id)) : [...new Set([...prev, ...selectedNodeIds])]; }); return; }
54
- if (key === 'h' && !ctrl && !e.shiftKey && selectedNodeIds.length > 0) { setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isFlippedH: !i.isFlippedH } : i)); return; }
55
- if (key === 'z' && !ctrl && !e.shiftKey) { setIsZoomLensActive(true); return; }
56
- if (ctrl && key === 'd') { e.preventDefault(); if (selectedNodeIds.length > 0) { const dupes = images.filter(i => selectedNodeIds.includes(i.id)).map(i => ({ ...i, id: crypto.randomUUID(), x: i.x + 30, y: i.y + 30 })); setImages(prev => [...prev, ...dupes]); setSelectedNodeIds(dupes.map(d => d.id)); setToastMsg(`Duplicated ${dupes.length}`); } return; }
57
- if (ctrl && key === 'g') { e.preventDefault(); if (e.shiftKey) { setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, groupId: undefined } : i)); setTextNotes(prev => prev.map(n => selectedNodeIds.includes(n.id) ? { ...n, groupId: undefined } : n)); setToastMsg('Ungrouped'); } else if (selectedNodeIds.length > 1) { const gid = crypto.randomUUID(); setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, groupId: gid } : i)); setTextNotes(prev => prev.map(n => selectedNodeIds.includes(n.id) ? { ...n, groupId: gid } : n)); setToastMsg('Grouped'); } return; }
 
 
 
58
  if (focusedImageId && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) { const idx = images.findIndex(i => i.id === focusedImageId); if (idx >= 0) { const next = e.key === 'ArrowRight' ? (idx + 1) % images.length : (idx - 1 + images.length) % images.length; setFocusedImageId(images[next].id); setSelectedNodeIds([images[next].id]); } return; }
59
- if (key === 'b' && !ctrl && !e.shiftKey) { const now = Date.now(); if (now - lastBPress.current < 300) { setIsWhisperBrowser(true); setIsBrowserOpen(true); } else { setIsBrowserOpen(p => !p); setIsWhisperBrowser(false); } lastBPress.current = now; return; }
60
- if (key === 'l' && !ctrl && !e.shiftKey) { setIsLibraryOpen(p => !p); return; }
61
- if (ctrl && key === ',') { e.preventDefault(); setIsSettingsOpen(p => !p); return; }
62
- if (key === 'a' && !ctrl && !e.shiftKey) { setIsAnnotationMode(p => !p); return; }
63
- if (key === 'd' && !ctrl) { if (e.shiftKey) setGlobalDesaturate(p => !p); else if (selectedNodeIds.length > 0) setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isDesaturated: !i.isDesaturated } : i)); return; }
64
- if (key === 't' && !ctrl && !e.shiftKey) { setIsAlwaysOnTop(p => !p); return; }
65
- if (e.key === 'Escape') { setIsBrowserOpen(false); setIsLibraryOpen(false); setIsSettingsOpen(false); setSelectedNodeIds([]); setFocusedImageId(null); return; }
66
- if (e.key === 'Delete' || e.key === 'Backspace') { if (selectedNodeIds.length > 0) { setImages(prev => prev.filter(i => !selectedNodeIds.includes(i.id))); setTextNotes(prev => prev.filter(n => !selectedNodeIds.includes(n.id))); setSelectedNodeIds([]); } return; }
67
- if (ctrl && key === 'a') { e.preventDefault(); setSelectedNodeIds(images.map(i => i.id)); return; }
68
- if (ctrl && key === '0') { e.preventDefault(); if (images.length === 0) { setZoom(1); setPan({ x: 0, y: 0 }); return; } let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; images.forEach(i => { minX = Math.min(minX, i.x); minY = Math.min(minY, i.y); maxX = Math.max(maxX, i.x + i.width); maxY = Math.max(maxY, i.y + i.height); }); const pad = 100, w = maxX - minX + pad*2, h = maxY - minY + pad*2; const nz = Math.max(0.1, Math.min(window.innerWidth/w, window.innerHeight/h)); setZoom(nz); setPan({ x: -((minX+maxX)/2)*nz + window.innerWidth/2, y: -((minY+maxY)/2)*nz + window.innerHeight/2 }); }
69
- if (ctrl && key === '1') { e.preventDefault(); setZoom(1); }
 
70
  };
71
- const onKeyUp = (e: KeyboardEvent) => { if (e.key.toLowerCase() === 'z') setIsZoomLensActive(false); };
72
  window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp);
73
  return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); };
74
  }, [selectedNodeIds, images, pan, zoom, focusedImageId, undo, redo]);
 
8
  import { getCurrentWindow } from '@tauri-apps/api/window';
9
  import { invoke } from '@tauri-apps/api/core';
10
  import { listen } from '@tauri-apps/api/event';
11
+ import { isEditableTarget, loadShortcutSettings, matchesShortcut } from './shortcutSystem';
12
 
13
  const BrowserPanel = lazy(() => import('./components/BrowserPanel').then(m => ({ default: m.BrowserPanel })));
14
  const LibraryPanel = lazy(() => import('./components/LibraryPanel').then(m => ({ default: m.LibraryPanel })));
 
24
  useEffect(() => { if (toastMsg) { const t = setTimeout(() => setToastMsg(null), 3000); return () => clearTimeout(t); } }, [toastMsg]);
25
  useEffect(() => { appWindow.setAlwaysOnTop(isAlwaysOnTop).catch(() => {}); }, [isAlwaysOnTop]);
26
 
27
+ useEffect(() => { const unlisten = listen<any>('board://image_added', (event) => { const data = event.payload; if (!data || !data.url) return; const img = new Image(); const add = (w0:number,h0:number) => { const ratio = w0 / h0; const w = Math.min(400, w0); const h = w / ratio; setImages(prev => [...prev, { id: data.id || crypto.randomUUID(), url: data.url, x: (-pan.x + window.innerWidth / 3 + Math.random() * 80) / zoom, y: (-pan.y + window.innerHeight / 3 + Math.random() * 80) / zoom, width: w, height: h, aspectRatio: ratio }]); setToastMsg('✓ Image added'); }; img.onload = () => add(img.width,img.height); img.onerror = () => add(data.width || 300, data.height || 200); img.src = data.url; }); return () => { unlisten.then(fn => fn()); }; }, [pan, zoom, setImages]);
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ const doScreenCapture = async () => { setIsCapturing(true); setToastMsg('Capturing...'); try { await appWindow.minimize(); await new Promise(r => setTimeout(r, 300)); const dataUrl = await invoke<string>('screen_capture_full'); await appWindow.unminimize(); await appWindow.setFocus(); if (dataUrl) { const img = new Image(); img.onload = () => { const ratio = img.width / img.height; const w = Math.min(800, img.width); const h = w / ratio; setImages(prev => [...prev, { id: crypto.randomUUID(), url: dataUrl, x: (-pan.x + window.innerWidth/2 - w/2)/zoom, y: (-pan.y + window.innerHeight/2 - h/2)/zoom, width: w, height: h, aspectRatio: ratio }]); setToastMsg('Screenshot captured!'); }; img.src = dataUrl; } } catch { setToastMsg('Capture failed'); await appWindow.unminimize().catch(() => {}); } setIsCapturing(false); };
 
 
 
 
30
 
31
  useEffect(() => {
32
  const onKeyDown = (e: KeyboardEvent) => {
33
+ if (isEditableTarget(e.target)) return;
34
+ const settings = loadShortcutSettings();
35
+ const run = (id: Parameters<typeof matchesShortcut>[0]) => matchesShortcut(id, e, settings);
36
+ if (run('edit.undo')) { e.preventDefault(); undo(); return; }
37
+ if (run('edit.redo')) { e.preventDefault(); redo(); return; }
38
+ if (run('file.save')) { e.preventDefault(); setToastMsg('Saved'); return; }
39
+ if (run('capture.screen')) { e.preventDefault(); doScreenCapture(); return; }
40
+ if (run('view.focus')) { if (focusedImageId) setFocusedImageId(null); else if (selectedNodeIds.length > 0) setFocusedImageId(selectedNodeIds[0]); return; }
41
+ if (run('view.valueMirror') && selectedNodeIds.length > 0) { setValueMirrorIds(prev => { const all = selectedNodeIds.every(id => prev.includes(id)); return all ? prev.filter(id => !selectedNodeIds.includes(id)) : [...new Set([...prev, ...selectedNodeIds])]; }); return; }
42
+ if (run('selection.flipH') && selectedNodeIds.length > 0) { setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isFlippedH: !i.isFlippedH } : i)); return; }
43
+ if (run('view.zoomLens')) { setIsZoomLensActive(true); return; }
44
+ if (run('selection.duplicate')) { e.preventDefault(); if (selectedNodeIds.length > 0) { const dupes = images.filter(i => selectedNodeIds.includes(i.id)).map(i => ({ ...i, id: crypto.randomUUID(), x: i.x + 30, y: i.y + 30 })); setImages(prev => [...prev, ...dupes]); setSelectedNodeIds(dupes.map(d => d.id)); setToastMsg(`Duplicated ${dupes.length}`); } return; }
45
+ if (run('selection.group')) { e.preventDefault(); if (selectedNodeIds.length > 1) { const gid = crypto.randomUUID(); setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, groupId: gid } : i)); setTextNotes(prev => prev.map(n => selectedNodeIds.includes(n.id) ? { ...n, groupId: gid } : n)); setToastMsg('Grouped'); } return; }
46
+ if (run('selection.ungroup')) { e.preventDefault(); setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, groupId: undefined } : i)); setTextNotes(prev => prev.map(n => selectedNodeIds.includes(n.id) ? { ...n, groupId: undefined } : n)); setToastMsg('Ungrouped'); return; }
47
  if (focusedImageId && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) { const idx = images.findIndex(i => i.id === focusedImageId); if (idx >= 0) { const next = e.key === 'ArrowRight' ? (idx + 1) % images.length : (idx - 1 + images.length) % images.length; setFocusedImageId(images[next].id); setSelectedNodeIds([images[next].id]); } return; }
48
+ if (run('panel.browser')) { const now = Date.now(); if (now - lastBPress.current < 300) { setIsWhisperBrowser(true); setIsBrowserOpen(true); } else { setIsBrowserOpen(p => !p); setIsWhisperBrowser(false); } lastBPress.current = now; return; }
49
+ if (run('panel.library')) { setIsLibraryOpen(p => !p); return; }
50
+ if (run('panel.settings')) { e.preventDefault(); setIsSettingsOpen(p => !p); return; }
51
+ if (run('tool.annotate')) { setIsAnnotationMode(p => !p); return; }
52
+ if (run('tool.globalDesaturate')) { setGlobalDesaturate(p => !p); return; }
53
+ if (run('selection.desaturate')) { if (selectedNodeIds.length > 0) setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isDesaturated: !i.isDesaturated } : i)); return; }
54
+ if (run('window.alwaysOnTop')) { setIsAlwaysOnTop(p => !p); return; }
55
+ if (run('panel.closeAll')) { setIsBrowserOpen(false); setIsLibraryOpen(false); setIsSettingsOpen(false); setSelectedNodeIds([]); setFocusedImageId(null); return; }
56
+ if (run('selection.delete')) { if (selectedNodeIds.length > 0) { setImages(prev => prev.filter(i => !selectedNodeIds.includes(i.id))); setTextNotes(prev => prev.filter(n => !selectedNodeIds.includes(n.id))); setSelectedNodeIds([]); } return; }
57
+ if (run('selection.selectAll')) { e.preventDefault(); setSelectedNodeIds(images.map(i => i.id)); return; }
58
+ if (run('view.fitAll')) { e.preventDefault(); if (images.length === 0) { setZoom(1); setPan({ x: 0, y: 0 }); return; } let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; images.forEach(i => { minX = Math.min(minX, i.x); minY = Math.min(minY, i.y); maxX = Math.max(maxX, i.x + i.width); maxY = Math.max(maxY, i.y + i.height); }); const pad = 100, w = maxX - minX + pad*2, h = maxY - minY + pad*2; const nz = Math.max(0.1, Math.min(window.innerWidth/w, window.innerHeight/h)); setZoom(nz); setPan({ x: -((minX+maxX)/2)*nz + window.innerWidth/2, y: -((minY+maxY)/2)*nz + window.innerHeight/2 }); return; }
59
+ if (run('view.zoom100')) { e.preventDefault(); setZoom(1); return; }
60
  };
61
+ const onKeyUp = (e: KeyboardEvent) => { if (matchesShortcut('view.zoomLens', e)) setIsZoomLensActive(false); };
62
  window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp);
63
  return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); };
64
  }, [selectedNodeIds, images, pan, zoom, focusedImageId, undo, redo]);