|
|
|
@@ -24,24 +24,21 @@ export default function Gallery({
|
|
|
|
|
}) {
|
|
|
|
|
const selecting = selected && selected.size > 0
|
|
|
|
|
const containerRef = useRef(null)
|
|
|
|
|
const [cursor, setCursor] = useState(-1)
|
|
|
|
|
// The outline only shows once the keyboard (or the viewer) was actually
|
|
|
|
|
// used — a fresh page must not present a photo as pre-marked.
|
|
|
|
|
const [cursorVisible, setCursorVisible] = useState(false)
|
|
|
|
|
// The cursor is always a valid index; `revealed` says whether the user has
|
|
|
|
|
// interacted yet. The first key press reveals the cursor where it is
|
|
|
|
|
// (nothing pre-marked on a fresh page, first arrow doesn't skip a photo).
|
|
|
|
|
const [cursor, setCursor] = useState(0)
|
|
|
|
|
const [revealed, setRevealed] = useState(false)
|
|
|
|
|
const live = useRef({})
|
|
|
|
|
live.current = { cursor, photos, onOpen, onToggleSelect, actions }
|
|
|
|
|
live.current = { cursor, revealed, photos, onOpen, onToggleSelect, actions }
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (externalIndex >= 0) {
|
|
|
|
|
setCursor(externalIndex)
|
|
|
|
|
setCursorVisible(true)
|
|
|
|
|
setRevealed(true)
|
|
|
|
|
}
|
|
|
|
|
}, [externalIndex])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (keyboard && cursor < 0 && photos.length > 0) setCursor(0)
|
|
|
|
|
}, [keyboard, cursor, photos.length])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (cursor >= photos.length) setCursor(Math.max(0, photos.length - 1))
|
|
|
|
|
}, [cursor, photos.length])
|
|
|
|
@@ -58,37 +55,48 @@ export default function Gallery({
|
|
|
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return
|
|
|
|
|
const s = live.current
|
|
|
|
|
if (s.photos.length === 0) return
|
|
|
|
|
const photo = s.cursor >= 0 ? s.photos[s.cursor] : null
|
|
|
|
|
const photo = s.photos[s.cursor]
|
|
|
|
|
const el = containerRef.current
|
|
|
|
|
const columns = el ? getComputedStyle(el).gridTemplateColumns.split(' ').length : 1
|
|
|
|
|
const count = s.photos.length
|
|
|
|
|
let next = null
|
|
|
|
|
if (e.key === 'ArrowRight') next = Math.min(count - 1, Math.max(0, s.cursor + 1))
|
|
|
|
|
if (e.key === 'ArrowRight') next = Math.min(count - 1, s.cursor + 1)
|
|
|
|
|
else if (e.key === 'ArrowLeft') next = Math.max(0, s.cursor - 1)
|
|
|
|
|
else if (e.key === 'ArrowDown') next = s.cursor < 0 ? 0 : Math.min(count - 1, s.cursor + columns)
|
|
|
|
|
else if (e.key === 'ArrowUp') next = s.cursor < 0 ? 0 : Math.max(0, s.cursor - columns)
|
|
|
|
|
else if (e.key === 'ArrowDown') next = Math.min(count - 1, s.cursor + columns)
|
|
|
|
|
else if (e.key === 'ArrowUp') next = Math.max(0, s.cursor - columns)
|
|
|
|
|
else if (e.key === ' ') {
|
|
|
|
|
if (photo) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
setRevealed(true)
|
|
|
|
|
s.onOpen?.(s.cursor)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
} else if (e.key === 'Enter') {
|
|
|
|
|
if (photo && s.onToggleSelect) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
setRevealed(true)
|
|
|
|
|
s.onToggleSelect(photo.id)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
} else {
|
|
|
|
|
const key = e.key.toLowerCase()
|
|
|
|
|
const action = s.actions?.find((a) => a.keys.includes(key))
|
|
|
|
|
if (action && photo) action.run(photo, key)
|
|
|
|
|
if (action && photo) {
|
|
|
|
|
setRevealed(true)
|
|
|
|
|
action.run(photo, key)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
setRevealed(true)
|
|
|
|
|
// The first arrow press only reveals the cursor; movement starts with
|
|
|
|
|
// the second.
|
|
|
|
|
if (s.revealed) {
|
|
|
|
|
setCursor(next)
|
|
|
|
|
setCursorVisible(true)
|
|
|
|
|
el?.children[next]?.scrollIntoView({ block: 'nearest' })
|
|
|
|
|
} else {
|
|
|
|
|
el?.children[s.cursor]?.scrollIntoView({ block: 'nearest' })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener('keydown', handler)
|
|
|
|
|
return () => window.removeEventListener('keydown', handler)
|
|
|
|
@@ -101,7 +109,7 @@ export default function Gallery({
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={p.id}
|
|
|
|
|
className={`g-item${isSelected ? ' selected' : ''}${cursorVisible && index === cursor ? ' focused' : ''}`}
|
|
|
|
|
className={`g-item${isSelected ? ' selected' : ''}${revealed && index === cursor ? ' focused' : ''}`}
|
|
|
|
|
onClick={() => onOpen && onOpen(index)}
|
|
|
|
|
>
|
|
|
|
|
<img src={imgUrl(p, 'thumb')} loading="lazy" alt={p.filename} />
|
|
|
|
|