fix: group local search series results

This commit is contained in:
ShukeBta
2026-05-29 10:55:01 +08:00
parent 4361ba73ba
commit 2f7ec3ab17
4 changed files with 42 additions and 21 deletions
+18 -6
View File
@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useState } from 'react' import { FormEvent, useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Loader2, Rss, Search, Sparkles, Wand2 } from 'lucide-react' import { Loader2, Rss, Search, Sparkles, Wand2 } from 'lucide-react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
@@ -8,6 +8,7 @@ import { imageURL } from '../api/client'
import { subscriptionsAPI } from '../api/subscriptions' import { subscriptionsAPI } from '../api/subscriptions'
import { MediaCard } from '../components/MediaCard' import { MediaCard } from '../components/MediaCard'
import type { Media } from '../types' import type { Media } from '../types'
import { groupSeries, seriesCardLink } from '../utils/groupSeries'
// AIAssistantPage exposes the two AI helpers backed by the Go server: // AIAssistantPage exposes the two AI helpers backed by the Go server:
// - smart search: parses a natural-language query into a SearchIntent + // - smart search: parses a natural-language query into a SearchIntent +
@@ -30,6 +31,7 @@ export function AIAssistantPage() {
const [items, setItems] = useState<Media[]>([]) const [items, setItems] = useState<Media[]>([])
const [externalItems, setExternalItems] = useState<ExternalMediaResult[]>([]) const [externalItems, setExternalItems] = useState<ExternalMediaResult[]>([])
const [subscribing, setSubscribing] = useState('') const [subscribing, setSubscribing] = useState('')
const localCards = useMemo(() => groupSeries(items), [items])
const [recs, setRecs] = useState<string[] | null>(null) const [recs, setRecs] = useState<string[] | null>(null)
const [recommending, setRecommending] = useState(false) const [recommending, setRecommending] = useState(false)
@@ -186,11 +188,21 @@ export function AIAssistantPage() {
</div> </div>
)} )}
{items.length > 0 && ( {localCards.length > 0 && (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"> <div className="space-y-2">
{items.map((m) => ( <div className="text-sm font-semibold text-ink-100">
<MediaCard key={m.id} media={m} /> 本地媒体库 · {localCards.length} 个合集 / {items.length} 个条目
))} </div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
{localCards.map((card) => (
<MediaCard
key={card.key}
media={card.rep}
count={card.count}
linkTo={seriesCardLink(card)}
/>
))}
</div>
</div> </div>
)} )}
+2 -9
View File
@@ -7,7 +7,7 @@ import { imageURL } from '../api/client'
import { libraryAPI } from '../api/library' import { libraryAPI } from '../api/library'
import { MediaCard } from '../components/MediaCard' import { MediaCard } from '../components/MediaCard'
import type { Library, Media } from '../types' import type { Library, Media } from '../types'
import { artworkScore, groupSeries, type SeriesCard } from '../utils/groupSeries' import { artworkScore, groupSeries, seriesCardLink, type SeriesCard } from '../utils/groupSeries'
type LibraryPreview = { type LibraryPreview = {
library: Library library: Library
@@ -206,7 +206,7 @@ function LibraryShelf({ preview }: { preview: LibraryPreview }) {
<MediaCard <MediaCard
media={card.rep} media={card.rep}
count={card.count} count={card.count}
linkTo={mediaCardLink(card)} linkTo={seriesCardLink(card)}
/> />
</div> </div>
))} ))}
@@ -226,13 +226,6 @@ function latestCards(items: Media[]): SeriesCard[] {
.slice(0, 10) .slice(0, 10)
} }
function mediaCardLink(card: SeriesCard): string {
if (card.count > 1) {
return `/library/${card.rep.library_id}?series=${encodeURIComponent(card.key)}`
}
return `/media/${card.rep.id}`
}
function mediaTime(media: Media): number { function mediaTime(media: Media): number {
return Date.parse(media.updated_at || media.created_at || '') || 0 return Date.parse(media.updated_at || media.created_at || '') || 0
} }
+15 -6
View File
@@ -1,4 +1,4 @@
import { ChangeEvent, FormEvent, useCallback, useEffect, useState } from 'react' import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useState } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { Rss, Sparkles } from 'lucide-react' import { Rss, Sparkles } from 'lucide-react'
@@ -8,6 +8,7 @@ import { mediaAPI } from '../api/library'
import { subscriptionsAPI } from '../api/subscriptions' import { subscriptionsAPI } from '../api/subscriptions'
import { MediaCard } from '../components/MediaCard' import { MediaCard } from '../components/MediaCard'
import type { Media } from '../types' import type { Media } from '../types'
import { groupSeries, seriesCardLink } from '../utils/groupSeries'
export function SearchPage() { export function SearchPage() {
const [q, setQ] = useState('') const [q, setQ] = useState('')
@@ -20,6 +21,7 @@ export function SearchPage() {
const [hasSearched, setHasSearched] = useState(false) const [hasSearched, setHasSearched] = useState(false)
const [externalItems, setExternalItems] = useState<ExternalMediaResult[]>([]) const [externalItems, setExternalItems] = useState<ExternalMediaResult[]>([])
const [subscribing, setSubscribing] = useState('') const [subscribing, setSubscribing] = useState('')
const localCards = useMemo(() => groupSeries(items), [items])
useEffect(() => { useEffect(() => {
aiAPI aiAPI
@@ -84,7 +86,7 @@ export function SearchPage() {
} }
} }
const showEmpty = !loading && !error && hasSearched && items.length === 0 const showEmpty = !loading && !error && hasSearched && localCards.length === 0
const showIdle = !loading && !error && !hasSearched const showIdle = !loading && !error && !hasSearched
return ( return (
@@ -160,12 +162,19 @@ export function SearchPage() {
</div> </div>
)} )}
{items.length > 0 && ( {localCards.length > 0 && (
<> <>
<div className="text-sm font-semibold text-ink-100">本地媒体库</div> <div className="text-sm font-semibold text-ink-100">
本地媒体库 · {localCards.length} 个合集 / {items.length} 个条目
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"> <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
{items.map((m) => ( {localCards.map((card) => (
<MediaCard key={m.id} media={m} /> <MediaCard
key={card.key}
media={card.rep}
count={card.count}
linkTo={seriesCardLink(card)}
/>
))} ))}
</div> </div>
</> </>
+7
View File
@@ -82,6 +82,13 @@ export function groupSeries(items: Media[]): SeriesCard[] {
return Array.from(groups.values()) return Array.from(groups.values())
} }
export function seriesCardLink(card: SeriesCard): string {
if (card.count > 1) {
return `/library/${card.rep.library_id}?series=${encodeURIComponent(card.key)}`
}
return `/media/${card.rep.id}`
}
export function artworkScore(media: Media): number { export function artworkScore(media: Media): number {
const poster = (media.poster_url ?? '').toLowerCase() const poster = (media.poster_url ?? '').toLowerCase()
const backdrop = (media.backdrop_url ?? '').toLowerCase() const backdrop = (media.backdrop_url ?? '').toLowerCase()