feat(nav): consolidate Posts/Artists/Tags into a Browse hub
Posts, Artists, and Tags are the three 'browse the library by an axis'
surfaces; Subscriptions stays purely management (operator-asked 2026-06-09).
New BrowseView renders them as tabs (?tab=posts|artists|tags); only the active
tab mounts. The old standalone paths become redirects into the matching tab,
preserving deep-link query (/posts?post_id=N → /browse?tab=posts&post_id=N) and
keeping the route names so existing { name: 'posts'|'artists'|'tags' } links and
path pushes still resolve. Nav now reads Showcase · Gallery · Browse · Series ·
Subscriptions, with Settings pinned right.
Test: /browse resolves; /tags and /artists redirect into their tabs; a posts
deep link survives the redirect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+21
-14
@@ -2,14 +2,12 @@ import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
|
||||
import SettingsView from './views/SettingsView.vue'
|
||||
import GalleryView from './views/GalleryView.vue'
|
||||
import ShowcaseView from './views/ShowcaseView.vue'
|
||||
import TagsView from './views/TagsView.vue'
|
||||
import BrowseView from './views/BrowseView.vue'
|
||||
import ArtistView from './views/ArtistView.vue'
|
||||
import SeriesView from './views/SeriesView.vue'
|
||||
import SeriesManageView from './views/SeriesManageView.vue'
|
||||
import SeriesReaderView from './views/SeriesReaderView.vue'
|
||||
import SubscriptionsView from './views/SubscriptionsView.vue'
|
||||
import PostsView from './views/PostsView.vue'
|
||||
import ArtistsView from './views/ArtistsView.vue'
|
||||
|
||||
// The application's front door. `/` redirects here. Changing the front door
|
||||
// is a one-line edit (e.g. '/gallery' or '/tags').
|
||||
@@ -22,27 +20,36 @@ const routes = [
|
||||
// FC-2: image backbone
|
||||
{ path: '/showcase', name: 'showcase', component: ShowcaseView, meta: { title: 'Showcase' } },
|
||||
{ path: '/gallery', name: 'gallery', component: GalleryView, meta: { title: 'Gallery' } },
|
||||
{ path: '/artists', name: 'artists', component: ArtistsView, meta: { title: 'Artists' } },
|
||||
{ path: '/tags', name: 'tags', component: TagsView, meta: { title: 'Tags' } },
|
||||
// Browse hub (operator-asked 2026-06-09): Posts / Artists / Tags as tabs —
|
||||
// the three "browse the library by an axis" surfaces. One nav entry; the old
|
||||
// standalone paths redirect into the matching tab (below).
|
||||
{ path: '/browse', name: 'browse', component: BrowseView, meta: { title: 'Browse' } },
|
||||
// Artist detail — no meta.title (reached by clicking an artist, not nav).
|
||||
{ path: '/artist/:slug', name: 'artist', component: ArtistView },
|
||||
// Series browse — a nav entry (meta.title). Peer of Posts.
|
||||
// Series browse — a nav entry (meta.title).
|
||||
{ path: '/series', name: 'series', component: SeriesView, meta: { title: 'Series' } },
|
||||
// Series management — no meta.title (reached from a series card/tag).
|
||||
{ path: '/series/:tagId', name: 'series-manage', component: SeriesManageView },
|
||||
// Series reader — immersive (no top nav, no meta.title).
|
||||
{ path: '/series/:tagId/read', name: 'series-read', component: SeriesReaderView, meta: { immersive: true } },
|
||||
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings' } },
|
||||
|
||||
// FC-3: subscription backbone
|
||||
// /credentials and /downloads were folded into /subscriptions as subtabs
|
||||
// 2026-05-27 (?tab=settings and ?tab=downloads). The hub view owns the
|
||||
// whole download pipeline domain.
|
||||
{ path: '/posts', name: 'posts', component: PostsView, meta: { title: 'Posts' } },
|
||||
// FC-3: subscription backbone — purely management (sources/downloads),
|
||||
// distinct from the Browse hub.
|
||||
{ path: '/subscriptions', name: 'subscriptions', component: SubscriptionsView, meta: { title: 'Subscriptions' } },
|
||||
|
||||
// Bookmark/back-button safety net for the routes that got folded in
|
||||
// (no meta.title — stay out of TopNav).
|
||||
// Settings — config, pinned to the right of the nav (TopNav special-cases it).
|
||||
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings' } },
|
||||
|
||||
// The old standalone paths now redirect into the Browse hub, preserving any
|
||||
// deep-link query (e.g. /posts?post_id=N → /browse?tab=posts&post_id=N). The
|
||||
// route NAMES stay so existing { name: 'posts' | 'artists' | 'tags' } links
|
||||
// and path pushes keep resolving.
|
||||
{ path: '/posts', name: 'posts', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'posts' } }) },
|
||||
{ path: '/artists', name: 'artists', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'artists' } }) },
|
||||
{ path: '/tags', name: 'tags', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'tags' } }) },
|
||||
|
||||
// Bookmark/back-button safety net for the routes that got folded into
|
||||
// /subscriptions (no meta.title — stay out of TopNav).
|
||||
{ path: '/credentials', redirect: '/subscriptions?tab=settings' },
|
||||
{ path: '/downloads', redirect: '/subscriptions?tab=downloads' }
|
||||
]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="fc-browse">
|
||||
<v-container fluid class="pt-2 pb-0">
|
||||
<v-tabs v-model="tab" density="compact">
|
||||
<v-tab value="posts">Posts</v-tab>
|
||||
<v-tab value="artists">Artists</v-tab>
|
||||
<v-tab value="tags">Tags</v-tab>
|
||||
</v-tabs>
|
||||
</v-container>
|
||||
|
||||
<!-- Each tab's view is self-contained (its own container + state). Only the
|
||||
active one is mounted; switching tabs swaps it. The Posts tab still
|
||||
reads its deep-link (post_id/artist_id/platform) from route.query, so
|
||||
/browse?tab=posts&post_id=N works exactly like the old /posts deep link.
|
||||
(FC nav consolidation, operator-asked 2026-06-09: Posts/Artists/Tags are
|
||||
the three "browse the library by an axis" surfaces.) -->
|
||||
<component :is="activeComponent" :key="tab" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ArtistsView from './ArtistsView.vue'
|
||||
import PostsView from './PostsView.vue'
|
||||
import TagsView from './TagsView.vue'
|
||||
|
||||
const TABS = { posts: PostsView, artists: ArtistsView, tags: TagsView }
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const tab = computed({
|
||||
get() {
|
||||
const t = route.query.tab
|
||||
return typeof t === 'string' && t in TABS ? t : 'posts'
|
||||
},
|
||||
set(value) {
|
||||
// Switching tabs starts a clean tab — a posts deep link (post_id, etc.)
|
||||
// belongs only to the Posts tab and shouldn't bleed into Artists/Tags.
|
||||
router.replace({ name: 'browse', query: { tab: value } })
|
||||
},
|
||||
})
|
||||
|
||||
const activeComponent = computed(() => TABS[tab.value])
|
||||
</script>
|
||||
@@ -17,13 +17,29 @@ describe('router', () => {
|
||||
expect(router.resolve('/gallery').name).toBe('gallery')
|
||||
})
|
||||
|
||||
it('showcase, tags, artist resolve to named routes', () => {
|
||||
it('showcase, artist resolve to named routes', () => {
|
||||
expect(router.resolve('/showcase').name).toBe('showcase')
|
||||
expect(router.resolve('/tags').name).toBe('tags')
|
||||
expect(router.resolve('/artist/some-slug').name).toBe('artist')
|
||||
expect(router.resolve('/artist/some-slug').params.slug).toBe('some-slug')
|
||||
})
|
||||
|
||||
it('browse hub is reachable and old axis paths redirect into its tabs', async () => {
|
||||
expect(router.resolve('/browse').name).toBe('browse')
|
||||
|
||||
await router.push('/tags')
|
||||
expect(router.currentRoute.value.name).toBe('browse')
|
||||
expect(router.currentRoute.value.query.tab).toBe('tags')
|
||||
|
||||
await router.push('/artists')
|
||||
expect(router.currentRoute.value.query.tab).toBe('artists')
|
||||
|
||||
// A posts deep link survives the redirect into the hub tab.
|
||||
await router.push({ path: '/posts', query: { post_id: '7' } })
|
||||
expect(router.currentRoute.value.name).toBe('browse')
|
||||
expect(router.currentRoute.value.query.tab).toBe('posts')
|
||||
expect(router.currentRoute.value.query.post_id).toBe('7')
|
||||
})
|
||||
|
||||
it('series-read is an immersive route', () => {
|
||||
const r = router.resolve('/series/5/read')
|
||||
expect(r.name).toBe('series-read')
|
||||
|
||||
Reference in New Issue
Block a user