What you pay for, what Discord drops, and pixiv switched off #251
@@ -13,8 +13,9 @@ tags it, and gives you something better than a folder full of images to look
|
|||||||
through afterwards.
|
through afterwards.
|
||||||
|
|
||||||
- **Gallery and browsing.** Images, videos and multi-page works, organised by
|
- **Gallery and browsing.** Images, videos and multi-page works, organised by
|
||||||
artist, tag, post and series. A Showcase front page, a filterable gallery, a
|
artist, tag, post and series. A newest-first feed of what just arrived as the
|
||||||
similarity-driven Explore view, and a page-turning reader for series.
|
front page, a random Showcase, a filterable gallery, a similarity-driven
|
||||||
|
Explore view, and a page-turning reader for series.
|
||||||
- **Subscriptions.** Follows creators on Patreon, SubscribeStar, Pixiv and
|
- **Subscriptions.** Follows creators on Patreon, SubscribeStar, Pixiv and
|
||||||
anything `gallery-dl` supports, on a schedule. Handles paywalled posts using
|
anything `gallery-dl` supports, on a schedule. Handles paywalled posts using
|
||||||
your own logged-in session.
|
your own logged-in session.
|
||||||
|
|||||||
+20
-1
@@ -9,15 +9,34 @@ import SeriesView from './views/SeriesView.vue'
|
|||||||
import SeriesManageView from './views/SeriesManageView.vue'
|
import SeriesManageView from './views/SeriesManageView.vue'
|
||||||
import SeriesReaderView from './views/SeriesReaderView.vue'
|
import SeriesReaderView from './views/SeriesReaderView.vue'
|
||||||
import SubscriptionsView from './views/SubscriptionsView.vue'
|
import SubscriptionsView from './views/SubscriptionsView.vue'
|
||||||
|
import PostsView from './views/PostsView.vue'
|
||||||
|
|
||||||
// The application's front door. `/` redirects here. Changing the front door
|
// The application's front door. `/` redirects here. Changing the front door
|
||||||
// is a one-line edit (e.g. '/gallery' or '/tags').
|
// is a one-line edit (e.g. '/gallery' or '/tags').
|
||||||
export const FRONT_DOOR = '/showcase'
|
//
|
||||||
|
// Moved from '/showcase' to '/latest' (milestone #387 B1). Showcase answers
|
||||||
|
// "show me something" — a random TABLESAMPLE, lean-back, and it can never tell
|
||||||
|
// you anything is wrong. The feed answers "what arrived?", which is a question
|
||||||
|
// the operator has every day, and it is the only view where a failing source
|
||||||
|
// shows up on its own as a creator who has gone quiet. The per-artist "new
|
||||||
|
// since last visit" badges (#597) were a workaround for this view not being
|
||||||
|
// the door. Showcase is demoted to a nav entry, not removed.
|
||||||
|
export const FRONT_DOOR = '/latest'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
// Root is a redirect only — no meta.title so it stays out of the nav.
|
// Root is a redirect only — no meta.title so it stays out of the nav.
|
||||||
{ path: '/', redirect: FRONT_DOOR },
|
{ path: '/', redirect: FRONT_DOOR },
|
||||||
|
|
||||||
|
// The front door (#387 B1): the post feed as its own surface. Reuses
|
||||||
|
// PostsView unchanged — it is already a self-contained feed (own container,
|
||||||
|
// own store, infinite scroll, deep-link anchoring), and Browse only ever
|
||||||
|
// wrapped it in a tab strip. Mounting it directly IS the difference the
|
||||||
|
// promotion was after: a door you arrive at, not a hub you navigate out of.
|
||||||
|
//
|
||||||
|
// No stickyChrome: unlike Browse/Gallery/Settings this view has no sticky
|
||||||
|
// sub-header for the nav to butt against, so the nav keeps its normal fade.
|
||||||
|
{ path: '/latest', name: 'latest', component: PostsView, meta: { title: 'Latest', navOrder: 5 } },
|
||||||
|
|
||||||
// FC-2: image backbone
|
// FC-2: image backbone
|
||||||
{ path: '/showcase', name: 'showcase', component: ShowcaseView, meta: { title: 'Showcase', navOrder: 10 } },
|
{ path: '/showcase', name: 'showcase', component: ShowcaseView, meta: { title: 'Showcase', navOrder: 10 } },
|
||||||
{ path: '/gallery', name: 'gallery', component: GalleryView, meta: { title: 'Gallery', navOrder: 20, stickyChrome: true } },
|
{ path: '/gallery', name: 'gallery', component: GalleryView, meta: { title: 'Gallery', navOrder: 20, stickyChrome: true } },
|
||||||
|
|||||||
@@ -3,9 +3,14 @@
|
|||||||
<!-- In-context view: deep-linked to one post, with bidirectional infinite
|
<!-- In-context view: deep-linked to one post, with bidirectional infinite
|
||||||
scroll — newer posts load above, older posts below. -->
|
scroll — newer posts load above, older posts below. -->
|
||||||
<template v-if="postIdFilter != null">
|
<template v-if="postIdFilter != null">
|
||||||
|
<!-- Returns to whichever surface you deep-linked FROM. This used to be a
|
||||||
|
hard `{ name: 'posts' }`, which redirects into Browse — fine while
|
||||||
|
this view only ever rendered inside Browse's tab, but it now also
|
||||||
|
serves the front door (#387 B1), where it would have yanked the
|
||||||
|
operator sideways into a different view. -->
|
||||||
<v-btn
|
<v-btn
|
||||||
variant="text" size="small" prepend-icon="mdi-arrow-left"
|
variant="text" size="small" prepend-icon="mdi-arrow-left"
|
||||||
:to="{ name: 'posts' }" class="mb-2"
|
:to="allPostsTarget" class="mb-2"
|
||||||
>All posts</v-btn>
|
>All posts</v-btn>
|
||||||
|
|
||||||
<v-alert v-if="store.error" type="error" variant="tonal" closable class="mb-3">
|
<v-alert v-if="store.error" type="error" variant="tonal" closable class="mb-3">
|
||||||
@@ -103,6 +108,13 @@ const hasActiveFilter = computed(() =>
|
|||||||
artistFilter.value != null || platformFilter.value != null || searchFilter.value != null
|
artistFilter.value != null || platformFilter.value != null || searchFilter.value != null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Drop only `post_id` and stay where we are — keeps Browse's `tab=posts` (and
|
||||||
|
// any active artist/platform scope) intact instead of resetting the surface.
|
||||||
|
const allPostsTarget = computed(() => {
|
||||||
|
const { post_id: _drop, ...rest } = route.query
|
||||||
|
return { name: route.name, query: rest }
|
||||||
|
})
|
||||||
|
|
||||||
// --- normal feed (downward infinite scroll) ---
|
// --- normal feed (downward infinite scroll) ---
|
||||||
const sentinel = ref(null)
|
const sentinel = ref(null)
|
||||||
let observer = null
|
let observer = null
|
||||||
|
|||||||
@@ -2,8 +2,29 @@ import { describe, it, expect } from 'vitest'
|
|||||||
import router, { FRONT_DOOR } from '../src/router.js'
|
import router, { FRONT_DOOR } from '../src/router.js'
|
||||||
|
|
||||||
describe('router', () => {
|
describe('router', () => {
|
||||||
it('FRONT_DOOR defaults to /showcase', () => {
|
it('FRONT_DOOR is the post feed', () => {
|
||||||
expect(FRONT_DOOR).toBe('/showcase')
|
// Moved from /showcase in #387 B1. Asserted on the constant rather than on
|
||||||
|
// a navigation so the intent is pinned: the door is the feed, and moving it
|
||||||
|
// again should be a deliberate edit to a failing test, not a quiet change.
|
||||||
|
expect(FRONT_DOOR).toBe('/latest')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/latest is the feed, mounted outside the Browse tab strip', () => {
|
||||||
|
const r = router.resolve('/latest')
|
||||||
|
expect(r.name).toBe('latest')
|
||||||
|
// A nav entry in its own right (meta.title is what TopNav lists on), and
|
||||||
|
// first in the order.
|
||||||
|
expect(r.meta.title).toBe('Latest')
|
||||||
|
expect(r.meta.navOrder).toBe(5)
|
||||||
|
// Deliberately NOT stickyChrome — it has no sticky sub-header for the nav
|
||||||
|
// to butt against, unlike Browse/Gallery/Settings.
|
||||||
|
expect(r.meta.stickyChrome).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('showcase is demoted, not removed — still reachable and still in the nav', () => {
|
||||||
|
const r = router.resolve('/showcase')
|
||||||
|
expect(r.name).toBe('showcase')
|
||||||
|
expect(r.meta.title).toBe('Showcase')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('/ redirects to FRONT_DOOR', async () => {
|
it('/ redirects to FRONT_DOOR', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user