记住用户选择的弹幕,调整弹幕选项卡可滑动区域

This commit is contained in:
mtvpls
2026-01-02 16:24:05 +08:00
parent 435d3a0f36
commit 06cf30640b
3 changed files with 270 additions and 26 deletions

View File

@@ -36,6 +36,12 @@ import {
initDanmakuModule,
getDanmakuFromCache,
} from '@/lib/danmaku/api';
import {
getDanmakuSourceIndex,
saveDanmakuSourceIndex,
getManualDanmakuSelection,
saveManualDanmakuSelection,
} from '@/lib/danmaku/selection-memory';
import type { DanmakuAnime, DanmakuSelection, DanmakuSettings } from '@/lib/danmaku/types';
import { SearchResult, DanmakuFilterConfig, EpisodeFilterConfig } from '@/lib/types';
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
@@ -563,9 +569,16 @@ function PlayPageClient() {
// 先尝试从 IndexedDB 缓存加载
try {
const cachedComments = await getDanmakuFromCache(title, episodeIndex);
if (cachedComments && cachedComments.length > 0 && danmakuPluginRef.current) {
if (cachedComments && cachedComments.length > 0) {
console.log(`[弹幕] 使用缓存: title="${title}", episodeIndex=${episodeIndex}, 数量=${cachedComments.length}`);
// 如果弹幕插件还未初始化,等待初始化
if (!danmakuPluginRef.current) {
console.log('[弹幕] 弹幕插件未初始化,等待初始化...');
// 缓存命中但插件未初始化,不执行搜索,等待下次触发
return;
}
setDanmakuLoading(true);
// 转换弹幕格式
@@ -631,8 +644,26 @@ function PlayPageClient() {
console.error('[弹幕] 读取缓存失败:', error);
}
// 没有缓存,执行自动搜索弹幕
console.log(`[弹幕] 第 ${episodeIndex + 1} 集缓存未命中,开始搜索`);
// 没有缓存,先检查是否有手动选择的剧集 ID
console.log(`[弹幕] 第 ${episodeIndex + 1} 集缓存未命中`);
// 检查是否有手动选择的剧集 ID
const manualEpisodeId = getManualDanmakuSelection(title, episodeIndex);
if (manualEpisodeId) {
console.log(`[弹幕记忆] 使用手动选择的剧集 ID: ${manualEpisodeId}`);
setDanmakuLoading(true);
try {
await loadDanmaku(manualEpisodeId);
console.log('[弹幕记忆] 使用手动选择的弹幕成功');
return; // 使用手动选择成功,直接返回
} catch (error) {
console.error('[弹幕记忆] 使用手动选择的弹幕失败:', error);
// 继续执行自动搜索
}
}
// 执行自动搜索弹幕
console.log(`[弹幕] 开始自动搜索`);
setDanmakuLoading(true);
try {
@@ -647,9 +678,54 @@ function PlayPageClient() {
videoYear
);
// 如果有多个匹配结果,让用户选择
// 如果有多个匹配结果,先检查是否有记忆的选择
if (filteredAnimes.length > 1) {
console.log(`找到 ${filteredAnimes.length} 个弹幕源,等待用户选择`);
console.log(`找到 ${filteredAnimes.length} 个弹幕源`);
// 检查是否有上次选择的下标
const rememberedIndex = getDanmakuSourceIndex(title);
if (rememberedIndex !== null && rememberedIndex < filteredAnimes.length) {
console.log(`[弹幕记忆] 使用上次选择的弹幕源,下标: ${rememberedIndex}`);
const anime = filteredAnimes[rememberedIndex];
// 获取剧集列表
const episodesResult = await getEpisodes(anime.animeId);
if (
episodesResult.success &&
episodesResult.bangumi.episodes.length > 0
) {
// 根据当前集数选择对应的弹幕
const currentEp = currentEpisodeIndexRef.current;
const videoEpTitle = detailRef.current?.episodes_titles?.[currentEp];
const episode = matchDanmakuEpisode(currentEp, episodesResult.bangumi.episodes, videoEpTitle);
if (episode) {
const selection: DanmakuSelection = {
animeId: anime.animeId,
episodeId: episode.episodeId,
animeTitle: anime.animeTitle,
episodeTitle: episode.episodeTitle,
};
// 设置选择记录
setCurrentDanmakuSelection(selection);
// 加载弹幕
await loadDanmaku(episode.episodeId);
// 设置剧集列表
setDanmakuEpisodesList(episodesResult.bangumi.episodes);
console.log('使用记忆的弹幕源成功:', selection);
setDanmakuLoading(false);
return;
}
}
}
// 没有记忆或记忆失效,让用户选择
console.log(`等待用户选择弹幕源`);
setDanmakuMatches(filteredAnimes);
setCurrentSearchKeyword(title);
setShowDanmakuSourceSelector(true);
@@ -2842,6 +2918,13 @@ function PlayPageClient() {
const handleDanmakuSelect = async (selection: DanmakuSelection) => {
setCurrentDanmakuSelection(selection);
// 保存手动选择的剧集 ID 到 sessionStorage
const title = videoTitleRef.current;
const episodeIndex = currentEpisodeIndexRef.current;
if (title && episodeIndex >= 0) {
saveManualDanmakuSelection(title, episodeIndex, selection.episodeId);
}
// 获取该动漫的所有剧集列表
try {
const episodesResult = await getEpisodes(selection.animeId);
@@ -2857,7 +2940,7 @@ function PlayPageClient() {
};
// 处理用户选择弹幕源
const handleDanmakuSourceSelect = async (selectedAnime: DanmakuAnime) => {
const handleDanmakuSourceSelect = async (selectedAnime: DanmakuAnime, selectedIndex?: number) => {
setShowDanmakuSourceSelector(false);
setDanmakuLoading(true);
@@ -2865,6 +2948,11 @@ function PlayPageClient() {
const title = videoTitleRef.current;
console.log('[弹幕] 用户选择弹幕源 - 视频:', title, '弹幕源:', selectedAnime.animeTitle);
// 如果提供了下标,保存到 sessionStorage
if (selectedIndex !== undefined && title) {
saveDanmakuSourceIndex(title, selectedIndex);
}
// 获取剧集列表
const episodesResult = await getEpisodes(selectedAnime.animeId);
@@ -5485,7 +5573,7 @@ function PlayPageClient() {
{danmakuMatches.map((anime, index) => (
<button
key={anime.animeId}
onClick={() => handleDanmakuSourceSelect(anime)}
onClick={() => handleDanmakuSourceSelect(anime, index)}
className='w-full flex flex-col p-5 bg-gray-50 dark:bg-gray-700/50
hover:bg-gray-100 dark:hover:bg-gray-700 rounded-xl transition-all
duration-200 text-left group border-2 border-transparent

View File

@@ -51,7 +51,7 @@ export default function DanmakuPanel({
} else {
setSearchResults([]);
setSearchError(
response.errorMessage || '未找到匹配的动漫,请尝试其他关键词'
response.errorMessage || '未找到匹配的剧集,请尝试其他关键词'
);
}
} catch (error) {
@@ -75,7 +75,7 @@ export default function DanmakuPanel({
setEpisodes(response.bangumi.episodes);
} else {
setEpisodes([]);
setSearchError('该动漫暂无剧集信息');
setSearchError('该剧集暂无弹幕信息');
}
} catch (error) {
console.error('获取剧集失败:', error);
@@ -128,7 +128,7 @@ export default function DanmakuPanel({
return (
<div className='flex h-full flex-col overflow-hidden'>
{/* 搜索区域 */}
{/* 搜索区域 - 固定在顶部 */}
<div className='mb-4 flex-shrink-0'>
<div className='flex gap-2'>
<input
@@ -140,7 +140,7 @@ export default function DanmakuPanel({
handleSearch(searchKeyword);
}
}}
placeholder='输入动漫名称搜索弹幕...'
placeholder='输入剧集名称搜索弹幕...'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
@@ -174,10 +174,23 @@ export default function DanmakuPanel({
</button>
</div>
{/* 错误提示 */}
{searchError && (
<div
className='mt-3 rounded-lg border border-red-500/30 bg-red-500/10
px-3 py-2 text-sm text-red-600 dark:text-red-400'
>
{searchError}
</div>
)}
</div>
{/* 可滚动内容区域 */}
<div className='flex-1 overflow-y-auto min-h-0'>
{/* 当前选择的弹幕信息 */}
{currentSelection && (
<div
className='mt-3 rounded-lg border border-green-500/30 bg-green-500/10
className='mb-4 rounded-lg border border-green-500/30 bg-green-500/10
px-3 py-2 text-sm'
>
<p className='font-semibold text-green-600 dark:text-green-400'>
@@ -192,19 +205,8 @@ export default function DanmakuPanel({
</div>
)}
{/* 错误提示 */}
{searchError && (
<div
className='mt-3 rounded-lg border border-red-500/30 bg-red-500/10
px-3 py-2 text-sm text-red-600 dark:text-red-400'
>
{searchError}
</div>
)}
</div>
{/* 内容区域 */}
<div className='flex-1 overflow-y-auto min-h-0'>
{/* 内容区域 */}
<div>
{/* 显示剧集列表 */}
{selectedAnime && (
<div className='space-y-2'>
@@ -361,10 +363,11 @@ export default function DanmakuPanel({
<div className='flex flex-col items-center justify-center py-12 text-center'>
<MagnifyingGlassIcon className='mb-3 h-12 w-12 text-gray-400' />
<p className='text-sm text-gray-500 dark:text-gray-400'>
</p>
</div>
)}
</div>
</div>
</div>
);

View File

@@ -0,0 +1,153 @@
/**
* 弹幕选择记忆管理
* 用于记住用户在多个弹幕源中的选择,避免换集时重复弹出选择对话框
*/
const STORAGE_KEY_PREFIX = 'danmaku_selection_';
/**
* 保存自动搜索时用户选择的弹幕源下标
* @param title 视频标题
* @param selectedIndex 用户选择的弹幕源在搜索结果中的下标
*/
export function saveDanmakuSourceIndex(title: string, selectedIndex: number): void {
if (typeof window === 'undefined') return;
try {
const key = `${STORAGE_KEY_PREFIX}index_${title}`;
sessionStorage.setItem(key, selectedIndex.toString());
console.log(`[弹幕记忆] 保存弹幕源下标: ${title} -> ${selectedIndex}`);
} catch (error) {
console.error('[弹幕记忆] 保存下标失败:', error);
}
}
/**
* 获取自动搜索时上次选择的弹幕源下标
* @param title 视频标题
* @returns 上次选择的下标,如果没有记录则返回 null
*/
export function getDanmakuSourceIndex(title: string): number | null {
if (typeof window === 'undefined') return null;
try {
const key = `${STORAGE_KEY_PREFIX}index_${title}`;
const value = sessionStorage.getItem(key);
if (value !== null) {
const index = parseInt(value, 10);
if (!isNaN(index) && index >= 0) {
console.log(`[弹幕记忆] 读取弹幕源下标: ${title} -> ${index}`);
return index;
}
}
} catch (error) {
console.error('[弹幕记忆] 读取下标失败:', error);
}
return null;
}
/**
* 保存用户手动选择的弹幕剧集 ID
* @param title 视频标题
* @param episodeIndex 视频集数下标
* @param episodeId 弹幕剧集 ID
*/
export function saveManualDanmakuSelection(
title: string,
episodeIndex: number,
episodeId: number
): void {
if (typeof window === 'undefined') return;
try {
const key = `${STORAGE_KEY_PREFIX}manual_${title}_${episodeIndex}`;
sessionStorage.setItem(key, episodeId.toString());
console.log(`[弹幕记忆] 保存手动选择: ${title}${episodeIndex}集 -> ${episodeId}`);
} catch (error) {
console.error('[弹幕记忆] 保存手动选择失败:', error);
}
}
/**
* 获取用户手动选择的弹幕剧集 ID
* @param title 视频标题
* @param episodeIndex 视频集数下标
* @returns 弹幕剧集 ID如果没有记录则返回 null
*/
export function getManualDanmakuSelection(
title: string,
episodeIndex: number
): number | null {
if (typeof window === 'undefined') return null;
try {
const key = `${STORAGE_KEY_PREFIX}manual_${title}_${episodeIndex}`;
const value = sessionStorage.getItem(key);
if (value !== null) {
const episodeId = parseInt(value, 10);
if (!isNaN(episodeId)) {
console.log(`[弹幕记忆] 读取手动选择: ${title}${episodeIndex}集 -> ${episodeId}`);
return episodeId;
}
}
} catch (error) {
console.error('[弹幕记忆] 读取手动选择失败:', error);
}
return null;
}
/**
* 清除指定视频的所有弹幕选择记忆
* @param title 视频标题
*/
export function clearDanmakuSelectionMemory(title: string): void {
if (typeof window === 'undefined') return;
try {
// 清除弹幕源下标记忆
const indexKey = `${STORAGE_KEY_PREFIX}index_${title}`;
sessionStorage.removeItem(indexKey);
// 清除所有手动选择记忆(遍历所有 sessionStorage 键)
const keysToRemove: string[] = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith(`${STORAGE_KEY_PREFIX}manual_${title}_`)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => sessionStorage.removeItem(key));
console.log(`[弹幕记忆] 清除记忆: ${title}`);
} catch (error) {
console.error('[弹幕记忆] 清除记忆失败:', error);
}
}
/**
* 清除所有弹幕选择记忆
*/
export function clearAllDanmakuSelectionMemory(): void {
if (typeof window === 'undefined') return;
try {
const keysToRemove: string[] = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith(STORAGE_KEY_PREFIX)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => sessionStorage.removeItem(key));
console.log('[弹幕记忆] 清除所有记忆');
} catch (error) {
console.error('[弹幕记忆] 清除所有记忆失败:', error);
}
}