记住用户选择的弹幕,调整弹幕选项卡可滑动区域
This commit is contained in:
+95
-7
@@ -36,6 +36,12 @@ import {
|
|||||||
initDanmakuModule,
|
initDanmakuModule,
|
||||||
getDanmakuFromCache,
|
getDanmakuFromCache,
|
||||||
} from '@/lib/danmaku/api';
|
} from '@/lib/danmaku/api';
|
||||||
|
import {
|
||||||
|
getDanmakuSourceIndex,
|
||||||
|
saveDanmakuSourceIndex,
|
||||||
|
getManualDanmakuSelection,
|
||||||
|
saveManualDanmakuSelection,
|
||||||
|
} from '@/lib/danmaku/selection-memory';
|
||||||
import type { DanmakuAnime, DanmakuSelection, DanmakuSettings } from '@/lib/danmaku/types';
|
import type { DanmakuAnime, DanmakuSelection, DanmakuSettings } from '@/lib/danmaku/types';
|
||||||
import { SearchResult, DanmakuFilterConfig, EpisodeFilterConfig } from '@/lib/types';
|
import { SearchResult, DanmakuFilterConfig, EpisodeFilterConfig } from '@/lib/types';
|
||||||
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
||||||
@@ -563,9 +569,16 @@ function PlayPageClient() {
|
|||||||
// 先尝试从 IndexedDB 缓存加载
|
// 先尝试从 IndexedDB 缓存加载
|
||||||
try {
|
try {
|
||||||
const cachedComments = await getDanmakuFromCache(title, episodeIndex);
|
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}`);
|
console.log(`[弹幕] 使用缓存: title="${title}", episodeIndex=${episodeIndex}, 数量=${cachedComments.length}`);
|
||||||
|
|
||||||
|
// 如果弹幕插件还未初始化,等待初始化
|
||||||
|
if (!danmakuPluginRef.current) {
|
||||||
|
console.log('[弹幕] 弹幕插件未初始化,等待初始化...');
|
||||||
|
// 缓存命中但插件未初始化,不执行搜索,等待下次触发
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setDanmakuLoading(true);
|
setDanmakuLoading(true);
|
||||||
|
|
||||||
// 转换弹幕格式
|
// 转换弹幕格式
|
||||||
@@ -631,8 +644,26 @@ function PlayPageClient() {
|
|||||||
console.error('[弹幕] 读取缓存失败:', error);
|
console.error('[弹幕] 读取缓存失败:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有缓存,执行自动搜索弹幕
|
// 没有缓存,先检查是否有手动选择的剧集 ID
|
||||||
console.log(`[弹幕] 第 ${episodeIndex + 1} 集缓存未命中,开始搜索`);
|
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);
|
setDanmakuLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -647,9 +678,54 @@ function PlayPageClient() {
|
|||||||
videoYear
|
videoYear
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果有多个匹配结果,让用户选择
|
// 如果有多个匹配结果,先检查是否有记忆的选择
|
||||||
if (filteredAnimes.length > 1) {
|
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);
|
setDanmakuMatches(filteredAnimes);
|
||||||
setCurrentSearchKeyword(title);
|
setCurrentSearchKeyword(title);
|
||||||
setShowDanmakuSourceSelector(true);
|
setShowDanmakuSourceSelector(true);
|
||||||
@@ -2842,6 +2918,13 @@ function PlayPageClient() {
|
|||||||
const handleDanmakuSelect = async (selection: DanmakuSelection) => {
|
const handleDanmakuSelect = async (selection: DanmakuSelection) => {
|
||||||
setCurrentDanmakuSelection(selection);
|
setCurrentDanmakuSelection(selection);
|
||||||
|
|
||||||
|
// 保存手动选择的剧集 ID 到 sessionStorage
|
||||||
|
const title = videoTitleRef.current;
|
||||||
|
const episodeIndex = currentEpisodeIndexRef.current;
|
||||||
|
if (title && episodeIndex >= 0) {
|
||||||
|
saveManualDanmakuSelection(title, episodeIndex, selection.episodeId);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取该动漫的所有剧集列表
|
// 获取该动漫的所有剧集列表
|
||||||
try {
|
try {
|
||||||
const episodesResult = await getEpisodes(selection.animeId);
|
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);
|
setShowDanmakuSourceSelector(false);
|
||||||
setDanmakuLoading(true);
|
setDanmakuLoading(true);
|
||||||
|
|
||||||
@@ -2865,6 +2948,11 @@ function PlayPageClient() {
|
|||||||
const title = videoTitleRef.current;
|
const title = videoTitleRef.current;
|
||||||
console.log('[弹幕] 用户选择弹幕源 - 视频:', title, '弹幕源:', selectedAnime.animeTitle);
|
console.log('[弹幕] 用户选择弹幕源 - 视频:', title, '弹幕源:', selectedAnime.animeTitle);
|
||||||
|
|
||||||
|
// 如果提供了下标,保存到 sessionStorage
|
||||||
|
if (selectedIndex !== undefined && title) {
|
||||||
|
saveDanmakuSourceIndex(title, selectedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取剧集列表
|
// 获取剧集列表
|
||||||
const episodesResult = await getEpisodes(selectedAnime.animeId);
|
const episodesResult = await getEpisodes(selectedAnime.animeId);
|
||||||
|
|
||||||
@@ -5485,7 +5573,7 @@ function PlayPageClient() {
|
|||||||
{danmakuMatches.map((anime, index) => (
|
{danmakuMatches.map((anime, index) => (
|
||||||
<button
|
<button
|
||||||
key={anime.animeId}
|
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
|
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
|
hover:bg-gray-100 dark:hover:bg-gray-700 rounded-xl transition-all
|
||||||
duration-200 text-left group border-2 border-transparent
|
duration-200 text-left group border-2 border-transparent
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function DanmakuPanel({
|
|||||||
} else {
|
} else {
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
setSearchError(
|
setSearchError(
|
||||||
response.errorMessage || '未找到匹配的动漫,请尝试其他关键词'
|
response.errorMessage || '未找到匹配的剧集,请尝试其他关键词'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -75,7 +75,7 @@ export default function DanmakuPanel({
|
|||||||
setEpisodes(response.bangumi.episodes);
|
setEpisodes(response.bangumi.episodes);
|
||||||
} else {
|
} else {
|
||||||
setEpisodes([]);
|
setEpisodes([]);
|
||||||
setSearchError('该动漫暂无剧集信息');
|
setSearchError('该剧集暂无弹幕信息');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取剧集失败:', error);
|
console.error('获取剧集失败:', error);
|
||||||
@@ -128,7 +128,7 @@ export default function DanmakuPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex h-full flex-col overflow-hidden'>
|
<div className='flex h-full flex-col overflow-hidden'>
|
||||||
{/* 搜索区域 */}
|
{/* 搜索区域 - 固定在顶部 */}
|
||||||
<div className='mb-4 flex-shrink-0'>
|
<div className='mb-4 flex-shrink-0'>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<input
|
<input
|
||||||
@@ -140,7 +140,7 @@ export default function DanmakuPanel({
|
|||||||
handleSearch(searchKeyword);
|
handleSearch(searchKeyword);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder='输入动漫名称搜索弹幕...'
|
placeholder='输入剧集名称搜索弹幕...'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
autoCorrect='off'
|
autoCorrect='off'
|
||||||
autoCapitalize='off'
|
autoCapitalize='off'
|
||||||
@@ -174,10 +174,23 @@ export default function DanmakuPanel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 && (
|
{currentSelection && (
|
||||||
<div
|
<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'
|
px-3 py-2 text-sm'
|
||||||
>
|
>
|
||||||
<p className='font-semibold text-green-600 dark:text-green-400'>
|
<p className='font-semibold text-green-600 dark:text-green-400'>
|
||||||
@@ -192,19 +205,8 @@ export default function DanmakuPanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 错误提示 */}
|
{/* 内容区域 */}
|
||||||
{searchError && (
|
<div>
|
||||||
<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'>
|
|
||||||
{/* 显示剧集列表 */}
|
{/* 显示剧集列表 */}
|
||||||
{selectedAnime && (
|
{selectedAnime && (
|
||||||
<div className='space-y-2'>
|
<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'>
|
<div className='flex flex-col items-center justify-center py-12 text-center'>
|
||||||
<MagnifyingGlassIcon className='mb-3 h-12 w-12 text-gray-400' />
|
<MagnifyingGlassIcon className='mb-3 h-12 w-12 text-gray-400' />
|
||||||
<p className='text-sm text-gray-500 dark:text-gray-400'>
|
<p className='text-sm text-gray-500 dark:text-gray-400'>
|
||||||
输入动漫名称搜索弹幕
|
输入剧集名称搜索弹幕
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user