mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-03 04:26:36 +08:00
Add favourite buttons to library list and series detail pages (#13)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
This commit is contained in:
@@ -11,7 +11,7 @@ type LibraryMediaSectionsProps = {
|
||||
seriesCards: SeriesCard[]
|
||||
selectedSeries: SeriesCard | null
|
||||
loading: boolean
|
||||
movieActions: (media: Media) => ReactNode
|
||||
cardActions: (media: Media) => ReactNode
|
||||
onSeriesClick: (series: SeriesCard) => void
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function LibraryMediaSections({
|
||||
seriesCards,
|
||||
selectedSeries,
|
||||
loading,
|
||||
movieActions,
|
||||
cardActions,
|
||||
onSeriesClick,
|
||||
}: LibraryMediaSectionsProps) {
|
||||
return (
|
||||
@@ -29,7 +29,7 @@ export function LibraryMediaSections({
|
||||
{!isSeries && items.length > 0 && (
|
||||
<div className="grid grid-cols-3 gap-4 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7 2xl:grid-cols-8">
|
||||
{items.map((media) => (
|
||||
<MediaCard key={media.id} media={media} actions={movieActions(media)} />
|
||||
<MediaCard key={media.id} media={media} actions={cardActions(media)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -45,6 +45,7 @@ export function LibraryMediaSections({
|
||||
key={series.key}
|
||||
media={series.rep}
|
||||
count={series.count}
|
||||
actions={cardActions(series.rep)}
|
||||
onClick={() => onSeriesClick(series)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState, Fragment, type ReactNode } from 'react'
|
||||
import { useLocation, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../utils/mediaSort'
|
||||
import { LibraryPageDialogs } from './LibraryPageDialogs'
|
||||
import { PageBackButton } from '../components/PageBackButton'
|
||||
import { MediaFavouriteButton } from '../components/MediaFavouriteButton'
|
||||
import { LibraryPageHeader } from './LibraryPageHeader'
|
||||
import { LibraryMediaSections } from './LibraryMediaSections'
|
||||
import { LibrarySeriesDetailSection } from './LibrarySeriesDetailSection'
|
||||
@@ -21,12 +22,17 @@ import { useLibraryData } from './useLibraryData'
|
||||
import { useLibraryScanStatus } from './useLibraryScanStatus'
|
||||
import { useLibrarySeriesSelection } from './useLibrarySeriesSelection'
|
||||
import { useLibraryAdminActions } from './useLibraryAdminActions'
|
||||
import { usePermission } from '../hooks/usePermission'
|
||||
import { useFavourites } from '../hooks/useFavourites'
|
||||
|
||||
export function LibraryPage() {
|
||||
const { id = '' } = useParams()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const location = useLocation()
|
||||
const role = useAuthStore((s) => s.user?.role)
|
||||
const canFavorite = usePermission('can_favorite')
|
||||
const { isFavourite, toggleFavourite } = useFavourites()
|
||||
const [favouriteBusyID, setFavouriteBusyID] = useState('')
|
||||
|
||||
const [scrapeDialogOpen, setScrapeDialogOpen] = useState(false)
|
||||
const [manualSeriesScrapeOpen, setManualSeriesScrapeOpen] = useState(false)
|
||||
@@ -169,6 +175,39 @@ export function LibraryPage() {
|
||||
setManualMovie,
|
||||
})
|
||||
|
||||
const handleToggleFavourite = async (mediaID: string) => {
|
||||
if (!canFavorite || favouriteBusyID) return
|
||||
setFavouriteBusyID(mediaID)
|
||||
try {
|
||||
await toggleFavourite(mediaID)
|
||||
} finally {
|
||||
setFavouriteBusyID('')
|
||||
}
|
||||
}
|
||||
|
||||
const cardActions = (media: Media): ReactNode => {
|
||||
const actions: ReactNode[] = []
|
||||
if (canFavorite) {
|
||||
actions.push(
|
||||
<MediaFavouriteButton
|
||||
key="favourite"
|
||||
variant="compact"
|
||||
favourite={isFavourite(media.id)}
|
||||
disabled={favouriteBusyID === media.id}
|
||||
onToggle={() => {
|
||||
void handleToggleFavourite(media.id)
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
const adminActions = movieActions(media)
|
||||
if (adminActions) {
|
||||
actions.push(<Fragment key="admin-actions">{adminActions}</Fragment>)
|
||||
}
|
||||
if (actions.length === 0) return undefined
|
||||
return <>{actions}</>
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-32">
|
||||
@@ -216,7 +255,7 @@ export function LibraryPage() {
|
||||
seriesCards={sortedSeriesCards}
|
||||
selectedSeries={selectedSeries}
|
||||
loading={loading}
|
||||
movieActions={movieActions}
|
||||
cardActions={cardActions}
|
||||
onSeriesClick={handleSeriesClick}
|
||||
/>
|
||||
|
||||
@@ -229,6 +268,13 @@ export function LibraryPage() {
|
||||
loadingEpisodes={loadingSeriesEpisodes}
|
||||
playbackFrom={`${location.pathname}${location.search}`}
|
||||
isAdmin={role === 'admin'}
|
||||
canFavorite={canFavorite}
|
||||
favourite={selectedSeries ? isFavourite(selectedSeries.rep.id) : false}
|
||||
favouriteBusy={!!selectedSeries && favouriteBusyID === selectedSeries.rep.id}
|
||||
onToggleFavourite={() => {
|
||||
if (!selectedSeries) return
|
||||
void handleToggleFavourite(selectedSeries.rep.id)
|
||||
}}
|
||||
seriesToolBusy={seriesToolBusy}
|
||||
onBack={clearSelectedSeries}
|
||||
onSmartScrape={handleSeriesSmartScrape}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ArrowLeft, Database, FileText, Film, FolderInput, Pencil, Play, Search,
|
||||
|
||||
import { imageURL } from '../api/client'
|
||||
import { ExternalPlayerButton } from '../components/ExternalPlayerButton'
|
||||
import { MediaFavouriteButton } from '../components/MediaFavouriteButton'
|
||||
import type { Media } from '../types'
|
||||
import { seriesTitle, type SeriesCard } from '../utils/groupSeries'
|
||||
|
||||
@@ -13,6 +14,10 @@ type LibrarySeriesDetailHeaderProps = {
|
||||
allEpisodes: Media[]
|
||||
playbackFrom: string
|
||||
isAdmin: boolean
|
||||
canFavorite: boolean
|
||||
favourite: boolean
|
||||
favouriteBusy: boolean
|
||||
onToggleFavourite: () => void
|
||||
seriesToolBusy: string
|
||||
onBack: () => void
|
||||
onSmartScrape: () => void
|
||||
@@ -30,6 +35,10 @@ export function LibrarySeriesDetailHeader({
|
||||
allEpisodes,
|
||||
playbackFrom,
|
||||
isAdmin,
|
||||
canFavorite,
|
||||
favourite,
|
||||
favouriteBusy,
|
||||
onToggleFavourite,
|
||||
seriesToolBusy,
|
||||
onBack,
|
||||
onSmartScrape,
|
||||
@@ -75,15 +84,24 @@ export function LibrarySeriesDetailHeader({
|
||||
{series.rep.overview || '暂无简介'}
|
||||
</p>
|
||||
|
||||
{firstEpisode && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link to={`/play/${firstEpisode.id}`} state={{ from: playbackFrom }} className="btn-primary inline-flex">
|
||||
<Play size={16} fill="currentColor" />
|
||||
从第一集开始播放
|
||||
</Link>
|
||||
<ExternalPlayerButton mediaId={firstEpisode.id} label="外部播放器播放" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{firstEpisode && (
|
||||
<>
|
||||
<Link to={`/play/${firstEpisode.id}`} state={{ from: playbackFrom }} className="btn-primary inline-flex">
|
||||
<Play size={16} fill="currentColor" />
|
||||
从第一集开始播放
|
||||
</Link>
|
||||
<ExternalPlayerButton mediaId={firstEpisode.id} label="外部播放器播放" />
|
||||
</>
|
||||
)}
|
||||
{canFavorite && (
|
||||
<MediaFavouriteButton
|
||||
favourite={favourite}
|
||||
disabled={favouriteBusy}
|
||||
onToggle={onToggleFavourite}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAdmin && allEpisodes.length > 0 && !isRemoteEmbyID(series.rep.id) && (
|
||||
<div className="rounded-2xl border border-sand-200 bg-white/80 p-4 shadow-sm">
|
||||
|
||||
@@ -19,6 +19,10 @@ type LibrarySeriesDetailSectionProps = {
|
||||
loadingEpisodes: boolean
|
||||
playbackFrom: string
|
||||
isAdmin: boolean
|
||||
canFavorite: boolean
|
||||
favourite: boolean
|
||||
favouriteBusy: boolean
|
||||
onToggleFavourite: () => void
|
||||
seriesToolBusy: string
|
||||
onBack: () => void
|
||||
onSmartScrape: () => void
|
||||
@@ -40,6 +44,10 @@ export function LibrarySeriesDetailSection({
|
||||
loadingEpisodes,
|
||||
playbackFrom,
|
||||
isAdmin,
|
||||
canFavorite,
|
||||
favourite,
|
||||
favouriteBusy,
|
||||
onToggleFavourite,
|
||||
seriesToolBusy,
|
||||
onBack,
|
||||
onSmartScrape,
|
||||
@@ -66,6 +74,10 @@ export function LibrarySeriesDetailSection({
|
||||
allEpisodes={allEpisodes}
|
||||
playbackFrom={playbackFrom}
|
||||
isAdmin={isAdmin}
|
||||
canFavorite={canFavorite}
|
||||
favourite={favourite}
|
||||
favouriteBusy={favouriteBusy}
|
||||
onToggleFavourite={onToggleFavourite}
|
||||
seriesToolBusy={seriesToolBusy}
|
||||
onBack={onBack}
|
||||
onSmartScrape={onSmartScrape}
|
||||
|
||||
Reference in New Issue
Block a user