diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index 68a707b..20f4a40 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -2216,6 +2216,20 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { setView('preview'); }; + // The frame next to this one, on the shelf's own order: the arrows at the + // edges of the stage and the swipe in the mobile preview both walk the list the + // reader is looking at, so what is drawn after the step is what was beside it. + const stepFrame = useCallback( + (d: number) => { + if (!frame) return; + const i = listed.findIndex((p) => p.id === frame.id); + if (i < 0) return; + const next = listed[i + d]; + if (next) setUp(next.id); + }, + [frame?.id, listed], + ); + // A frame's score: click a star to give it, click the star it already has to // take the score back. The catalogue holds it, so it is the same score the next // visit reads — and the list on screen is told, rather than read back whole for @@ -2469,6 +2483,9 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { const gradedTile = frame && edited.has(frame.id) ? urls[frame.id] ?? null : null; const stageSrc = gradedTile ?? (stageUrl && stageFor === frame?.id ? stageUrl : frame ? urls[frame.id] ?? null : null); const stageTurned = useTurned(stageSrc, frame?.rot ?? 0); + // Where the open frame stands on the shelf, so the arrows at the stage's edges + // know which of them has nothing to go to. + const frameAt = listed.findIndex((p) => p.id === frame?.id); @@ -3185,6 +3202,50 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { touchAction: 'none', }} > + {/* The frame's neighbours, at the edges the eye already + reads as "there is more this way". Both are drawn + whenever the shelf holds another frame, and the one + with nothing to go to is dimmed rather than taken + away, so the pair does not jump about. They are the + stage's own overlay: a press on one must not reach + the zoom, the pan or the open under it. */} + {listed.length > 1 && frameAt >= 0 ? ( + <> + {( + [ + { d: -1, key: 'lib-prev', edge: { left: '10px' }, label: t('lib.prev'), icon: 'M10 2 4 8l6 6' }, + { d: 1, key: 'lib-next', edge: { right: '10px' }, label: t('lib.next'), icon: 'M6 2l6 6-6 6' }, + ] as const + ).map((a) => { + const at = frameAt + a.d; + const off = at < 0 || at >= listed.length; + return ( + + ); + })} + + ) : null} {stageTurned ? ( .lib-noimg { flex: 1 1 auto; min-height: 0; aspect-ratio: auto; } .lib-root .adm-stage > .adm-meta, .lib-root .adm-stage > .chip-row { flex: 0 0 auto; } diff --git a/docker/frontend/src/ui/MobileLibrary.tsx b/docker/frontend/src/ui/MobileLibrary.tsx index 082fb47..a15a0e1 100644 --- a/docker/frontend/src/ui/MobileLibrary.tsx +++ b/docker/frontend/src/ui/MobileLibrary.tsx @@ -584,6 +584,18 @@ export function MobileLibrary({ return groups; }, [visiblePhotos, lang]); + // The frame a swipe lands on: the list the reader is looking at is the shelf, + // so the step goes the way that list is read and stops at its ends. + const stepPhoto = (d: number) => { + if (!selectedPhoto) return; + const i = searchResults.findIndex((p) => p.id === selectedPhoto.id); + const next = i < 0 ? undefined : searchResults[i + d]; + if (next) setSelectedPhoto(next); + }; + // Where the finger went down: only the distance it travelled matters, and only + // once it has lifted. + const swipeFrom = useRef<{ x: number; y: number } | null>(null); + // Filtered photos in Search tab const searchResults = useMemo(() => { let list = photos; @@ -2243,7 +2255,28 @@ export function MobileLibrary({ -
+ {/* A finger across the picture is the shelf's own step: left goes the way + the list is read, right goes back. A drag that wandered more up and + down than sideways is a scroll of a page that cannot scroll, and is + left alone — and so is a tap, which is a press and a lift in place. */} +
{ + const t = e.touches[0]; + swipeFrom.current = { x: t.clientX, y: t.clientY }; + }} + onTouchEnd={(e) => { + const start = swipeFrom.current; + swipeFrom.current = null; + if (!start) return; + const t = e.changedTouches[0]; + const dx = t.clientX - start.x; + const dy = t.clientY - start.y; + if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy)) return; + stepPhoto(dx < 0 ? 1 : -1); + }} + >