Files
FabledCurator/frontend/src/views/GalleryView.vue
T
bvandeusen 9726d6f4b5
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 37s
CI / integration (push) Successful in 3m40s
feat(ui): hidden-view review strip — flagged auto-hides with keep / un-hide (#141 step 5)
When "Show hidden" is on, a review strip appears atop the gallery listing the
auto-hidden chrome flagged "also looks like content" (most-concerning first):
thumbnail + "also looks like <X>" + Keep hidden / Un-hide. Un-hide removes the
presentation tag (image returns to the gallery) and trains the head; Keep
resolves the flag. Self-hides when there's nothing to review; theme-token styled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
2026-07-06 23:39:02 -04:00

85 lines
3.0 KiB
Vue

<template>
<v-container fluid class="pt-2 pb-6">
<Teleport to="#fc-nav-actions">
<v-btn
:color="sel.isSelectMode ? 'accent' : undefined"
:variant="sel.isSelectMode ? 'flat' : 'tonal'"
size="small"
@click="sel.isSelectMode ? sel.exitSelectMode() : sel.enterSelectMode()"
>{{ sel.isSelectMode ? 'Done' : 'Select' }}</v-btn>
</Teleport>
<div class="fc-gallery-layout">
<div class="fc-gallery-layout__main">
<PostInfoHeader />
<GalleryFilterBar v-if="store.filter.post_id == null" />
<HiddenReviewStrip />
<EmptyState v-if="store.isEmpty" />
<GalleryGrid v-else @open="openImage" />
</div>
<TimelineSidebar
v-if="store.images.length > 0 && store.filter.similar_to == null"
class="fc-gallery-layout__sidebar"
/>
</div>
<BulkEditorPanel />
</v-container>
</template>
<script setup>
import { onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useGalleryStore } from '../stores/gallery.js'
import { useModalStore } from '../stores/modal.js'
import GalleryGrid from '../components/gallery/GalleryGrid.vue'
import GalleryFilterBar from '../components/gallery/GalleryFilterBar.vue'
import HiddenReviewStrip from '../components/gallery/HiddenReviewStrip.vue'
import TimelineSidebar from '../components/gallery/TimelineSidebar.vue'
import EmptyState from '../components/gallery/EmptyState.vue'
import PostInfoHeader from '../components/gallery/PostInfoHeader.vue'
import BulkEditorPanel from '../components/gallery/BulkEditorPanel.vue'
import { useGallerySelectionStore } from '../stores/gallerySelection.js'
const store = useGalleryStore()
const modal = useModalStore()
const sel = useGallerySelectionStore()
const route = useRoute()
// The URL query is the single source of truth for filters. Apply it on
// mount and on any query change (filter bar pushes, back button, deep-link).
onMounted(() => store.applyFilterFromQuery(route.query))
watch(() => route.query, (q) => {
sel.clear() // result set changed — selected ids are no longer valid
store.applyFilterFromQuery(q)
})
function openImage(id) {
// Walk the current gallery filter in the modal (#1322) — prev/next moves
// through exactly the filtered set the operator is viewing, not global
// neighbours. Snapshot of the currently-loaded, filtered, ordered ids.
modal.open(id, { playlistIds: store.images.map((i) => i.id) })
}
</script>
<style scoped>
.fc-gallery-layout {
display: flex;
gap: 16px;
align-items: flex-start;
}
.fc-gallery-layout__main { flex: 1; min-width: 0; }
.fc-gallery-layout__sidebar { flex-shrink: 0; }
@media (max-width: 900px) {
.fc-gallery-layout { flex-direction: column-reverse; }
.fc-gallery-layout__sidebar { width: 100%; max-height: 200px; }
}
/* Phones: the year/month timeline strip eats vertical space and reads poorly
as a horizontal band — drop it; the gallery scroll is the primary nav here. */
@media (max-width: 600px) {
.fc-gallery-layout__sidebar { display: none; }
}
</style>