feat: snappier timeline — instant discard, progressive load, restore preview origin
- Discard yanks photos from the grid optimistically (cache strip + active cursor advance) instead of waiting for the mutation round-trip; wired through the X hotkey, RightSidebar bulk discard, LeftSidebar discard drop, and DiscardActionBar restore/delete. - usePhotosQuery resolves on the first 500-photo page and streams the remaining pages into the cache in the background, so the first thumbnails paint immediately on large libraries. - Closing preview restores the photo it was opened on (snapshot ref in PreviewView, written directly to the store) and Timeline scrolls that row back into view. Escape is handled on the dialog with stopPropagation so Timeline's window-level Esc handler doesn't wipe the restored selection. - Preview overlay bumped to z-[1000] so it covers Leaflet map tiles, and the right sidebar no longer collapses during preview — both fix visible layout shifts on close. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -321,6 +321,11 @@ export function Timeline() {
|
||||
// they share one cache entry, regardless of filter state.
|
||||
const { data: photos = [], isLoading } = usePhotosQuery()
|
||||
|
||||
// Tracks the previous viewMode so the "preview just closed" scroll
|
||||
// effect (defined further down, after photoRows) only fires on the
|
||||
// actual transition rather than every items[] recomputation.
|
||||
const prevViewModeRef = useRef(viewMode)
|
||||
|
||||
// Auto-focus the first photo on initial grid load so arrow-key nav
|
||||
// works immediately without a pre-click. Only fires when there's no
|
||||
// current active photo — we never clobber the user's selection or
|
||||
@@ -459,6 +464,44 @@ export function Timeline() {
|
||||
return map
|
||||
}, [items])
|
||||
|
||||
// After the preview closes, scroll the photo it was originally
|
||||
// opened on back into view. The store's closePreview already
|
||||
// restored activePhotoId to that origin id; we just need to make
|
||||
// sure it's actually visible in the scroll viewport. Guarded by
|
||||
// prevViewModeRef so this only fires on the actual preview→grid
|
||||
// transition, not every time photoRows recomputes.
|
||||
useEffect(() => {
|
||||
const prev = prevViewModeRef.current
|
||||
prevViewModeRef.current = viewMode
|
||||
if (prev !== 'preview' || viewMode !== 'grid') return
|
||||
if (!activePhotoId) return
|
||||
let rowIdx = -1
|
||||
for (let r = 0; r < photoRows.length; r++) {
|
||||
if (photoRows[r].cells.some((c) => c.photo.id === activePhotoId)) {
|
||||
rowIdx = r
|
||||
break
|
||||
}
|
||||
}
|
||||
if (rowIdx < 0) return
|
||||
const itemIdx = photoRowItemIndex[rowIdx]
|
||||
const scrollEl = parentRef.current
|
||||
if (itemIdx === undefined || !scrollEl) return
|
||||
let rowTop = 0
|
||||
for (let i = 0; i < itemIdx; i++) rowTop += items[i].height
|
||||
const rowHeight = items[itemIdx].height
|
||||
const viewTop = scrollEl.scrollTop
|
||||
const viewBottom = viewTop + scrollEl.clientHeight
|
||||
if (rowTop >= viewTop && rowTop + rowHeight <= viewBottom) return
|
||||
// Center the row in the viewport — the user is returning to a
|
||||
// specific photo, not resuming a scroll, so context above and
|
||||
// below is what they want.
|
||||
const target = Math.max(
|
||||
0,
|
||||
rowTop - scrollEl.clientHeight / 2 + rowHeight / 2
|
||||
)
|
||||
scrollEl.scrollTo({ top: target })
|
||||
}, [viewMode, activePhotoId, photoRows, photoRowItemIndex, items])
|
||||
|
||||
// Flat visible-order id sequence — exactly the order the user reads
|
||||
// off the grid (top-to-bottom, left-to-right within each row).
|
||||
// Includes duplicates from tag-grouping; landing on the same photo's
|
||||
|
||||
Reference in New Issue
Block a user