First key press (arrow or action) targets photo 1 and shows the cursor without moving it; movement starts with the second arrow. Replaces the -1 sentinel plus visibility flag double-state.
This commit is contained in:
@@ -112,8 +112,8 @@ The prebuilt image is at `git.draic.info/nils/photos` (single image contains
|
|||||||
`server`, `worker`, and the built frontend). To build your own instead:
|
`server`, `worker`, and the built frontend). To build your own instead:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker build -t registry.example.com/you/photos:0.5.0 .
|
docker build -t registry.example.com/you/photos:0.5.1 .
|
||||||
docker push registry.example.com/you/photos:0.5.0
|
docker push registry.example.com/you/photos:0.5.1
|
||||||
```
|
```
|
||||||
|
|
||||||
Install the chart, pointing it at your existing Postgres and S3:
|
Install the chart, pointing it at your existing Postgres and S3:
|
||||||
@@ -121,7 +121,7 @@ Install the chart, pointing it at your existing Postgres and S3:
|
|||||||
```sh
|
```sh
|
||||||
helm install photos deploy/chart \
|
helm install photos deploy/chart \
|
||||||
--set image.repository=git.draic.info/nils/photos \
|
--set image.repository=git.draic.info/nils/photos \
|
||||||
--set image.tag=0.5.0 \
|
--set image.tag=0.5.1 \
|
||||||
--set publicUrl=https://photos.example.com \
|
--set publicUrl=https://photos.example.com \
|
||||||
--set ingress.host=photos.example.com \
|
--set ingress.host=photos.example.com \
|
||||||
--set config.oidcIssuer=https://auth.example.com \
|
--set config.oidcIssuer=https://auth.example.com \
|
||||||
|
|||||||
@@ -24,24 +24,21 @@ export default function Gallery({
|
|||||||
}) {
|
}) {
|
||||||
const selecting = selected && selected.size > 0
|
const selecting = selected && selected.size > 0
|
||||||
const containerRef = useRef(null)
|
const containerRef = useRef(null)
|
||||||
const [cursor, setCursor] = useState(-1)
|
// The cursor is always a valid index; `revealed` says whether the user has
|
||||||
// The outline only shows once the keyboard (or the viewer) was actually
|
// interacted yet. The first key press reveals the cursor where it is
|
||||||
// used — a fresh page must not present a photo as pre-marked.
|
// (nothing pre-marked on a fresh page, first arrow doesn't skip a photo).
|
||||||
const [cursorVisible, setCursorVisible] = useState(false)
|
const [cursor, setCursor] = useState(0)
|
||||||
|
const [revealed, setRevealed] = useState(false)
|
||||||
const live = useRef({})
|
const live = useRef({})
|
||||||
live.current = { cursor, photos, onOpen, onToggleSelect, actions }
|
live.current = { cursor, revealed, photos, onOpen, onToggleSelect, actions }
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (externalIndex >= 0) {
|
if (externalIndex >= 0) {
|
||||||
setCursor(externalIndex)
|
setCursor(externalIndex)
|
||||||
setCursorVisible(true)
|
setRevealed(true)
|
||||||
}
|
}
|
||||||
}, [externalIndex])
|
}, [externalIndex])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (keyboard && cursor < 0 && photos.length > 0) setCursor(0)
|
|
||||||
}, [keyboard, cursor, photos.length])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cursor >= photos.length) setCursor(Math.max(0, photos.length - 1))
|
if (cursor >= photos.length) setCursor(Math.max(0, photos.length - 1))
|
||||||
}, [cursor, photos.length])
|
}, [cursor, photos.length])
|
||||||
@@ -58,37 +55,48 @@ export default function Gallery({
|
|||||||
if (e.metaKey || e.ctrlKey || e.altKey) return
|
if (e.metaKey || e.ctrlKey || e.altKey) return
|
||||||
const s = live.current
|
const s = live.current
|
||||||
if (s.photos.length === 0) return
|
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 el = containerRef.current
|
||||||
const columns = el ? getComputedStyle(el).gridTemplateColumns.split(' ').length : 1
|
const columns = el ? getComputedStyle(el).gridTemplateColumns.split(' ').length : 1
|
||||||
const count = s.photos.length
|
const count = s.photos.length
|
||||||
let next = null
|
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 === '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 === 'ArrowDown') next = 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 === 'ArrowUp') next = Math.max(0, s.cursor - columns)
|
||||||
else if (e.key === ' ') {
|
else if (e.key === ' ') {
|
||||||
if (photo) {
|
if (photo) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
setRevealed(true)
|
||||||
s.onOpen?.(s.cursor)
|
s.onOpen?.(s.cursor)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
if (photo && s.onToggleSelect) {
|
if (photo && s.onToggleSelect) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
setRevealed(true)
|
||||||
s.onToggleSelect(photo.id)
|
s.onToggleSelect(photo.id)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
const key = e.key.toLowerCase()
|
const key = e.key.toLowerCase()
|
||||||
const action = s.actions?.find((a) => a.keys.includes(key))
|
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
|
return
|
||||||
}
|
}
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
setRevealed(true)
|
||||||
|
// The first arrow press only reveals the cursor; movement starts with
|
||||||
|
// the second.
|
||||||
|
if (s.revealed) {
|
||||||
setCursor(next)
|
setCursor(next)
|
||||||
setCursorVisible(true)
|
|
||||||
el?.children[next]?.scrollIntoView({ block: 'nearest' })
|
el?.children[next]?.scrollIntoView({ block: 'nearest' })
|
||||||
|
} else {
|
||||||
|
el?.children[s.cursor]?.scrollIntoView({ block: 'nearest' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener('keydown', handler)
|
window.addEventListener('keydown', handler)
|
||||||
return () => window.removeEventListener('keydown', handler)
|
return () => window.removeEventListener('keydown', handler)
|
||||||
@@ -101,7 +109,7 @@ export default function Gallery({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={p.id}
|
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)}
|
onClick={() => onOpen && onOpen(index)}
|
||||||
>
|
>
|
||||||
<img src={imgUrl(p, 'thumb')} loading="lazy" alt={p.filename} />
|
<img src={imgUrl(p, 'thumb')} loading="lazy" alt={p.filename} />
|
||||||
|
|||||||
Reference in New Issue
Block a user