This commit is contained in:
truewhile
2026-09-19 21:30:32 +08:00
parent 474513f18a
commit 0c4668c156
5 changed files with 50 additions and 12 deletions
+4 -3
View File
@@ -10,13 +10,14 @@ const ACTION_OVERLAY_CLASS =
// memo:父级状态变化(如轮播切图、其它卡片操作)不再级联重渲染所有卡片。 // memo:父级状态变化(如轮播切图、其它卡片操作)不再级联重渲染所有卡片。
// 注意 actions/renderActions 必须引用稳定(用 renderActions 传函数)memo 才生效。 // 注意 actions/renderActions 必须引用稳定(用 renderActions 传函数)memo 才生效。
export const MediaCard = memo(function MediaCard({ export const MediaCard = memo(function MediaCard({
media, progress, count, rating, linkTo, onClick, actions, renderActions, compact, media, progress, count, rating, linkTo, linkState, onClick, actions, renderActions, compact,
}: { }: {
media: Media media: Media
progress?: number progress?: number
count?: number count?: number
rating?: number rating?: number
linkTo?: string linkTo?: string
linkState?: { from?: string }
onClick?: () => void onClick?: () => void
actions?: ReactNode actions?: ReactNode
renderActions?: (media: Media) => ReactNode renderActions?: (media: Media) => ReactNode
@@ -169,7 +170,7 @@ export const MediaCard = memo(function MediaCard({
if (actionContent) { if (actionContent) {
return ( return (
<div className="group relative block"> <div className="group relative block">
<Link to={href} className="block"> <Link to={href} state={linkState} className="block">
{card} {card}
</Link> </Link>
<div className={ACTION_OVERLAY_CLASS}> <div className={ACTION_OVERLAY_CLASS}>
@@ -180,7 +181,7 @@ export const MediaCard = memo(function MediaCard({
} }
return ( return (
<Link to={href} className="group block"> <Link to={href} state={linkState} className="group block">
{card} {card}
</Link> </Link>
) )
+1 -1
View File
@@ -74,7 +74,7 @@ export function FavouritesPage() {
{items.length > 0 && ( {items.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="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) => ( {items.map((m) => (
<MediaCard key={m.id} media={m} /> <MediaCard key={m.id} media={m} linkState={{ from: '/favourites' }} />
))} ))}
</div> </div>
)} )}
+1 -1
View File
@@ -85,7 +85,7 @@ export function MediaDetailBackButton({ onBack }: { onBack: () => void }) {
className="btn-ghost gap-2 bg-white/80 shadow-sm backdrop-blur hover:bg-white" className="btn-ghost gap-2 bg-white/80 shadow-sm backdrop-blur hover:bg-white"
> >
<ArrowLeft size={16} /> <ArrowLeft size={16} />
<span>返回媒体库</span> <span>返回</span>
</button> </button>
</div> </div>
) )
+2
View File
@@ -127,6 +127,7 @@ export function WatchHistoryPage() {
<div className="min-w-0 flex-1 space-y-1"> <div className="min-w-0 flex-1 space-y-1">
<Link <Link
to={`/media/${m.id}`} to={`/media/${m.id}`}
state={{ from: '/history' }}
className="block truncate font-medium text-ink-600 transition hover:text-brand-500" className="block truncate font-medium text-ink-600 transition hover:text-brand-500"
> >
{m.title} {m.title}
@@ -150,6 +151,7 @@ export function WatchHistoryPage() {
<div className="flex w-full items-center gap-2 sm:w-auto sm:shrink-0"> <div className="flex w-full items-center gap-2 sm:w-auto sm:shrink-0">
<Link <Link
to={`/play/${m.id}`} to={`/play/${m.id}`}
state={{ from: '/history' }}
className="neon-button !px-3 !py-1 !text-xs" className="neon-button !px-3 !py-1 !text-xs"
> >
<Play size={12} /> 继续 <Play size={12} /> 继续
+42 -7
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState, type Dispatch, type SetStateAction } from 'react' import { useCallback, useEffect, useMemo, useState, type Dispatch, type SetStateAction } from 'react'
import type { NavigateFunction } from 'react-router-dom' import { useLocation, type NavigateFunction } from 'react-router-dom'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { api } from '../api/client' import { api } from '../api/client'
@@ -11,6 +11,8 @@ import type { Media } from '../types'
import { seasonSortOrder } from '../utils/groupSeries' import { seasonSortOrder } from '../utils/groupSeries'
import { mediaLibraryBackTarget } from './MediaDetailPageModel' import { mediaLibraryBackTarget } from './MediaDetailPageModel'
type MediaDetailLocationState = { from?: string } | null
interface MediaDetailPageStateParams { interface MediaDetailPageStateParams {
id: string id: string
navigate: NavigateFunction navigate: NavigateFunction
@@ -29,6 +31,7 @@ interface MediaDetailActionsParams {
media: Media | null media: Media | null
scrapeEpisodeArtwork: boolean scrapeEpisodeArtwork: boolean
navigate: NavigateFunction navigate: NavigateFunction
backFrom: string | undefined
refresh: () => Promise<void> refresh: () => Promise<void>
setFavourite: Dispatch<SetStateAction<boolean>> setFavourite: Dispatch<SetStateAction<boolean>>
} }
@@ -54,10 +57,13 @@ export function useMediaDetailPageState({ id, navigate }: MediaDetailPageStatePa
setEpisodes, setEpisodes,
setLoadingEpisodes, setLoadingEpisodes,
}) })
const location = useLocation()
const backFrom = (location.state as MediaDetailLocationState)?.from
const actions = useMediaDetailActions({ const actions = useMediaDetailActions({
media, media,
scrapeEpisodeArtwork, scrapeEpisodeArtwork,
navigate, navigate,
backFrom,
refresh, refresh,
setFavourite, setFavourite,
}) })
@@ -173,10 +179,14 @@ function useMediaDetailActions({
media, media,
scrapeEpisodeArtwork, scrapeEpisodeArtwork,
navigate, navigate,
backFrom,
refresh, refresh,
setFavourite, setFavourite,
}: MediaDetailActionsParams) { }: MediaDetailActionsParams) {
const goBack = useCallback(() => goBackFromMediaDetail(media, navigate), [media, navigate]) const goBack = useCallback(
() => goBackFromMediaDetail(media, navigate, backFrom),
[backFrom, media, navigate],
)
const toggleFavourite = useCallback( const toggleFavourite = useCallback(
() => toggleMediaFavourite(media, setFavourite), () => toggleMediaFavourite(media, setFavourite),
[media, setFavourite], [media, setFavourite],
@@ -188,13 +198,34 @@ function useMediaDetailActions({
const reprobe = useCallback(() => reprobeMedia(media, refresh), [media, refresh]) const reprobe = useCallback(() => reprobeMedia(media, refresh), [media, refresh])
const exportNFO = useCallback(() => exportMediaNFO(media), [media]) const exportNFO = useCallback(() => exportMediaNFO(media), [media])
const deleteMedia = useCallback( const deleteMedia = useCallback(
() => deleteMediaFromLibrary(media, navigate), () => deleteMediaFromLibrary(media, navigate, backFrom),
[media, navigate], [backFrom, media, navigate],
) )
return { goBack, toggleFavourite, rescrape, reprobe, exportNFO, deleteMedia } return { goBack, toggleFavourite, rescrape, reprobe, exportNFO, deleteMedia }
} }
function goBackFromMediaDetail(media: Media | null, navigate: NavigateFunction, replace = false): void { function canNavigateHistoryBack(): boolean {
if (typeof window === 'undefined') return false
const idx = (window.history.state as { idx?: number } | null)?.idx
return typeof idx === 'number' ? idx > 0 : window.history.length > 1
}
function goBackFromMediaDetail(
media: Media | null,
navigate: NavigateFunction,
from: string | undefined,
replace = false,
): void {
// Prefer the explicit return path (e.g. Favourites → Detail), then browser
// history, and only fall back to the owning library for direct/deep links.
if (typeof from === 'string' && from.length > 0) {
navigate(from, replace ? { replace: true } : undefined)
return
}
if (canNavigateHistoryBack()) {
navigate(-1)
return
}
if (!media) { if (!media) {
navigate('/libraries') navigate('/libraries')
return return
@@ -259,7 +290,11 @@ async function exportMediaNFO(media: Media | null): Promise<void> {
} }
} }
async function deleteMediaFromLibrary(media: Media | null, navigate: NavigateFunction): Promise<void> { async function deleteMediaFromLibrary(
media: Media | null,
navigate: NavigateFunction,
from: string | undefined,
): Promise<void> {
if (!media) return if (!media) return
const result = await confirmActionResult({ const result = await confirmActionResult({
title: '删除媒体', title: '删除媒体',
@@ -275,7 +310,7 @@ async function deleteMediaFromLibrary(media: Media | null, navigate: NavigateFun
return return
} }
toast.success(result.checked ? '已删除媒体及本地文件' : '已从媒体库删除') toast.success(result.checked ? '已删除媒体及本地文件' : '已从媒体库删除')
goBackFromMediaDetail(media, navigate, true) goBackFromMediaDetail(media, navigate, from, true)
} }
function apiErrorMessage(err: unknown, fallback: string): string { function apiErrorMessage(err: unknown, fallback: string): string {