feat(danmaku): 支持快捷填入当前片名并优化重新搜索机制

This commit is contained in:
truewhile
2026-09-06 20:32:12 +08:00
parent 465e98a792
commit 394d1a3cda
4 changed files with 38 additions and 5 deletions
+5 -2
View File
@@ -28,6 +28,8 @@ type DanmakuStageProps = {
search?: string | null search?: string | null
/** Explicit danmaku library chosen by the user; null = auto-resolve. */ /** Explicit danmaku library chosen by the user; null = auto-resolve. */
episodeId?: number | string | null episodeId?: number | string | null
/** Counter or token changed to trigger refetch even when search stays identical. */
searchTrigger?: number
/** Called after each fetch attempt (success or error) finishes with metadata. */ /** Called after each fetch attempt (success or error) finishes with metadata. */
onLoaded?: (info: DanmakuLoadedInfo | null) => void onLoaded?: (info: DanmakuLoadedInfo | null) => void
/** Called when multiple anime matched and the user must pick one. */ /** Called when multiple anime matched and the user must pick one. */
@@ -50,6 +52,7 @@ export function DanmakuStage({
area = 1, area = 1,
search = null, search = null,
episodeId = null, episodeId = null,
searchTrigger = 0,
onLoaded, onLoaded,
onCandidates, onCandidates,
}: DanmakuStageProps) { }: DanmakuStageProps) {
@@ -252,8 +255,8 @@ export function DanmakuStage({
manager.unmount() manager.unmount()
managerRef.current = null managerRef.current = null
} }
// search / episodeId 变化时重新拉取弹幕(含媒体/开关切换)。 // search / episodeId / searchTrigger 变化时重新拉取弹幕(含媒体/开关切换)。
}, [media, videoRef, enabled, search, episodeId, onLoaded, onCandidates]) }, [media, videoRef, enabled, search, episodeId, searchTrigger, onLoaded, onCandidates])
// Live renderer knobs: opacity / area / font size without recreating the // Live renderer knobs: opacity / area / font size without recreating the
// engine. font size additionally rescales currently visible comments. // engine. font size additionally rescales currently visible comments.
+18 -3
View File
@@ -25,6 +25,8 @@ type PlayerDanmakuPanelProps = {
candidates: DanmakuAnime[] candidates: DanmakuAnime[]
/** Human-readable label of the currently selected library. */ /** Human-readable label of the currently selected library. */
selectedSource?: string selectedSource?: string
/** Title used by auto-matching (e.g. anime title, media title or filename). */
autoMatchTitle?: string
/** Loaded danmaku metadata (title, episode, count, match mode). */ /** Loaded danmaku metadata (title, episode, count, match mode). */
danmakuInfo?: DanmakuLoadedInfo | null danmakuInfo?: DanmakuLoadedInfo | null
onSelectEpisode: (episodeId: number, animeTitle: string, episodeTitle: string) => void onSelectEpisode: (episodeId: number, animeTitle: string, episodeTitle: string) => void
@@ -47,6 +49,7 @@ export function PlayerDanmakuPanel({
onFontSizeChange, onFontSizeChange,
candidates, candidates,
selectedSource, selectedSource,
autoMatchTitle,
danmakuInfo, danmakuInfo,
onSelectEpisode, onSelectEpisode,
onResetAuto, onResetAuto,
@@ -203,7 +206,19 @@ export function PlayerDanmakuPanel({
{/* 搜索弹幕 */} {/* 搜索弹幕 */}
<div className="mb-4"> <div className="mb-4">
<div className="mb-1 text-xs text-white/60">搜索弹幕(留空 = 按视频名自动匹配)</div> <div className="mb-1 flex items-center justify-between text-xs text-white/60">
<span>搜索弹幕(留空 = 按视频名自动匹配)</span>
{autoMatchTitle && (
<button
type="button"
onClick={() => setDraft(autoMatchTitle)}
className="text-[11px] text-rose-300 transition hover:text-rose-200"
title="填入当前识别到的视频名"
>
填入当前名
</button>
)}
</div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<input <input
value={draft} value={draft}
@@ -211,8 +226,8 @@ export function PlayerDanmakuPanel({
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') onSearch(draft.trim()) if (e.key === 'Enter') onSearch(draft.trim())
}} }}
placeholder="输入番剧或电影名…" placeholder={autoMatchTitle ? `自动匹配:${autoMatchTitle}` : '输入番剧或电影名…'}
className="min-w-0 flex-1 rounded-lg border border-white/15 bg-white/5 px-2.5 py-1.5 text-xs outline-none placeholder:text-white/35 focus:border-rose-400/60" className="min-w-0 flex-1 rounded-lg border border-white/15 bg-white/5 px-2.5 py-1.5 text-xs outline-none placeholder:text-white/40 focus:border-rose-400/60"
/> />
<button <button
onClick={() => onSearch(draft.trim())} onClick={() => onSearch(draft.trim())}
+12
View File
@@ -74,6 +74,7 @@ export function PlayerPage() {
const [danmakuOpen, setDanmakuOpen] = useState(false) const [danmakuOpen, setDanmakuOpen] = useState(false)
const [danmakuEnabled, setDanmakuEnabled] = useState(true) const [danmakuEnabled, setDanmakuEnabled] = useState(true)
const [danmakuSearch, setDanmakuSearch] = useState<string | null>(null) const [danmakuSearch, setDanmakuSearch] = useState<string | null>(null)
const [danmakuSearchTrigger, setDanmakuSearchTrigger] = useState(0)
const [danmakuSearching, setDanmakuSearching] = useState(false) const [danmakuSearching, setDanmakuSearching] = useState(false)
// 用户从候选列表选定的弹幕库;null = 自动匹配。 // 用户从候选列表选定的弹幕库;null = 自动匹配。
const [danmakuEpisodeId, setDanmakuEpisodeId] = useState<number | string | null>(null) const [danmakuEpisodeId, setDanmakuEpisodeId] = useState<number | string | null>(null)
@@ -141,6 +142,7 @@ export function PlayerPage() {
setDanmakuEpisodeId(null) setDanmakuEpisodeId(null)
setDanmakuInfo(null) setDanmakuInfo(null)
setDanmakuSearch(kw || null) setDanmakuSearch(kw || null)
setDanmakuSearchTrigger((prev) => prev + 1)
}, []) }, [])
const danmakuLoaded = useCallback((info: DanmakuLoadedInfo | null) => { const danmakuLoaded = useCallback((info: DanmakuLoadedInfo | null) => {
@@ -164,6 +166,7 @@ export function PlayerPage() {
setDanmakuSearching(true) setDanmakuSearching(true)
// 展示当前所选来源(面板标题处可见)。 // 展示当前所选来源(面板标题处可见)。
setDanmakuSelectedSource(episodeTitle ? `${animeTitle}・${episodeTitle}` : animeTitle) setDanmakuSelectedSource(episodeTitle ? `${animeTitle}・${episodeTitle}` : animeTitle)
setDanmakuSearchTrigger((prev) => prev + 1)
}, []) }, [])
// 回到自动匹配(清除用户手动选择)。 // 回到自动匹配(清除用户手动选择)。
@@ -174,6 +177,7 @@ export function PlayerPage() {
setDanmakuSearch(null) setDanmakuSearch(null)
setDanmakuSelectedSource('') setDanmakuSelectedSource('')
setDanmakuInfo(null) setDanmakuInfo(null)
setDanmakuSearchTrigger((prev) => prev + 1)
}, []) }, [])
// 切换视频时重置媒体与弹幕状态,确保新视频自动重新识别并加载弹幕 // 切换视频时重置媒体与弹幕状态,确保新视频自动重新识别并加载弹幕
@@ -549,6 +553,12 @@ export function PlayerPage() {
toast.error('视频播放失败,请检查文件是否存在') toast.error('视频播放失败,请检查文件是否存在')
}, [directOnly, hlsUnavailable, media, mode, params, setParams]) }, [directOnly, hlsUnavailable, media, mode, params, setParams])
const danmakuAutoTitle =
danmakuInfo?.animeTitle ||
media?.original_name?.trim() ||
media?.title?.trim() ||
''
return ( return (
<div className="relative flex h-full w-full flex-1 flex-col overflow-hidden bg-black"> <div className="relative flex h-full w-full flex-1 flex-col overflow-hidden bg-black">
<PlayerTopBar <PlayerTopBar
@@ -580,6 +590,7 @@ export function PlayerPage() {
danmakuArea={danmakuArea} danmakuArea={danmakuArea}
danmakuSearch={danmakuSearch} danmakuSearch={danmakuSearch}
danmakuEpisodeId={danmakuEpisodeId} danmakuEpisodeId={danmakuEpisodeId}
danmakuSearchTrigger={danmakuSearchTrigger}
danmakuOpen={danmakuOpen} danmakuOpen={danmakuOpen}
onToggleDanmaku={toggleDanmakuOpen} onToggleDanmaku={toggleDanmakuOpen}
onDanmakuLoaded={danmakuLoaded} onDanmakuLoaded={danmakuLoaded}
@@ -619,6 +630,7 @@ export function PlayerPage() {
onFontSizeChange={setDanmakuFontSize} onFontSizeChange={setDanmakuFontSize}
candidates={danmakuCandidates} candidates={danmakuCandidates}
selectedSource={danmakuSelectedSource} selectedSource={danmakuSelectedSource}
autoMatchTitle={danmakuAutoTitle}
danmakuInfo={danmakuInfo} danmakuInfo={danmakuInfo}
onSelectEpisode={danmakuSelectEpisode} onSelectEpisode={danmakuSelectEpisode}
onResetAuto={danmakuResetAuto} onResetAuto={danmakuResetAuto}
+3
View File
@@ -24,6 +24,7 @@ type PlayerVideoStageProps = {
danmakuArea: number danmakuArea: number
danmakuSearch: string | null danmakuSearch: string | null
danmakuEpisodeId: number | string | null danmakuEpisodeId: number | string | null
danmakuSearchTrigger?: number
danmakuOpen: boolean danmakuOpen: boolean
onToggleDanmaku: () => void onToggleDanmaku: () => void
onDanmakuLoaded: (info: DanmakuLoadedInfo | null) => void onDanmakuLoaded: (info: DanmakuLoadedInfo | null) => void
@@ -58,6 +59,7 @@ export function PlayerVideoStage({
danmakuArea, danmakuArea,
danmakuSearch, danmakuSearch,
danmakuEpisodeId, danmakuEpisodeId,
danmakuSearchTrigger = 0,
danmakuOpen, danmakuOpen,
onToggleDanmaku, onToggleDanmaku,
onDanmakuLoaded, onDanmakuLoaded,
@@ -280,6 +282,7 @@ export function PlayerVideoStage({
area={danmakuArea} area={danmakuArea}
search={danmakuSearch} search={danmakuSearch}
episodeId={danmakuEpisodeId} episodeId={danmakuEpisodeId}
searchTrigger={danmakuSearchTrigger}
onLoaded={onDanmakuLoaded} onLoaded={onDanmakuLoaded}
onCandidates={onDanmakuCandidates} onCandidates={onDanmakuCandidates}
/> />