Files
FabledCurator/frontend/src/views/ShowcaseView.vue
T
bvandeusen adeee64a2d feat(ui): batch initial showcase/gallery loads into smaller chunks
Showcase and gallery were fetching the full initial batch (60 / 50
items) in a single API call. Operator-flagged 2026-05-30: items should
stream in as small batches so they render progressively rather than
blocking on one big response.

- showcase store: PAGE 60 → 5, INITIAL_BATCHES = 12 (5 × 12 = 60, same
  total). loadInitial() fetches PAGE chunks in sequence; shuffle()
  delegates. fetchPage() still returns one PAGE-sized chunk so
  infinite-scroll also pulls 5 per trigger.
- gallery store: loadMore() limit 50 → PAGE (5), INITIAL_BATCHES = 10.
  loadInitial() loops loadMore() up to INITIAL_BATCHES times, stopping
  early when nextCursor becomes null (end of data).
- ShowcaseView: onMounted calls loadInitial() instead of fetchPage().
  Gallery/setTagFilter/setPostFilter already routed through loadInitial.

Net visual effect: each ~5-item batch lands and renders independently;
combined with MasonryGrid's animateFromIndex logic, the showcase now
cascades batches in as they arrive instead of one big pop-in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:51:58 -04:00

78 lines
2.4 KiB
Vue

<template>
<v-container fluid class="pt-2 pb-6">
<Teleport to="#fc-nav-actions">
<v-btn
prepend-icon="mdi-shuffle-variant" variant="tonal" color="accent"
size="small" :loading="store.loading" @click="store.shuffle()"
>Shuffle</v-btn>
</Teleport>
<v-alert v-if="store.error" type="error" variant="tonal" closable class="my-4">
Failed to load: {{ store.error }}
</v-alert>
<v-alert v-else-if="store.isEmpty" type="info" variant="tonal" class="my-4">
No images yet.
</v-alert>
<MasonryGrid
:items="store.images"
:loading="store.loading"
:has-more="store.hasMore"
:animate-from-index="animateFromIndex"
@load-more="store.fetchPage()"
@open="openImage"
/>
</v-container>
</template>
<script setup>
import { onMounted, onUnmounted, ref, watch } from 'vue'
import MasonryGrid from '../components/discovery/MasonryGrid.vue'
import { useModalStore } from '../stores/modal.js'
import { useShowcaseStore } from '../stores/showcase.js'
const store = useShowcaseStore()
const modal = useModalStore()
// Track when items were appended vs replaced so MasonryGrid only animates
// items new to the current batch (mirrors IR's behavior: animate on
// initial load and on shuffle, but skip silent infinite-scroll appends).
const animateFromIndex = ref(0)
let prevCount = 0
watch(() => store.images.length, (newCount) => {
if (newCount < prevCount || prevCount === 0) {
// Reset (shuffle) or initial load — animate everything from 0.
animateFromIndex.value = 0
} else {
// Append — only animate the newly-added tail.
animateFromIndex.value = prevCount
}
prevCount = newCount
})
function openImage(id) {
modal.open(id)
}
// IR-parity keyboard shuffle: press R anywhere on the page (not inside a
// text input, not while a Vuetify overlay is open) to reshuffle.
function onKeydown(e) {
const t = e.target
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return
// Vuetify marks the active overlay (v-dialog, v-menu) on the body when
// open. Skip shuffle when a modal is in the way.
if (document.querySelector('.v-overlay--active')) return
if (e.key === 'r' || e.key === 'R') {
e.preventDefault()
store.shuffle()
}
}
onMounted(() => {
if (store.images.length === 0) store.loadInitial()
window.addEventListener('keydown', onKeydown)
})
onUnmounted(() => window.removeEventListener('keydown', onKeydown))
</script>