import { useEffect, useState } from 'react' import { Check, ChevronRight, Film, Hash, Loader2, MessageSquareText, RefreshCw, Search, Sparkles, Tag, X } from 'lucide-react' import type { DanmakuAnime, DanmakuEpisode, DanmakuLoadedInfo } from '../api/danmaku' // PlayerDanmakuPanel — the on-player danmaku control panel. It displays // the matched danmaku details (anime title, episode title, comment count, // match mode), toggles loading, lets the user re-search by a custom keyword, // and adjusts the renderer knobs (display area / opacity / font size) live. type PlayerDanmakuPanelProps = { open: boolean onClose: () => void enabled: boolean onToggleEnabled: (v: boolean) => void search: string onSearch: (kw: string) => void searching: boolean area: number onAreaChange: (v: number) => void opacity: number onOpacityChange: (v: number) => void fontSize: number onFontSizeChange: (v: number) => void /** Multiple anime matched — user must pick one. */ candidates: DanmakuAnime[] /** Human-readable label of the currently selected library. */ selectedSource?: string /** Loaded danmaku metadata (title, episode, count, match mode). */ danmakuInfo?: DanmakuLoadedInfo | null onSelectEpisode: (episodeId: number, animeTitle: string, episodeTitle: string) => void onResetAuto: () => void } export function PlayerDanmakuPanel({ open, onClose, enabled, onToggleEnabled, search, onSearch, searching, area, onAreaChange, opacity, onOpacityChange, fontSize, onFontSizeChange, candidates, selectedSource, danmakuInfo, onSelectEpisode, onResetAuto, }: PlayerDanmakuPanelProps) { const [draft, setDraft] = useState(search) // 展开的番剧(动画 → 集数两级树),默认全展开便于选择。 const [openAnime, setOpenAnime] = useState>(new Set()) // 面板打开时同步外部搜索词到输入框草稿,并展开全部候选。 useEffect(() => { if (open) { setDraft(search) setOpenAnime(new Set(candidates.map((c) => c.animeId))) } }, [open, search, candidates]) if (!open) return null const toggleAnime = (animeId: number) => { setOpenAnime((prev) => { const next = new Set(prev) if (next.has(animeId)) { next.delete(animeId) } else { next.add(animeId) } return next }) } // 匹配模式标签显示辅助 const renderMatchBadge = (mode?: string) => { switch (mode) { case 'hash': return ( 哈希精准匹配 ) case 'filename': return ( 文件名匹配 ) case 'search': return ( 标题搜索匹配 ) case 'manual': return ( 手动指定 ) default: return null } } const isCustomOrManual = Boolean(search || selectedSource || danmakuInfo?.matchMode === 'manual') return ( // 面板悬浮于视频上方:阻止点击冒泡,避免触发视频区域的播放/暂停切换。
e.stopPropagation()} className="absolute right-4 top-16 z-30 w-80 rounded-2xl border border-white/15 bg-black/85 p-4 text-white shadow-2xl backdrop-blur-md" >
弹幕设置
{/* 是否加载弹幕 */} {/* 当前加载的弹幕信息卡片 */} {enabled && (
{searching ? (
正在匹配弹幕…
) : danmakuInfo && (danmakuInfo.totalCount > 0 || danmakuInfo.animeTitle) ? (
{danmakuInfo.animeTitle || selectedSource || '未知番剧'}
{danmakuInfo.episodeTitle && (
{danmakuInfo.episodeTitle}
)}
{isCustomOrManual && ( )}
{renderMatchBadge(danmakuInfo.matchMode)}
{danmakuInfo.totalCount > 0 ? `共 ${danmakuInfo.totalCount.toLocaleString()} 条弹幕` : '暂无弹幕内容'}
) : candidates.length === 0 ? (
未匹配到弹幕,可在下方手动搜索 {isCustomOrManual && ( )}
) : null}
)} {/* 搜索弹幕 */}
搜索弹幕(留空 = 按视频名自动匹配)
setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') onSearch(draft.trim()) }} placeholder="输入番剧或电影名…" 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" />
{/* 多番剧命中候选列表 */} {candidates.length > 0 && (
搜到多部番剧,请选择对应集数:
{candidates.map((anime, i) => (
{openAnime.has(anime.animeId) && (
{anime.episodes.map((ep) => ( onSelectEpisode(ep.episodeId, anime.animeTitle, ep.episodeTitle) } /> ))}
)}
))}
)} {/* 屏幕占比(显示区域) */} `${Math.round(v * 100)}%`} onChange={onAreaChange} /> {/* 透明度 */} `${Math.round(v * 100)}%`} onChange={onOpacityChange} /> {/* 字体大小 */} `${Math.round(v)}px`} onChange={onFontSizeChange} />
) } function SliderRow({ label, value, min, max, step, format, onChange, }: { label: string value: number min: number max: number step: number format: (v: number) => string onChange: (v: number) => void }) { return (
{label} {format(value)}
onChange(Number(e.target.value))} className="w-full accent-rose-500" />
) } function EpisodeRow({ episode, onSelect, }: { episode: DanmakuEpisode onSelect: () => void }) { return ( ) }