mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
优化
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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} /> 继续
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user