feat(ui): modal prev/next walks the current gallery filter (#1322)
CI / lint (push) Failing after 2s
CI / frontend-build (push) Failing after 17s
CI / backend-lint-and-test (push) Failing after 27s
CI / integration (push) Successful in 3m43s

The image modal cycled GLOBAL neighbours; now the gallery hands it a snapshot of
the currently-filtered, ordered id list so prev/next moves through exactly what
you're viewing — the filtered-playlist behaviour lost in the ImageRepo→FC move.
Generalized the modal store's post-scoped cycle into a `playlistIds` playlist
reused by both GalleryView and PostCard (falls back to global neighbours when no
playlist is passed, e.g. Explore's "open full viewer").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-06 19:02:38 -04:00
parent bae077e323
commit 3bf41ecac3
3 changed files with 38 additions and 34 deletions
+2 -2
View File
@@ -200,13 +200,13 @@ async function fullImageIds () {
} }
async function openModal (imageId) { async function openModal (imageId) {
modal.open(imageId, { postImageIds: await fullImageIds() }) modal.open(imageId, { playlistIds: await fullImageIds() })
} }
async function openModalAtMore () { async function openModalAtMore () {
const ids = await fullImageIds() const ids = await fullImageIds()
const first = ids[visibleCount.value] ?? ids[0] const first = ids[visibleCount.value] ?? ids[0]
if (first != null) modal.open(first, { postImageIds: ids }) if (first != null) modal.open(first, { playlistIds: ids })
} }
// --- description "Show more" (text-only, in place, only when truncated) ---- // --- description "Show more" (text-only, in place, only when truncated) ----
+32 -31
View File
@@ -17,12 +17,13 @@ export const useModalStore = defineStore('modal', () => {
// chip rail. Audit 2026-06-02. // chip rail. Audit 2026-06-02.
const inflight = useInflightToken() const inflight = useInflightToken()
// Post-scoped cycle. When set, prev/next cycles within this array // Scoped playlist. When set, prev/next cycles within THIS ordered id array
// (used by PostCard image clicks — the modal is scoped to that post's // the current gallery filter (GalleryView) or a post's images (PostCard) — so
// images). When null, prev/next falls back to current.value.neighbors // the modal walks exactly what the user was looking at, not a global order.
// (the gallery-store-driven /api/gallery/image/<id> neighbors). // When null, prev/next falls back to current.value.neighbors (the
const postImageIds = ref(null) // /api/gallery/image/<id> global neighbours).
const postImageIndex = ref(0) const playlistIds = ref(null)
const playlistIndex = ref(0)
async function open (id, opts = {}) { async function open (id, opts = {}) {
// Cancel any in-flight tag mutation or reloadTags from the // Cancel any in-flight tag mutation or reloadTags from the
@@ -32,13 +33,13 @@ export const useModalStore = defineStore('modal', () => {
current.value = null // cleared upfront so it stays null on error current.value = null // cleared upfront so it stays null on error
// Update post-scoped state if caller passed it; otherwise clear so // Update post-scoped state if caller passed it; otherwise clear so
// the next open() from gallery context uses neighbors mode. // the next open() from gallery context uses neighbors mode.
if (opts.postImageIds != null) { if (opts.playlistIds != null) {
postImageIds.value = opts.postImageIds playlistIds.value = opts.playlistIds
postImageIndex.value = opts.postImageIds.indexOf(id) playlistIndex.value = opts.playlistIds.indexOf(id)
if (postImageIndex.value < 0) postImageIndex.value = 0 if (playlistIndex.value < 0) playlistIndex.value = 0
} else if (opts.clearPostScope !== false && postImageIds.value != null) { } else if (opts.clearPlaylist !== false && playlistIds.value != null) {
postImageIds.value = null playlistIds.value = null
postImageIndex.value = 0 playlistIndex.value = 0
} }
const t = inflight.claim() const t = inflight.claim()
await run(async () => { await run(async () => {
@@ -53,17 +54,17 @@ export const useModalStore = defineStore('modal', () => {
currentImageId.value = null currentImageId.value = null
current.value = null current.value = null
error.value = null error.value = null
postImageIds.value = null playlistIds.value = null
postImageIndex.value = 0 playlistIndex.value = 0
} }
async function goPrev () { async function goPrev () {
if (postImageIds.value != null) { if (playlistIds.value != null) {
if (postImageIndex.value > 0) { if (playlistIndex.value > 0) {
const newIdx = postImageIndex.value - 1 const newIdx = playlistIndex.value - 1
const newId = postImageIds.value[newIdx] const newId = playlistIds.value[newIdx]
postImageIndex.value = newIdx playlistIndex.value = newIdx
await open(newId, { postImageIds: postImageIds.value }) await open(newId, { playlistIds: playlistIds.value })
} }
return return
} }
@@ -73,12 +74,12 @@ export const useModalStore = defineStore('modal', () => {
} }
async function goNext () { async function goNext () {
if (postImageIds.value != null) { if (playlistIds.value != null) {
if (postImageIndex.value < postImageIds.value.length - 1) { if (playlistIndex.value < playlistIds.value.length - 1) {
const newIdx = postImageIndex.value + 1 const newIdx = playlistIndex.value + 1
const newId = postImageIds.value[newIdx] const newId = playlistIds.value[newIdx]
postImageIndex.value = newIdx playlistIndex.value = newIdx
await open(newId, { postImageIds: postImageIds.value }) await open(newId, { playlistIds: playlistIds.value })
} }
return return
} }
@@ -177,19 +178,19 @@ export const useModalStore = defineStore('modal', () => {
const isOpen = computed(() => currentImageId.value !== null) const isOpen = computed(() => currentImageId.value !== null)
const canPrev = computed(() => { const canPrev = computed(() => {
if (postImageIds.value != null) return postImageIndex.value > 0 if (playlistIds.value != null) return playlistIndex.value > 0
return current.value?.neighbors?.prev_id != null return current.value?.neighbors?.prev_id != null
}) })
const canNext = computed(() => { const canNext = computed(() => {
if (postImageIds.value != null) { if (playlistIds.value != null) {
return postImageIndex.value < (postImageIds.value.length - 1) return playlistIndex.value < (playlistIds.value.length - 1)
} }
return current.value?.neighbors?.next_id != null return current.value?.neighbors?.next_id != null
}) })
return { return {
currentImageId, current, loading, error, currentImageId, current, loading, error,
postImageIds, postImageIndex, playlistIds, playlistIndex,
isOpen, canPrev, canNext, isOpen, canPrev, canNext,
open, close, goPrev, goNext, open, close, goPrev, goNext,
reloadTags, removeTag, addExistingTag, createAndAdd, reloadTags, removeTag, addExistingTag, createAndAdd,
+4 -1
View File
@@ -54,7 +54,10 @@ watch(() => route.query, (q) => {
}) })
function openImage(id) { function openImage(id) {
modal.open(id) // Walk the current gallery filter in the modal (#1322) — prev/next moves
// through exactly the filtered set the operator is viewing, not global
// neighbours. Snapshot of the currently-loaded, filtered, ordered ids.
modal.open(id, { playlistIds: store.images.map((i) => i.id) })
} }
</script> </script>