diff --git a/web/src/features/media/components/playback-controls.tsx b/web/src/features/media/components/playback-controls.tsx index 78fd75c..c46277e 100644 --- a/web/src/features/media/components/playback-controls.tsx +++ b/web/src/features/media/components/playback-controls.tsx @@ -1,6 +1,6 @@ import {MediaSelect} from './select.tsx'; import {SleepTimerOptions} from './sleep-timer.tsx'; -import { ChevronLeft, Layers, Play, Pause, SkipBack, SkipForward, RotateCcw, RotateCw, Square, Timer, Gauge, Ellipsis, BookOpen, ListMusic, Search, ArrowUp, ArrowDown, X, Volume2, Pencil } from 'lucide-react'; +import { ChevronLeft, Layers, Play, Pause, SkipBack, SkipForward, RotateCcw, RotateCw, Square, Timer, Gauge, Ellipsis, BookOpen, MicVocal, ListMusic, Search, ArrowUp, ArrowDown, X, Volume2, Pencil } from 'lucide-react'; import type { MediaPlayer } from '../services/player.ts'; import { useEffect, useState, useRef, useLayoutEffect } from 'react'; import type {Detail,MediaApi,Part} from '../api/media-api.ts'; @@ -15,7 +15,7 @@ import {MediaLoading} from './loading.tsx'; import {MediaScreenError} from './screen-error.tsx'; import {PlaybackProblem} from './playback-problem.tsx'; -function PlayingArtwork({api,item,failed,onRetry,player,lyrics,onMore}:{api:MediaApi;item:Detail|null;failed:boolean;onRetry:()=>void;player:MediaPlayer;lyrics:boolean;onMore:()=>void}){ +function PlayingArtwork({api,item,failed,onRetry,player,lyrics}:{api:MediaApi;item:Detail|null;failed:boolean;onRetry:()=>void;player:MediaPlayer;lyrics:boolean}){ const field=(key:string)=>item?.overrides[key]??item?.metadata[key]; const edition=item?.editions?.find(entry=>entry.parts.some(part=>part.id===player.currentPartId)),chapter=edition?.parts.find(part=>part.id===player.currentPartId); const book=item?.kind==='audiobook'; @@ -24,7 +24,7 @@ function PlayingArtwork({api,item,failed,onRetry,player,lyrics,onMore}:{api:Medi return <>{lyrics?<>

{title}

{credit&&

{credit}

}
:
{!item&&!failed&&}{item?:
{failed?'作品资料暂不可用':null}
} {book&&chapter&&

第 {edition!.parts.indexOf(chapter)+1} / {edition!.parts.length} 章 · {item.title}

} -

{title}

{credit&&

{credit}

}{failed&&

作品资料读取失败。

}
+

{title}

{credit&&

{credit}

}{failed&&

作品资料读取失败。

}
}; } @@ -92,7 +92,7 @@ export function PlaybackControls({player,api,panel,onPanelChange,onBack,onFavori {player.error&&open('versions')}:{})} {...(onBack?{onBack}:{})} backLabel={view==='main'?'返回浏览':'返回播放'}/>} {!player.error&&player.loadingStatus&&} {player.canOpenNativeVideo&&} - {view==='chapters'?setRetry(value=>value+1)}/>:view==='queue'?:!player.isVideo&&itemId&&setRetry(value=>value+1)} player={player} lyrics={lyrics} onMore={()=>open('more')}/>} + {view==='chapters'?setRetry(value=>value+1)}/>:view==='queue'?:!player.isVideo&&itemId&&setRetry(value=>value+1)} player={player} lyrics={lyrics}/>} {view!=='queue'&&view!=='chapters'&&
{!player.isVideo&&}
@@ -106,7 +106,7 @@ export function PlaybackControls({player,api,panel,onPanelChange,onBack,onFavori {channel==='music'?:speed} - {channel==='audiobook'?:channel==='music'?:null} + {channel==='audiobook'?:channel==='music'?:null} {!onBack&&}
} diff --git a/web/src/features/media/components/video-controls.tsx b/web/src/features/media/components/video-controls.tsx index 97bf50d..29affc2 100644 --- a/web/src/features/media/components/video-controls.tsx +++ b/web/src/features/media/components/video-controls.tsx @@ -1,6 +1,6 @@ import {VideoEpisodeRail} from './video-episode-rail.tsx'; import {SleepTimerOptions} from './sleep-timer.tsx'; -import {Ellipsis,X,Square,Check,Subtitles,AudioLines,Layers} from 'lucide-react'; +import {Ellipsis,X,Square,Check,Subtitles,AudioLines,Layers,ListVideo} from 'lucide-react'; import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'; import type {MediaPlayer} from '../services/player.ts'; import type {Detail,MediaApi} from '../api/media-api.ts'; @@ -44,7 +44,7 @@ export function VideoControls({player,api,onBack}:{player:MediaPlayer;api:MediaA async function run(action:()=>Promise,close=false){if(busy)return;setBusy(true);setNotice('');try{await action();if(close&&!player.error)setTool(null);}catch(error){setNotice(error instanceof Error?error.message:'操作失败,请重试。');}finally{setBusy(false);}} return
{item?.title||player.title}{edition&&{edition.label}}
-
{entries.length>1&&}
+
{entries.length>1&&}
{player.error&&open('versions')}:{})} {...(onBack?{onBack}:{})}/>} {notice&&

{notice}

} {plot&&
剧情简介

{plot}

} diff --git a/web/test/media-audio-tools.test.tsx b/web/test/media-audio-tools.test.tsx index e74652e..57a47fa 100644 --- a/web/test/media-audio-tools.test.tsx +++ b/web/test/media-audio-tools.test.tsx @@ -51,3 +51,16 @@ it('reads uncertain favorite state before allowing another write',async()=>{ await vi.waitFor(()=>expect(root.querySelector('[aria-label="取消收藏"]')).not.toBeNull());expect(writes).toBe(1); const signal=request.mock.calls.at(-1)?.[3] as unknown as AbortSignal;act(()=>render(null,root));expect(signal.aborted).toBe(true); }); +it('offers one more-options entry point and keeps the book icon for chapters only',async()=>{ + const detail=vi.fn().mockResolvedValue(work('song','track')); + const api={detail,request:vi.fn().mockResolvedValue({favorite:false})} as unknown as MediaApi,player={active:true,currentItemId:'song',title:'歌',currentPlaylist:[]} as unknown as MediaPlayer; + for(const withBack of [true,false]){ + await act(async()=>render({}}:{})}/>,root)); + await vi.waitFor(()=>expect(root.querySelector('[data-channel]')?.getAttribute('data-channel')).toBe('music')); + // The title-row ellipsis used to open the same sheet as the header/options ellipsis. + expect(root.querySelectorAll('[aria-label="更多播放选项"]').length).toBe(1); + expect(root.querySelector('[aria-label="当前内容操作"]')).toBeNull(); + expect(root.querySelector('[aria-label="歌词"] svg')?.classList.contains('lucide-mic-vocal')).toBe(true); + await act(async()=>render(null,root)); + } +}); diff --git a/web/test/media-video-states.test.tsx b/web/test/media-video-states.test.tsx index d6759c3..34a2b57 100644 --- a/web/test/media-video-states.test.tsx +++ b/web/test/media-video-states.test.tsx @@ -38,3 +38,9 @@ it('opens recovery choices without restarting an expired session automatically', expect(root.textContent).toContain('播放会话已失效');expect(p.play).not.toHaveBeenCalled(); act(()=>root.querySelector('button')!.click());expect(versions).toHaveBeenCalledOnce();expect(p.play).not.toHaveBeenCalled(); }); +it('uses different icons for the episode list and the version switcher',async()=>{ + const p=player();Object.defineProperty(p,'currentPlaylist',{value:[{index:0,title:'第 1 集',current:true},{index:1,title:'第 2 集',current:false}]}); + await act(async()=>render(,root)); + const icon=(label:string)=>[...root.querySelectorAll('.media-video-toolbar button')].find(button=>button.textContent===label)?.querySelector('svg')?.getAttribute('class')??''; + expect(icon('选集')).toContain('lucide-list-video');expect(icon('版本')).toContain('lucide-layers'); +}); diff --git a/web/tools/media-v2-review.cjs b/web/tools/media-v2-review.cjs index f956945..0d57b6b 100644 --- a/web/tools/media-v2-review.cjs +++ b/web/tools/media-v2-review.cjs @@ -33,7 +33,7 @@ async function saveShot(capture){for(let attempt=0;;attempt++){try{return await const layout=await page.evaluate(()=>{ const rect=s=>{const e=document.querySelector(s);if(!e||!e.getClientRects().length)return null;const r=e.getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height};}; const tab=document.querySelector('.media-tabs'),title=document.querySelector('.media-page-heading h1,.media-page-heading h2,.media-audio-heading>strong'); - return {library:rect('.media-library-filter'),count:rect('.media-browse-tools>span,.media-person-toolbar>span'),tabsScrollbar:tab?getComputedStyle(tab).scrollbarWidth:null,titleSize:title?getComputedStyle(title).fontSize:null,synopsis:rect('.media-detail-description'),chapters:rect('.media-edition'),episodes:rect('.media-episode-grid'),episodeColumns:document.querySelector('.media-episode-grid')?getComputedStyle(document.querySelector('.media-episode-grid')).gridTemplateColumns.split(' ').length:null,episodeLabels:[...document.querySelectorAll('.media-episode-grid>button strong')].map(e=>e.textContent),queue:rect('.media-current-playlist'),contentActions:rect('[aria-label="当前内容操作"]'),stateIcon:rect('.media-state-icon,.media-empty-emblem'),stateTitle:document.querySelector('.media-state-page h2,.media-screen-error.is-page>strong,.media-library-empty h2')?.textContent}; + return {library:rect('.media-library-filter'),count:rect('.media-browse-tools>span,.media-person-toolbar>span'),tabsScrollbar:tab?getComputedStyle(tab).scrollbarWidth:null,titleSize:title?getComputedStyle(title).fontSize:null,synopsis:rect('.media-detail-description'),chapters:rect('.media-edition'),episodes:rect('.media-episode-grid'),episodeColumns:document.querySelector('.media-episode-grid')?getComputedStyle(document.querySelector('.media-episode-grid')).gridTemplateColumns.split(' ').length:null,episodeLabels:[...document.querySelectorAll('.media-episode-grid>button strong')].map(e=>e.textContent),queue:rect('.media-current-playlist'),contentActions:rect('[aria-label="更多播放选项"]'),stateIcon:rect('.media-state-icon,.media-empty-emblem'),stateTitle:document.querySelector('.media-state-page h2,.media-screen-error.is-page>strong,.media-library-empty h2')?.textContent}; }); if(layout.library&&layout.count)assert.ok(layout.library.x+layout.library.width<=layout.count.x+1,key+' library filter must be left of count'); if(layout.tabsScrollbar)assert.equal(layout.tabsScrollbar,'none',key+' tabs must hide scrollbars');