adeee64a2d
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>
78 lines
2.4 KiB
Vue
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>
|