feat: wire culling keyboard shortcuts to photo mutations
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, string> = {
|
||||
'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 })
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user