feat(gallery): pinned filter bar (Phase 1)
Gallery now has in-view filtering, styled like the app's sticky v-tabs
chrome (pinned at top:64px under TopNav).
- GalleryFilterBar: combined tag+artist autocomplete (searches
/api/tags + /api/artists), closable filter chips (multi-tag AND),
media toggle (All/Images/Videos), Newest/Oldest sort, Clear. Writes all
state to the URL via router.push.
- gallery store: filter is now { tag_ids, artist_id, media_type, sort,
post_id }; applyFilterFromQuery makes the URL the single source of truth
(deep-linkable, back-button works); chip labels resolved by id or
pre-noted on pick. Replaces the standalone tag chip + setTag/PostFilter.
- GalleryView: renders the bar (hidden in post-detail), syncs route.query
→ store on mount + every query change.
Also untracks the transient .claude/scheduled_tasks.lock committed in
3f30327 and gitignores it.
Tests: store parses query → composable scroll params, post_id exclusivity,
newest-sort omitted, label pre-seed, single initial fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,13 +12,7 @@
|
||||
<div class="fc-gallery-layout">
|
||||
<div class="fc-gallery-layout__main">
|
||||
<PostInfoHeader />
|
||||
<div v-if="store.filter.tag_id != null" class="fc-gallery-activefilter">
|
||||
<v-chip
|
||||
color="accent" variant="tonal" closable
|
||||
prepend-icon="mdi-tag"
|
||||
@click:close="clearTagFilter"
|
||||
>Tag: {{ store.filterTagName || `#${store.filter.tag_id}` }}</v-chip>
|
||||
</div>
|
||||
<GalleryFilterBar v-if="store.filter.post_id == null" />
|
||||
<EmptyState v-if="store.isEmpty" />
|
||||
<GalleryGrid v-else @open="openImage" />
|
||||
</div>
|
||||
@@ -31,10 +25,11 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
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 TimelineSidebar from '../components/gallery/TimelineSidebar.vue'
|
||||
import EmptyState from '../components/gallery/EmptyState.vue'
|
||||
import PostInfoHeader from '../components/gallery/PostInfoHeader.vue'
|
||||
@@ -45,40 +40,19 @@ const store = useGalleryStore()
|
||||
const modal = useModalStore()
|
||||
const sel = useGallerySelectionStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(async () => {
|
||||
const postId = parseInt(route.query.post_id, 10)
|
||||
const tagId = parseInt(route.query.tag_id, 10)
|
||||
if (!isNaN(postId)) store.setPostFilter(postId)
|
||||
else if (!isNaN(tagId)) store.setTagFilter(tagId)
|
||||
await store.loadInitial()
|
||||
await store.loadTimeline()
|
||||
})
|
||||
// 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.tag_id, (q) => {
|
||||
watch(() => route.query, (q) => {
|
||||
sel.clear() // result set changed — selected ids are no longer valid
|
||||
const tagId = parseInt(q, 10)
|
||||
store.setTagFilter(isNaN(tagId) ? null : tagId)
|
||||
})
|
||||
|
||||
watch(() => route.query.post_id, (q) => {
|
||||
sel.clear() // result set changed — selected ids are no longer valid
|
||||
const postId = parseInt(q, 10)
|
||||
store.setPostFilter(isNaN(postId) ? null : postId)
|
||||
store.applyFilterFromQuery(q)
|
||||
})
|
||||
|
||||
function openImage(id) {
|
||||
modal.open(id)
|
||||
}
|
||||
|
||||
// Clear the active tag filter by dropping it from the URL; the route
|
||||
// watcher above resets the store filter and reloads.
|
||||
function clearTagFilter() {
|
||||
const q = { ...route.query }
|
||||
delete q.tag_id
|
||||
router.push({ query: q })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -88,7 +62,6 @@ function clearTagFilter() {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.fc-gallery-layout__main { flex: 1; min-width: 0; }
|
||||
.fc-gallery-activefilter { margin-bottom: 12px; }
|
||||
.fc-gallery-layout__sidebar { flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
Reference in New Issue
Block a user