fix: visible-order range selection + preview-after-Space sequence
Two related bugs around visible vs API order. 1. Multi-select range selection (Shift+Click, Shift+Arrow): - The previous selectRange walked the API photos array and only ADDED to the existing selection, never replacing or shrinking. So Shift+clicking to the left often "did nothing" (already-selected ids skipped) and the selection never matched the user's intended range. - Replace with a store action that walks visiblePhotoIds (the visual row-major sequence Timeline already publishes), de-dupes ids (tag-grouped views can repeat photos), and REPLACES the selection. - Track the range anchor as rangeStartId (a photo id) instead of an index so it survives filter changes and works correctly when API index != visual position. - Drop the now-redundant lastSelectedIndex / globalIndex plumbing from selectPhoto / togglePhotoSelection — call sites simplify to pass just the photo id. 2. Preview navigation after pressing Space: - The Space hotkey path called openPreview(id) without a sequence and relied on the store's fallback to whatever Timeline most recently published. Make it explicit: read visiblePhotoIds from the store snapshot at fire time and pass it through. Same effect in the happy case but eliminates any subtle publisher timing question. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -166,10 +166,9 @@ export function Timeline() {
|
||||
const {
|
||||
selectedPhotos,
|
||||
activePhotoId,
|
||||
lastSelectedIndex,
|
||||
rangeStartIndex,
|
||||
selectPhoto,
|
||||
togglePhotoSelection,
|
||||
selectRange,
|
||||
clearSelection,
|
||||
openPreview,
|
||||
} = usePhotoStore()
|
||||
@@ -220,19 +219,6 @@ export function Timeline() {
|
||||
return result
|
||||
}, [items])
|
||||
|
||||
// Range-selection helper. Operates on the global photos array, not on
|
||||
// virtualizer items.
|
||||
const selectRange = (endIndex: number) => {
|
||||
const startIndex = rangeStartIndex ?? lastSelectedIndex ?? 0
|
||||
const minIndex = Math.min(startIndex, endIndex)
|
||||
const maxIndex = Math.max(startIndex, endIndex)
|
||||
for (let i = minIndex; i <= maxIndex; i++) {
|
||||
if (i < photos.length && !selectedPhotos.includes(photos[i].id)) {
|
||||
togglePhotoSelection(photos[i].id, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual scrolling setup with per-item heights.
|
||||
const virtualizer = useVirtualizer({
|
||||
count: items.length,
|
||||
@@ -384,9 +370,9 @@ export function Timeline() {
|
||||
const dest = photoRows[nextRow]?.cells[nextCol]
|
||||
if (!dest) return
|
||||
if (e.shiftKey) {
|
||||
selectRange(dest.globalIndex)
|
||||
selectRange(dest.photo.id)
|
||||
} else {
|
||||
selectPhoto(dest.photo.id, dest.globalIndex)
|
||||
selectPhoto(dest.photo.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,9 +396,9 @@ export function Timeline() {
|
||||
case 'a':
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault()
|
||||
photos.forEach((photo, index) => {
|
||||
photos.forEach((photo) => {
|
||||
if (!selectedPhotos.includes(photo.id)) {
|
||||
togglePhotoSelection(photo.id, index)
|
||||
togglePhotoSelection(photo.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -509,7 +495,7 @@ export function Timeline() {
|
||||
}}
|
||||
>
|
||||
<div className="flex" style={{ gap: `${GAP}px` }}>
|
||||
{item.cells.map(({ photo, globalIndex }) => (
|
||||
{item.cells.map(({ photo }) => (
|
||||
<PhotoThumbnail
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
@@ -517,12 +503,12 @@ export function Timeline() {
|
||||
isSelected={selectedPhotos.includes(photo.id)}
|
||||
isInActiveHeap={activeHeapMembers.has(photo.id)}
|
||||
onClick={(e) => {
|
||||
if (e.shiftKey && lastSelectedIndex !== null) {
|
||||
selectRange(globalIndex)
|
||||
if (e.shiftKey) {
|
||||
selectRange(photo.id)
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
togglePhotoSelection(photo.id, globalIndex)
|
||||
togglePhotoSelection(photo.id)
|
||||
} else {
|
||||
selectPhoto(photo.id, globalIndex)
|
||||
selectPhoto(photo.id)
|
||||
}
|
||||
}}
|
||||
onDoubleClick={() => openPreview(photo.id, visibleSequence)}
|
||||
|
||||
Reference in New Issue
Block a user