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:
Cursor Agent
2026-09-02 02:19:25 +00:00
parent 1171fe9464
commit eba7904c99
15 changed files with 388 additions and 37 deletions
+103 -3
View File
@@ -1,9 +1,13 @@
import type { LucideIcon } from 'lucide-react'
import {
Clock,
FileOutput,
FolderOpen,
Heart,
Home,
Library,
ListChecks,
ListMusic,
Settings,
Tv,
User,
@@ -19,6 +23,20 @@ export type LayoutNavItem = {
adminOnly?: boolean
}
export type HeaderBackTarget = {
to: string
label: string
}
export type MobileBottomNavItem = {
to: string
label: string
icon: LucideIcon
end?: boolean
match?: (pathname: string) => boolean
}
/** Desktop admin sidebar and admin mobile drawer. */
export const LAYOUT_NAV_ITEMS: LayoutNavItem[] = [
{ to: '/profile', label: '个人资料', icon: User },
{ to: '/libraries?from=admin', label: '媒体库', icon: Library },
@@ -26,8 +44,90 @@ export const LAYOUT_NAV_ITEMS: LayoutNavItem[] = [
{ to: '/admin', label: '用户管理', icon: Users, adminOnly: true },
{ to: '/files', label: '文件管理', icon: FolderOpen, adminOnly: true },
{ to: '/settings', label: '系统设置', icon: Settings, adminOnly: true },
// Emby 挂载(设置区):远程 Emby 媒体库挂载
{ to: '/emby-mount', label: 'Emby 挂载', icon: Tv, adminOnly: true },
// STRM 管理(设置区):网盘目录生成 strm 与元数据同步
{ to: '/strm', label: 'STRM 管理', icon: FileOutput, adminOnly: true },
]
]
/** Media browsing links for the mobile drawer when browsing libraries. */
export const MEDIA_NAV_ITEMS: LayoutNavItem[] = [
{ to: '/', label: '首页', icon: Home, end: true },
{ to: '/libraries', label: '媒体库', icon: Library },
{ to: '/favourites', label: '我的收藏', icon: Heart },
{ to: '/playlists', label: '播放列表', icon: ListMusic },
{ to: '/history', label: '观看历史', icon: Clock },
]
export const MOBILE_BOTTOM_NAV_ITEMS: MobileBottomNavItem[] = [
{ to: '/', label: '首页', icon: Home, end: true },
{
to: '/libraries',
label: '媒体库',
icon: Library,
match: (p) => p === '/libraries' || p.startsWith('/library'),
},
{ to: '/favourites', label: '收藏', icon: Heart },
{
to: '/playlists',
label: '列表',
icon: ListMusic,
match: (p) => p === '/playlists' || p.startsWith('/playlist'),
},
]
const ROOT_MEDIA_PATHS = new Set(['/', '/libraries', '/favourites', '/playlists', '/history'])
export function isRootMediaPath(pathname: string): boolean {
return ROOT_MEDIA_PATHS.has(pathname)
}
export function resolveHeaderBack(pathname: string): HeaderBackTarget | null {
if (pathname.startsWith('/library/')) {
return { to: '/libraries', label: '媒体库' }
}
if (pathname.startsWith('/playlist/')) {
return { to: '/playlists', label: '播放列表' }
}
if (pathname.startsWith('/media/')) {
return null
}
if (pathname === '/scraper/queue') {
return { to: '/queue', label: '任务队列' }
}
if (pathname === '/strm/downloads' || pathname === '/strm/uploads') {
return { to: '/strm', label: 'STRM 管理' }
}
if (
pathname === '/profile' ||
pathname === '/dlna' ||
pathname === '/play-profiles' ||
pathname === '/poster-wall'
) {
return { to: '/', label: '首页' }
}
if (
pathname === '/settings' ||
pathname === '/admin' ||
pathname === '/files' ||
pathname === '/queue' ||
pathname === '/emby-mount' ||
pathname === '/strm'
) {
return { to: '/', label: '首页' }
}
return null
}
export function shouldShowMobileBottomNav(pathname: string): boolean {
if (pathname.startsWith('/play')) return false
return (
pathname === '/' ||
pathname === '/libraries' ||
pathname.startsWith('/library') ||
pathname.startsWith('/media') ||
pathname === '/favourites' ||
pathname === '/playlists' ||
pathname.startsWith('/playlist') ||
pathname === '/history' ||
pathname === '/poster-wall'
)
}