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:父级状态变化(如轮播切图、其它卡片操作)不再级联重渲染所有卡片。
// 注意 actions/renderActions 必须引用稳定(用 renderActions 传函数)memo 才生效。
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
progress?: number
count?: number
rating?: number
linkTo?: string
linkState?: { from?: string }
onClick?: () => void
actions?: ReactNode
renderActions?: (media: Media) => ReactNode
@@ -169,7 +170,7 @@ export const MediaCard = memo(function MediaCard({
if (actionContent) {
return (
<div className="group relative block">
<Link to={href} className="block">
<Link to={href} state={linkState} className="block">
{card}
</Link>
<div className={ACTION_OVERLAY_CLASS}>
@@ -180,7 +181,7 @@ export const MediaCard = memo(function MediaCard({
}
return (
<Link to={href} className="group block">
<Link to={href} state={linkState} className="group block">
{card}
</Link>
)
+1 -1
View File
@@ -74,7 +74,7 @@ export function FavouritesPage() {
{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">
{items.map((m) => (
<MediaCard key={m.id} media={m} />
<MediaCard key={m.id} media={m} linkState={{ from: '/favourites' }} />
))}
</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"
>
<ArrowLeft size={16} />
<span>返回媒体库</span>
<span>返回</span>
</button>
</div>
)
+2
View File
@@ -127,6 +127,7 @@ export function WatchHistoryPage() {
<div className="min-w-0 flex-1 space-y-1">
<Link
to={`/media/${m.id}`}
state={{ from: '/history' }}
className="block truncate font-medium text-ink-600 transition hover:text-brand-500"
>
{m.title}
@@ -150,6 +151,7 @@ export function WatchHistoryPage() {
<div className="flex w-full items-center gap-2 sm:w-auto sm:shrink-0">
<Link
to={`/play/${m.id}`}
state={{ from: '/history' }}
className="neon-button !px-3 !py-1 !text-xs"
>
<Play size={12} /> 继续
+42 -7
View File
@@ -1,5 +1,5 @@
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 { api } from '../api/client'
@@ -11,6 +11,8 @@ import type { Media } from '../types'
import { seasonSortOrder } from '../utils/groupSeries'
import { mediaLibraryBackTarget } from './MediaDetailPageModel'
type MediaDetailLocationState = { from?: string } | null
interface MediaDetailPageStateParams {
id: string
navigate: NavigateFunction
@@ -29,6 +31,7 @@ interface MediaDetailActionsParams {
media: Media | null
scrapeEpisodeArtwork: boolean
navigate: NavigateFunction
backFrom: string | undefined
refresh: () => Promise<void>
setFavourite: Dispatch<SetStateAction<boolean>>
}
@@ -54,10 +57,13 @@ export function useMediaDetailPageState({ id, navigate }: MediaDetailPageStatePa
setEpisodes,
setLoadingEpisodes,
})
const location = useLocation()
const backFrom = (location.state as MediaDetailLocationState)?.from
const actions = useMediaDetailActions({
media,
scrapeEpisodeArtwork,
navigate,
backFrom,
refresh,
setFavourite,
})
@@ -173,10 +179,14 @@ function useMediaDetailActions({
media,
scrapeEpisodeArtwork,
navigate,
backFrom,
refresh,
setFavourite,
}: MediaDetailActionsParams) {
const goBack = useCallback(() => goBackFromMediaDetail(media, navigate), [media, navigate])
const goBack = useCallback(
() => goBackFromMediaDetail(media, navigate, backFrom),
[backFrom, media, navigate],
)
const toggleFavourite = useCallback(
() => toggleMediaFavourite(media, setFavourite),
[media, setFavourite],
@@ -188,13 +198,34 @@ function useMediaDetailActions({
const reprobe = useCallback(() => reprobeMedia(media, refresh), [media, refresh])
const exportNFO = useCallback(() => exportMediaNFO(media), [media])
const deleteMedia = useCallback(
() => deleteMediaFromLibrary(media, navigate),
[media, navigate],
() => deleteMediaFromLibrary(media, navigate, backFrom),
[backFrom, media, navigate],
)
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) {
navigate('/libraries')
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
const result = await confirmActionResult({
title: '删除媒体',
@@ -275,7 +310,7 @@ async function deleteMediaFromLibrary(media: Media | null, navigate: NavigateFun
return
}
toast.success(result.checked ? '已删除媒体及本地文件' : '已从媒体库删除')
goBackFromMediaDetail(media, navigate, true)
goBackFromMediaDetail(media, navigate, from, true)
}
function apiErrorMessage(err: unknown, fallback: string): string {