记住用户选择的弹幕,调整弹幕选项卡可滑动区域
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
153
src/lib/danmaku/selection-memory.ts
Normal file
153
src/lib/danmaku/selection-memory.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user