91be9df671
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.
83 lines
2.7 KiB
Vue
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>
|