Files
FabledCurator/frontend/src/views/ShowcaseView.vue
T
bvandeusen 91be9df671
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 14s
CI / frontend-build (push) Successful in 26s
CI / intimp (push) Successful in 3m32s
CI / intapi (push) Successful in 7m43s
CI / intcore (push) Successful in 8m14s
fix(download): dispatch archive/non-media in attach_in_place; reshuffle showcase on mount
attach_in_place mirrored only the media flow, so gallery-dl-downloaded
zips/PDFs/audio bounced back as `skipped+invalid_image`, which
download_service counted as an ingest error and flipped runs to
status="error" despite N successful image attaches. Lustria patreon
event #38998 (21 images + 1 OST zip) went red for exactly this reason.

Now attach_in_place dispatches the same way as import_one: archives →
_import_archive (extracts media members, captures archive as
PostAttachment), non-media → _capture_attachment. Download_service
accepts the new `attached` result and treats non-duplicate skips as
soft skips, not ingest errors.

Also: ShowcaseView always loadInitial() on mount, not just when the
store is empty — Pinia persists across navigations and operator wants
a fresh shuffle every time the showcase loads.
2026-06-02 08:16:13 -04:00

83 lines
2.7 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(() => {
// Operator-stated 2026-06-02: every load of the showcase view should
// show new images. Pinia persists the store across navigations, so
// returning to /showcase used to render the same set as before. Now
// we always reshuffle on mount — loadInitial() resets `seen`,
// `images`, and `exhausted` and pipelines a fresh batch.
store.loadInitial()
window.addEventListener('keydown', onKeydown)
})
onUnmounted(() => window.removeEventListener('keydown', onKeydown))
</script>