mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 20:16:36 +08:00
Improve frontend navigation and mobile UX
- Add mobile bottom tab bar for media browsing (Home, Libraries, Favourites, Playlists, More) - Split mobile drawer: media links first, admin links under a Management section - Show contextual back button in mobile header for deep routes - Add reusable PageBackButton and PageHeader components - Add back navigation to playlist detail, library page, and missing media state - Embed queue panels in TaskQueuePage to avoid duplicate page titles - Expose theme toggle in user menu on small screens Co-authored-by: truewhile <truewhile@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
type SortOrder,
|
||||
} from '../utils/mediaSort'
|
||||
import { LibraryPageDialogs } from './LibraryPageDialogs'
|
||||
import { PageBackButton } from '../components/PageBackButton'
|
||||
import { LibraryPageHeader } from './LibraryPageHeader'
|
||||
import { LibraryMediaSections } from './LibraryMediaSections'
|
||||
import { LibrarySeriesDetailSection } from './LibrarySeriesDetailSection'
|
||||
@@ -181,6 +182,12 @@ export function LibraryPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{!selectedSeries && (
|
||||
<div className="hidden lg:block">
|
||||
<PageBackButton to="/libraries" label="全部媒体库" compact />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!selectedSeries && (
|
||||
<LibraryPageHeader
|
||||
library={library}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ArrowLeft, Heart, Play, RefreshCw } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { PageBackButton } from '../components/PageBackButton'
|
||||
|
||||
import { ExternalPlayerButton } from '../components/ExternalPlayerButton'
|
||||
import { ManualScrapeDialog } from '../components/ManualScrapeDialog'
|
||||
import { MetadataEditDialog } from '../components/MetadataEditDialog'
|
||||
@@ -63,8 +65,11 @@ export function MediaDetailLoading() {
|
||||
|
||||
export function MediaDetailMissing() {
|
||||
return (
|
||||
<div className="text-center py-24 bg-white rounded-2xl border border-gray-200">
|
||||
<p className="text-gray-500">媒体资源已被移除或不存在</p>
|
||||
<div className="space-y-4 py-12 text-center">
|
||||
<PageBackButton label="返回上一页" className="mx-auto" />
|
||||
<div className="rounded-2xl border border-gray-200 bg-white py-16">
|
||||
<p className="text-gray-500">媒体资源已被移除或不存在</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import toast from 'react-hot-toast'
|
||||
|
||||
import { playbackAPI, type PlaylistDetail } from '../api/playback'
|
||||
import { MediaCard } from '../components/MediaCard'
|
||||
import { PageHeader } from '../components/PageHeader'
|
||||
|
||||
export function PlaylistDetailPage() {
|
||||
const { id = '' } = useParams()
|
||||
@@ -24,10 +25,12 @@ export function PlaylistDetailPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header>
|
||||
<h1 className="font-display text-3xl font-bold text-ink-600">{detail.playlist.name}</h1>
|
||||
<p className="text-sm text-ink-50">{detail.items.length} 部影片</p>
|
||||
</header>
|
||||
<PageHeader
|
||||
backTo="/playlists"
|
||||
backLabel="播放列表"
|
||||
title={detail.playlist.name}
|
||||
description={`${detail.items.length} 部影片`}
|
||||
/>
|
||||
|
||||
{detail.items.length === 0 && (
|
||||
<p className="text-ink-50">暂无内容,前往媒体详情页通过「加入播放列表」添加。</p>
|
||||
|
||||
@@ -60,7 +60,7 @@ const TYPE_LABELS: Record<string, string> = {
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export function ScraperQueuePage() {
|
||||
export function ScraperQueuePage({ embedded = false }: { embedded?: boolean }) {
|
||||
const [snapshot, setSnapshot] = useState<ScrapeQueueSnapshot | null>(null)
|
||||
const [filter, setFilter] = useState<'all' | ScrapeTaskStatus>('all')
|
||||
const [search, setSearch] = useState('')
|
||||
@@ -251,7 +251,7 @@ export function ScraperQueuePage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 1. Header */}
|
||||
{!embedded && (
|
||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-primary-400/30 bg-primary-400/10 text-brand-500 shadow-sm">
|
||||
@@ -385,6 +385,7 @@ export function ScraperQueuePage() {
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* 2. Status Cards */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
|
||||
|
||||
@@ -55,7 +55,13 @@ function getFileIcon(filename: string): ReactNode {
|
||||
return <File size={15} className="text-gray-400 shrink-0" />
|
||||
}
|
||||
|
||||
export function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) {
|
||||
export function StrmQueuePanel({
|
||||
kind,
|
||||
embedded = false,
|
||||
}: {
|
||||
kind: 'download' | 'upload'
|
||||
embedded?: boolean
|
||||
}) {
|
||||
const [snapshot, setSnapshot] = useState<StrmQueueSnapshot | null>(null)
|
||||
const [filter, setFilter] = useState<'all' | StrmTaskStatus>('all')
|
||||
const [search, setSearch] = useState('')
|
||||
@@ -242,7 +248,7 @@ export function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 1. Header */}
|
||||
{!embedded && (
|
||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-primary-400/30 bg-primary-400/10 text-brand-500 shadow-sm">
|
||||
@@ -403,6 +409,7 @@ export function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) {
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* 2. Interactive Status Cards */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
|
||||
|
||||
@@ -115,9 +115,9 @@ export function TaskQueuePage() {
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
{type === 'scrape' && <ScraperQueuePage />}
|
||||
{type === 'download' && <StrmQueuePanel kind="download" />}
|
||||
{type === 'upload' && <StrmQueuePanel kind="upload" />}
|
||||
{type === 'scrape' && <ScraperQueuePage embedded />}
|
||||
{type === 'download' && <StrmQueuePanel kind="download" embedded />}
|
||||
{type === 'upload' && <StrmQueuePanel kind="upload" embedded />}
|
||||
{type === 'all' && (
|
||||
<div className="space-y-10">
|
||||
<section>
|
||||
@@ -125,21 +125,21 @@ export function TaskQueuePage() {
|
||||
<Sparkles size={18} className="text-brand-500" />
|
||||
刮削队列
|
||||
</h2>
|
||||
<ScraperQueuePage />
|
||||
<ScraperQueuePage embedded />
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="mb-2 flex items-center gap-2 font-display text-xl font-bold text-ink-600">
|
||||
<Download size={18} className="text-brand-500" />
|
||||
下载队列
|
||||
</h2>
|
||||
<StrmQueuePanel kind="download" />
|
||||
<StrmQueuePanel kind="download" embedded />
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="mb-2 flex items-center gap-2 font-display text-xl font-bold text-ink-600">
|
||||
<Upload size={18} className="text-brand-500" />
|
||||
上传队列
|
||||
</h2>
|
||||
<StrmQueuePanel kind="upload" />
|
||||
<StrmQueuePanel kind="upload" embedded />
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user