mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-09 06:46:37 +08:00
fix: group local search series results
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user