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');