feat(modal-store): post-scoped cycle — open(id, { postImageIds }) pins prev/next to the array; canPrev/canNext + goPrev/goNext check the array index instead of current.value.neighbors when set. Gallery-context callers unchanged (default args clear scope)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 20:48:30 -04:00
parent 07344e0843
commit b8d89b9f2a
+66 -21
View File
@@ -6,14 +6,31 @@ export const useModalStore = defineStore('modal', () => {
const api = useApi() const api = useApi()
const currentImageId = ref(null) const currentImageId = ref(null)
const current = ref(null) // full image detail from API const current = ref(null)
const loading = ref(false) const loading = ref(false)
const error = ref(null) const error = ref(null)
async function open(id) { // Post-scoped cycle. When set, prev/next cycles within this array
// (used by PostModal's PostImageGrid clicks). When null, prev/next
// falls back to current.value.neighbors (the gallery-store-driven
// /api/gallery/image/<id> neighbors).
const postImageIds = ref(null)
const postImageIndex = ref(0)
async function open (id, opts = {}) {
currentImageId.value = id currentImageId.value = id
loading.value = true loading.value = true
error.value = null error.value = null
// Update post-scoped state if caller passed it; otherwise clear so
// the next open() from gallery context uses neighbors mode.
if (opts.postImageIds != null) {
postImageIds.value = opts.postImageIds
postImageIndex.value = opts.postImageIds.indexOf(id)
if (postImageIndex.value < 0) postImageIndex.value = 0
} else if (opts.clearPostScope !== false && postImageIds.value != null) {
postImageIds.value = null
postImageIndex.value = 0
}
try { try {
current.value = await api.get(`/api/gallery/image/${id}`) current.value = await api.get(`/api/gallery/image/${id}`)
} catch (e) { } catch (e) {
@@ -24,41 +41,58 @@ export const useModalStore = defineStore('modal', () => {
} }
} }
async function close() { async function close () {
currentImageId.value = null currentImageId.value = null
current.value = null current.value = null
error.value = null error.value = null
postImageIds.value = null
postImageIndex.value = 0
} }
async function goPrev() { async function goPrev () {
if (current.value && current.value.neighbors.prev_id) { if (postImageIds.value != null) {
if (postImageIndex.value > 0) {
const newIdx = postImageIndex.value - 1
const newId = postImageIds.value[newIdx]
postImageIndex.value = newIdx
await open(newId, { postImageIds: postImageIds.value })
}
return
}
if (current.value && current.value.neighbors?.prev_id) {
await open(current.value.neighbors.prev_id) await open(current.value.neighbors.prev_id)
} }
} }
async function goNext() {
if (current.value && current.value.neighbors.next_id) { async function goNext () {
if (postImageIds.value != null) {
if (postImageIndex.value < postImageIds.value.length - 1) {
const newIdx = postImageIndex.value + 1
const newId = postImageIds.value[newIdx]
postImageIndex.value = newIdx
await open(newId, { postImageIds: postImageIds.value })
}
return
}
if (current.value && current.value.neighbors?.next_id) {
await open(current.value.neighbors.next_id) await open(current.value.neighbors.next_id)
} }
} }
async function reloadTags() { async function reloadTags () {
if (!currentImageId.value) return if (!currentImageId.value) return
const tags = await api.get(`/api/images/${currentImageId.value}/tags`) const tags = await api.get(`/api/images/${currentImageId.value}/tags`)
if (current.value) current.value.tags = tags if (current.value) current.value.tags = tags
} }
async function removeTag(tagId) { async function removeTag (tagId) {
if (!currentImageId.value) return if (!currentImageId.value) return
// Optimistic UI: remove locally first, restore on error.
const prev = current.value.tags const prev = current.value.tags
current.value.tags = current.value.tags.filter(t => t.id !== tagId) current.value.tags = current.value.tags.filter(t => t.id !== tagId)
try { try {
// FC-2b: removal also records a per-image rejection (suggestions/dismiss
// is the rejection-recording endpoint) so the allowlist maintenance
// task won't re-apply this tag to this image.
await api.delete(`/api/images/${currentImageId.value}/tags/${tagId}`) await api.delete(`/api/images/${currentImageId.value}/tags/${tagId}`)
await api.post(`/api/images/${currentImageId.value}/suggestions/dismiss`, { await api.post(`/api/images/${currentImageId.value}/suggestions/dismiss`, {
body: { tag_id: tagId } body: { tag_id: tagId },
}) })
} catch (e) { } catch (e) {
current.value.tags = prev current.value.tags = prev
@@ -67,25 +101,36 @@ export const useModalStore = defineStore('modal', () => {
} }
} }
async function addExistingTag(tagId) { async function addExistingTag (tagId) {
if (!currentImageId.value) return if (!currentImageId.value) return
await api.post(`/api/images/${currentImageId.value}/tags`, { await api.post(`/api/images/${currentImageId.value}/tags`, {
body: { tag_id: tagId, source: 'manual' } body: { tag_id: tagId, source: 'manual' },
}) })
await reloadTags() await reloadTags()
} }
async function createAndAdd({ name, kind, fandom_id = null }) { async function createAndAdd ({ name, kind, fandom_id = null }) {
const tag = await api.post('/api/tags', { body: { name, kind, fandom_id } }) const tag = await api.post('/api/tags', { body: { name, kind, fandom_id } })
await addExistingTag(tag.id) await addExistingTag(tag.id)
} }
const isOpen = computed(() => currentImageId.value !== null) const isOpen = computed(() => currentImageId.value !== null)
const canPrev = computed(() => current.value?.neighbors?.prev_id != null) const canPrev = computed(() => {
const canNext = computed(() => current.value?.neighbors?.next_id != null) if (postImageIds.value != null) return postImageIndex.value > 0
return current.value?.neighbors?.prev_id != null
})
const canNext = computed(() => {
if (postImageIds.value != null) {
return postImageIndex.value < (postImageIds.value.length - 1)
}
return current.value?.neighbors?.next_id != null
})
return { return {
currentImageId, current, loading, error, isOpen, canPrev, canNext, currentImageId, current, loading, error,
open, close, goPrev, goNext, reloadTags, removeTag, addExistingTag, createAndAdd postImageIds, postImageIndex,
isOpen, canPrev, canNext,
open, close, goPrev, goNext,
reloadTags, removeTag, addExistingTag, createAndAdd,
} }
}) })