diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index e003bd5..b13a808 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -8,7 +8,7 @@ import { LayoutWorkspace, } from './LayoutSections' import { MobileBottomNav } from './MobileBottomNav' -import { shouldShowMobileBottomNav } from './layoutNavigation' +import { isPlayerRoute, shouldShowMobileBottomNav } from './layoutNavigation' import { useLayoutPermissions } from './useLayoutPermissions' import { useLayoutProfiles } from './useLayoutProfiles' import { useLayoutSidebar } from './useLayoutSidebar' @@ -26,7 +26,7 @@ function isMediaView(pathname: string, search: string): boolean { pathname === '/libraries' || pathname.startsWith('/library') || pathname.startsWith('/media') || - pathname.startsWith('/play') || + isPlayerRoute(pathname) || pathname === '/favourites' || pathname === '/playlists' || pathname.startsWith('/playlist') || @@ -52,7 +52,7 @@ export function Layout() { const showSidebar = !isMediaView(location.pathname, location.search) const hideSearch = location.pathname.startsWith('/settings') - const isPlayPage = location.pathname.startsWith('/play') + const isPlayPage = isPlayerRoute(location.pathname) const showMobileBottomNav = shouldShowMobileBottomNav(location.pathname) return ( diff --git a/web/src/components/LayoutSections.tsx b/web/src/components/LayoutSections.tsx index fa592af..01b6bd3 100644 --- a/web/src/components/LayoutSections.tsx +++ b/web/src/components/LayoutSections.tsx @@ -3,6 +3,7 @@ import { AnimatePresence, motion } from 'framer-motion' import clsx from 'clsx' import { LayoutSidebarContent, type LayoutSidebarContentProps } from './LayoutSidebarContent' +import { isPlayerRoute } from './layoutNavigation' import { RouteErrorBoundary } from './RouteErrorBoundary' import type { useLayoutSidebar } from './useLayoutSidebar' @@ -125,7 +126,7 @@ export function LayoutWorkspace({ routeKey, showMobileBottomNav = false }: Layou ? 'pb-[calc(3.75rem+env(safe-area-inset-bottom,0px))] lg:pb-10' : '' - if (routeKey.startsWith('/play')) { + if (isPlayerRoute(routeKey)) { return (
diff --git a/web/src/components/layoutNavigation.ts b/web/src/components/layoutNavigation.ts index 1c21806..6a437fd 100644 --- a/web/src/components/layoutNavigation.ts +++ b/web/src/components/layoutNavigation.ts @@ -76,6 +76,11 @@ export const MOBILE_BOTTOM_NAV_ITEMS: MobileBottomNavItem[] = [ const ROOT_MEDIA_PATHS = new Set(['/', '/libraries', '/favourites', '/playlists', '/history']) +/** True only for the fullscreen player route (`/play` or `/play/:id`), not `/playlists`. */ +export function isPlayerRoute(pathname: string): boolean { + return pathname === '/play' || pathname.startsWith('/play/') +} + export function isRootMediaPath(pathname: string): boolean { return ROOT_MEDIA_PATHS.has(pathname) } @@ -84,6 +89,9 @@ export function resolveHeaderBack(pathname: string): HeaderBackTarget | null { if (pathname.startsWith('/library/')) { return { to: '/libraries', label: '媒体库' } } + if (pathname === '/playlists' || pathname === '/favourites' || pathname === '/history') { + return { to: '/', label: '首页' } + } if (pathname.startsWith('/playlist/')) { return { to: '/playlists', label: '播放列表' } } @@ -118,7 +126,7 @@ export function resolveHeaderBack(pathname: string): HeaderBackTarget | null { } export function shouldShowMobileBottomNav(pathname: string): boolean { - if (pathname.startsWith('/play')) return false + if (isPlayerRoute(pathname)) return false return ( pathname === '/' || pathname === '/libraries' || diff --git a/web/src/pages/PlaylistsPage.tsx b/web/src/pages/PlaylistsPage.tsx index 9e4c6a0..e7e7615 100644 --- a/web/src/pages/PlaylistsPage.tsx +++ b/web/src/pages/PlaylistsPage.tsx @@ -1,18 +1,18 @@ import { FormEvent, useEffect, useState } from 'react' import { Link } from 'react-router-dom' -import { ListPlus, Trash2 } from 'lucide-react' +import { ChevronRight, ListMusic, ListPlus, Trash2 } from 'lucide-react' import toast from 'react-hot-toast' import { playbackAPI } from '../api/playback' import { confirmAction } from '../components/confirmAction' +import { PageHeader } from '../components/PageHeader' import type { Playlist } from '../types' -// Landing page for playlists. Lists every playlist owned by the current -// user and lets them create / delete one. export function PlaylistsPage() { const [items, setItems] = useState([]) const [name, setName] = useState('') const [loading, setLoading] = useState(true) + const [creating, setCreating] = useState(false) const refresh = () => playbackAPI @@ -26,67 +26,124 @@ export function PlaylistsPage() { const onCreate = async (e: FormEvent) => { e.preventDefault() + const trimmed = name.trim() + if (!trimmed) return + + setCreating(true) try { - await playbackAPI.createPlaylist(name) + await playbackAPI.createPlaylist(trimmed) toast.success('已创建') setName('') await refresh() } catch { toast.error('创建失败') + } finally { + setCreating(false) } } + const isEmpty = !loading && items.length === 0 + return (
-

播放列表

+ -
+ setName(e.target.value)} + disabled={creating} /> -
- {loading &&

加载中…

} - {!loading && items.length === 0 &&

还没有任何播放列表。

} + {loading && ( +
+ + 加载中… +
+ )} -
- {items.map((p) => ( -
- - {p.name} - {p.is_public && ( - - public - - )} - - + {isEmpty && ( +
+
+
- ))} -
+
+

还没有任何播放列表

+

+ 在上方输入名称创建列表,或在媒体详情页点击「加入播放列表」 +

+
+
+ )} + + {items.length > 0 && ( +
+ {items.map((p) => ( +
+ +
+ +
+
+

+ {p.name} +

+ {p.is_public && ( + + 公开 + + )} +
+ + + +
+ ))} +
+ )}
) }