From 892e8e1da4f75df578b65ce71f61ffc8403579d6 Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 7 Apr 2026 21:44:47 +0200 Subject: [PATCH] feat: wire culling keyboard shortcuts to photo mutations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the console.log stubs in useKeyboardShortcuts with real PATCH /photos/{id} mutations against the active photo, so 1-5 / 0 / P / X / U / 6-9 actually rate, flag, and color-label photos. Mutations invalidate both the photo detail query and the timeline list query, so the RightSidebar and grid update immediately. Shortcuts now work in BOTH grid and loupe modes (the previous { enabled: isGrid } gate is removed) so the user can cull while browsing in the loupe — the Lightroom workflow. Color labels 6-9 are wired to red/orange/yellow/green per spec §6.4. Active photo id is read fresh via usePhotoStore.getState() inside each handler, so we don't re-bind hotkeys on every selection change. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/hooks/useKeyboardShortcuts.ts | 88 ++++++++++++++++------ 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/frontend/src/hooks/useKeyboardShortcuts.ts b/frontend/src/hooks/useKeyboardShortcuts.ts index 4cdd97e..39c9e69 100644 --- a/frontend/src/hooks/useKeyboardShortcuts.ts +++ b/frontend/src/hooks/useKeyboardShortcuts.ts @@ -1,5 +1,7 @@ import { useHotkeys } from 'react-hotkeys-hook' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { usePhotoStore } from '../store/photoStore' +import { photos as photosApi } from '../services/api' interface KeyboardShortcutsProps { onToggleLeftSidebar: () => void @@ -8,17 +10,50 @@ interface KeyboardShortcutsProps { getFirstPhotoId?: () => string | null } +interface PhotoUpdate { + rating?: number + is_picked?: boolean + is_rejected?: boolean + color_label?: string | null +} + +// Spec §6.4 number-key color labels. +const COLOR_LABELS: Record = { + '6': 'red', + '7': 'orange', + '8': 'yellow', + '9': 'green', +} + export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { const { onToggleLeftSidebar, onToggleRightSidebar, getFirstPhotoId } = props const viewMode = usePhotoStore((s) => s.viewMode) - const activePhotoId = usePhotoStore((s) => s.activePhotoId) const openLoupe = usePhotoStore((s) => s.openLoupe) const closeLoupe = usePhotoStore((s) => s.closeLoupe) const isGrid = viewMode === 'grid' const isLoupe = viewMode === 'loupe' + // Photo mutation shared by every culling shortcut. Reads the active photo + // id from the store at fire time so the closure stays fresh without forcing + // hotkey re-binding on every selection change. + const queryClient = useQueryClient() + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: PhotoUpdate }) => + photosApi.update(id, data), + onSuccess: (_data, vars) => { + queryClient.invalidateQueries({ queryKey: ['photo', vars.id] }) + queryClient.invalidateQueries({ queryKey: ['photos'] }) + }, + }) + + const updateActive = (data: PhotoUpdate) => { + const id = usePhotoStore.getState().activePhotoId + if (!id) return + updateMutation.mutate({ id, data }) + } + // Toggle sidebars (allowed in both modes; right sidebar is hidden in loupe // by App-level CSS so toggling it is effectively grid-only.) useHotkeys('tab', (e) => { @@ -31,15 +66,14 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { onToggleRightSidebar() }) - // Grid view: G always returns to grid (closes loupe if open). + // G always returns to grid (closes loupe if open). useHotkeys('g', () => { closeLoupe() }) - // Loupe view: E toggles loupe (open from grid, close from loupe). - // Enter also opens loupe from grid. + // E toggles loupe (open from grid, close from loupe). Enter opens from grid. const openLoupeFromGrid = () => { - const id = activePhotoId ?? getFirstPhotoId?.() ?? null + const id = usePhotoStore.getState().activePhotoId ?? getFirstPhotoId?.() ?? null if (id) openLoupe(id) } @@ -52,7 +86,7 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { openLoupeFromGrid() } }, - [isLoupe, activePhotoId, getFirstPhotoId] + [isLoupe, getFirstPhotoId] ) useHotkeys( @@ -64,33 +98,37 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { } }, { enabled: isGrid }, - [isGrid, activePhotoId, getFirstPhotoId] + [isGrid, getFirstPhotoId] ) - // Rating shortcuts (grid only — stubs from a different phase) - useHotkeys( - '1,2,3,4,5', - (_e, handler) => { - const rating = parseInt(handler.keys![0]) - console.log('Set rating:', rating) - }, - { enabled: isGrid } - ) + // ── Culling shortcuts (work in both grid and loupe) ────────────────────── + + // Star rating: 1-5 set, 0 clears. + useHotkeys('1,2,3,4,5', (_e, handler) => { + const rating = parseInt(handler.keys![0]) + if (Number.isFinite(rating)) updateActive({ rating }) + }) useHotkeys('0', () => { - console.log('Remove rating') - }, { enabled: isGrid }) + updateActive({ rating: 0 }) + }) - // Flag shortcuts (grid only) + // Pick / reject / unflag. useHotkeys('p', () => { - console.log('Pick photo') - }, { enabled: isGrid }) + updateActive({ is_picked: true, is_rejected: false }) + }) useHotkeys('x', () => { - console.log('Reject photo') - }, { enabled: isGrid }) + updateActive({ is_rejected: true, is_picked: false }) + }) useHotkeys('u', () => { - console.log('Unflag photo') - }, { enabled: isGrid }) + updateActive({ is_picked: false, is_rejected: false }) + }) + + // Color labels 6-9 (red/orange/yellow/green per spec §6.4). + useHotkeys('6,7,8,9', (_e, handler) => { + const label = COLOR_LABELS[handler.keys![0]] + if (label) updateActive({ color_label: label }) + }) }