diff --git a/web/src/pages/DiscoverContentRow.tsx b/web/src/pages/DiscoverContentRow.tsx
index 85579b1..b15f2d3 100644
--- a/web/src/pages/DiscoverContentRow.tsx
+++ b/web/src/pages/DiscoverContentRow.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
-import { Info } from 'lucide-react'
+import { ChevronLeft, ChevronRight, Info } from 'lucide-react'
import type { DiscoverItem } from '../api/discover'
import { imageURL } from '../api/client'
@@ -8,19 +8,50 @@ import { discoverItemSource } from './discoverPageModel'
export function ContentRow({
title,
items,
+ page = 1,
+ canNext = false,
imageVersion,
refreshImageVersion,
+ onPageChange,
onSelect,
}: {
title: string
items: DiscoverItem[]
+ page?: number
+ canNext?: boolean
imageVersion?: string
refreshImageVersion?: string
+ onPageChange?: (delta: number) => void
onSelect: (item: DiscoverItem) => void
}) {
return (
- {title}
+
+
{title}
+ {onPageChange && (
+
+
+ 第 {page} 页
+
+
+ )}
+
{items.map((item, index) => (
([])
const [selected, setSelected] = useState([])
const [rows, setRows] = useState>({})
+ const [rowPages, setRowPages] = useState>({})
+ const [rowCanNext, setRowCanNext] = useState>({})
const [rowLoading, setRowLoading] = useState>({})
const [rowErrors, setRowErrors] = useState>({})
const [sectionsReady, setSectionsReady] = useState(false)
@@ -35,7 +37,9 @@ export function DiscoverPage() {
const saved = readSavedSections(items)
const available = new Set(items.map((item) => item.key))
const fallback = defaultSections.filter((key) => available.has(key))
- setSelected(saved.length > 0 ? saved : fallback)
+ const nextSelected = saved.length > 0 ? saved : fallback
+ setSelected(nextSelected)
+ setRowPages(Object.fromEntries(nextSelected.map((key) => [key, 1])))
setSectionsReady(true)
})
.catch(() => {
@@ -60,6 +64,7 @@ export function DiscoverPage() {
if (selected.length === 0) {
setRows({})
setRowLoading({})
+ setRowCanNext({})
setRowErrors({})
setLoading(false)
return
@@ -83,16 +88,19 @@ export function DiscoverPage() {
if (!cancelled && pending <= 0) setLoading(false)
}
for (const key of selected) {
+ const page = rowPages[key] ?? 1
discoverAPI
- .feed([key])
+ .feed([key], page)
.then((feed) => {
if (cancelled) return
- setRows((current) => ({ ...current, [key]: feed[key] ?? [] }))
+ setRows((current) => ({ ...current, [key]: feed.items[key] ?? [] }))
+ setRowCanNext((current) => ({ ...current, [key]: Boolean(feed.meta[key]?.has_next) }))
})
.catch((err) => {
if (cancelled) return
const message = err instanceof Error ? err.message : String(err)
setRows((current) => ({ ...current, [key]: [] }))
+ setRowCanNext((current) => ({ ...current, [key]: false }))
setRowErrors((current) => ({ ...current, [key]: message }))
})
.finally(() => {
@@ -105,7 +113,7 @@ export function DiscoverPage() {
return () => {
cancelled = true
}
- }, [sections, sectionsReady, selected, reloadSeq])
+ }, [sections, sectionsReady, selected, rowPages, reloadSeq])
const sectionMap = useMemo(
() => new Map(sections.map((section) => [section.key, section])),
@@ -121,6 +129,15 @@ export function DiscoverPage() {
}
return [...current, key]
})
+ setRowPages((current) => ({ ...current, [key]: current[key] ?? 1 }))
+ }
+
+ const changeDiscoverPage = (key: string, delta: number) => {
+ setRowPages((current) => {
+ const nextPage = Math.max(1, (current[key] ?? 1) + delta)
+ if (nextPage === (current[key] ?? 1)) return current
+ return { ...current, [key]: nextPage }
+ })
}
const refreshDiscover = () => {
@@ -153,11 +170,14 @@ export function DiscoverPage() {
rows={rows}
rowLoading={rowLoading}
rowErrors={rowErrors}
+ rowPages={rowPages}
+ rowCanNext={rowCanNext}
loading={loading}
hasContent={hasContent}
imageVersion={imageVersion}
refreshImageVersion={refreshImageVersion}
sectionLabel={sectionLabel}
+ onPageChange={changeDiscoverPage}
onSelect={setActiveItem}
/>
)}
diff --git a/web/src/pages/DiscoverPageSections.tsx b/web/src/pages/DiscoverPageSections.tsx
index 3c20c5a..62a4c39 100644
--- a/web/src/pages/DiscoverPageSections.tsx
+++ b/web/src/pages/DiscoverPageSections.tsx
@@ -84,22 +84,28 @@ export function DiscoverResults({
rows,
rowLoading,
rowErrors,
+ rowPages,
+ rowCanNext,
loading,
hasContent,
imageVersion,
refreshImageVersion,
sectionLabel,
+ onPageChange,
onSelect,
}: {
selected: string[]
rows: Record
rowLoading: Record
rowErrors: Record
+ rowPages: Record
+ rowCanNext: Record
loading: boolean
hasContent: boolean
imageVersion: string
refreshImageVersion?: string
sectionLabel: SectionLabel
+ onPageChange: (key: string, delta: number) => void
onSelect: (item: DiscoverItem) => void
}) {
const hasRowErrors = Object.keys(rowErrors).length > 0
@@ -119,8 +125,11 @@ export function DiscoverResults({
key={key}
title={sectionLabel(key)}
items={items}
+ page={rowPages[key] ?? 1}
+ canNext={Boolean(rowCanNext[key])}
imageVersion={imageVersion}
refreshImageVersion={refreshImageVersion}
+ onPageChange={(delta) => onPageChange(key, delta)}
onSelect={onSelect}
/>
)
diff --git a/web/src/pages/SearchExternalResults.tsx b/web/src/pages/SearchExternalResults.tsx
index e88ae47..532cf2e 100644
--- a/web/src/pages/SearchExternalResults.tsx
+++ b/web/src/pages/SearchExternalResults.tsx
@@ -1,17 +1,22 @@
import { useState } from 'react'
import { CheckCircle2, Info, Rss } from 'lucide-react'
+import toast from 'react-hot-toast'
+import type { DiscoverItem } from '../api/discover'
import type { ExternalMediaResult } from '../api/ai'
import { imageURL } from '../api/client'
+import { subscriptionsAPI } from '../api/subscriptions'
+import { DiscoverSubscriptionRules } from './DiscoverDetailModalSections'
+import {
+ apiErrorMessage,
+ buildDiscoverSubscriptionInput,
+ initialDiscoverSubscriptionForm,
+} from './discoverDetailModalModel'
export function ExternalResults({
items,
- busyKey,
- onSubscribe,
}: {
items: ExternalMediaResult[]
- busyKey: string
- onSubscribe: (item: ExternalMediaResult) => Promise
}) {
const [detail, setDetail] = useState(null)
return (
@@ -72,13 +77,12 @@ export function ExternalResults({
@@ -88,9 +92,7 @@ export function ExternalResults({
{detail && (
setDetail(null)}
- onSubscribe={onSubscribe}
/>
)}
@@ -99,19 +101,37 @@ export function ExternalResults({
function ExternalDetailModal({
item,
- busy,
onClose,
- onSubscribe,
}: {
item: ExternalMediaResult
- busy: boolean
onClose: () => void
- onSubscribe: (item: ExternalMediaResult) => Promise