Fix Safari playback recovery and episode progress memory
This commit is contained in:
@@ -11,6 +11,7 @@ import React, {
|
||||
} from 'react';
|
||||
|
||||
import type { DanmakuComment,DanmakuSelection } from '@/lib/danmaku/types';
|
||||
import { generateStorageKey, getCachedPlayRecordsSnapshot } from '@/lib/db.client';
|
||||
import { EpisodeFilterConfig,SearchResult } from '@/lib/types';
|
||||
import { getVideoResolutionFromM3u8 } from '@/lib/utils';
|
||||
|
||||
@@ -109,6 +110,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
const [isRetestingAll, setIsRetestingAll] = useState(false);
|
||||
// 标记是否正在进行初始测速
|
||||
const [isInitialTesting, setIsInitialTesting] = useState(false);
|
||||
const [watchedEpisodes, setWatchedEpisodes] = useState<Set<number>>(new Set());
|
||||
|
||||
// 使用 ref 来避免闭包问题
|
||||
const attemptedSourcesRef = useRef<Set<string>>(new Set());
|
||||
@@ -123,6 +125,62 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
videoInfoMapRef.current = videoInfoMap;
|
||||
}, [videoInfoMap]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !currentSource || !currentId) {
|
||||
setWatchedEpisodes(new Set());
|
||||
return;
|
||||
}
|
||||
|
||||
const readWatchedEpisodes = () => {
|
||||
const watched = new Set<number>();
|
||||
|
||||
try {
|
||||
const records = getCachedPlayRecordsSnapshot();
|
||||
const record = records[generateStorageKey(currentSource, currentId)];
|
||||
if (record && record.index > 0 && record.play_time > 1) {
|
||||
watched.add(record.index);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[EpisodeSelector] Failed to read cached play records:', error);
|
||||
}
|
||||
|
||||
for (let episodeNumber = 1; episodeNumber <= totalEpisodes; episodeNumber++) {
|
||||
try {
|
||||
const raw = localStorage.getItem(
|
||||
`moontv_episode_progress:${currentSource}+${currentId}:${episodeNumber - 1}`
|
||||
);
|
||||
if (!raw) continue;
|
||||
|
||||
const parsed = JSON.parse(raw) as { playTime?: number };
|
||||
if (Number(parsed.playTime) > 1) {
|
||||
watched.add(episodeNumber);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[EpisodeSelector] Failed to read local episode progress:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setWatchedEpisodes(watched);
|
||||
};
|
||||
|
||||
readWatchedEpisodes();
|
||||
|
||||
const handlePlayRecordsUpdated = () => {
|
||||
readWatchedEpisodes();
|
||||
};
|
||||
|
||||
window.addEventListener('playRecordsUpdated', handlePlayRecordsUpdated as EventListener);
|
||||
window.addEventListener('storage', handlePlayRecordsUpdated);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
'playRecordsUpdated',
|
||||
handlePlayRecordsUpdated as EventListener
|
||||
);
|
||||
window.removeEventListener('storage', handlePlayRecordsUpdated);
|
||||
};
|
||||
}, [currentSource, currentId, totalEpisodes, value]);
|
||||
|
||||
// 主要的 tab 状态:'danmaku' | 'episodes' | 'sources'
|
||||
// 默认显示选集选项卡,但如果是房员则显示弹幕
|
||||
const [activeTab, setActiveTab] = useState<'danmaku' | 'episodes' | 'sources'>(
|
||||
@@ -723,16 +781,23 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
.filter(episodeNumber => !isEpisodeFiltered(episodeNumber))
|
||||
.map((episodeNumber) => {
|
||||
const isActive = episodeNumber === value;
|
||||
const isWatched = watchedEpisodes.has(episodeNumber);
|
||||
return (
|
||||
<button
|
||||
key={episodeNumber}
|
||||
onClick={() => handleEpisodeClick(episodeNumber - 1)}
|
||||
className={`h-10 min-w-10 px-3 py-2 flex items-center justify-center text-sm font-medium rounded-md transition-all duration-200 whitespace-nowrap font-mono
|
||||
className={`relative h-10 min-w-10 px-3 py-2 flex items-center justify-center text-sm font-medium rounded-md transition-all duration-200 whitespace-nowrap font-mono border
|
||||
${isActive
|
||||
? 'bg-green-500 text-white shadow-lg shadow-green-500/25 dark:bg-green-600'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 hover:scale-105 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
? 'bg-green-500 text-white border-green-400 shadow-lg shadow-green-500/25 dark:bg-green-600'
|
||||
: isWatched
|
||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200 hover:bg-emerald-100 hover:scale-105 dark:bg-emerald-900/20 dark:text-emerald-300 dark:border-emerald-700/60 dark:hover:bg-emerald-900/30'
|
||||
: 'bg-gray-200 text-gray-700 border-transparent hover:bg-gray-300 hover:scale-105 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`.trim()}
|
||||
title={isWatched && !isActive ? '已观看过' : undefined}
|
||||
>
|
||||
{isWatched && !isActive && (
|
||||
<span className='absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400' />
|
||||
)}
|
||||
{(() => {
|
||||
const title = episodes_titles?.[episodeNumber - 1];
|
||||
if (!title) {
|
||||
|
||||
@@ -542,6 +542,10 @@ export const UserMenu: React.FC = () => {
|
||||
const savedDisableAutoLoadDanmaku = localStorage.getItem('disableAutoLoadDanmaku');
|
||||
if (savedDisableAutoLoadDanmaku !== null) {
|
||||
setDisableAutoLoadDanmaku(savedDisableAutoLoadDanmaku === 'true');
|
||||
} else {
|
||||
const runtimeDefault =
|
||||
(window as any).RUNTIME_CONFIG?.DANMAKU_AUTO_LOAD_DEFAULT !== false;
|
||||
setDisableAutoLoadDanmaku(!runtimeDefault);
|
||||
}
|
||||
|
||||
const savedDanmakuMaxCount = localStorage.getItem('danmakuMaxCount');
|
||||
@@ -1339,7 +1343,11 @@ export const UserMenu: React.FC = () => {
|
||||
setBufferStrategy('medium');
|
||||
setNextEpisodePreCache(true);
|
||||
setNextEpisodeDanmakuPreload(true);
|
||||
setDisableAutoLoadDanmaku(false);
|
||||
const defaultDanmakuAutoLoad =
|
||||
(typeof window !== 'undefined' &&
|
||||
(window as any).RUNTIME_CONFIG?.DANMAKU_AUTO_LOAD_DEFAULT !== false) ||
|
||||
false;
|
||||
setDisableAutoLoadDanmaku(!defaultDanmakuAutoLoad);
|
||||
setHomeBannerEnabled(true);
|
||||
setHomeContinueWatchingEnabled(true);
|
||||
setHomeModules(defaultHomeModules);
|
||||
@@ -1364,7 +1372,10 @@ export const UserMenu: React.FC = () => {
|
||||
localStorage.setItem('bufferStrategy', 'medium');
|
||||
localStorage.setItem('nextEpisodePreCache', 'true');
|
||||
localStorage.setItem('nextEpisodeDanmakuPreload', 'true');
|
||||
localStorage.setItem('disableAutoLoadDanmaku', 'false');
|
||||
localStorage.setItem(
|
||||
'disableAutoLoadDanmaku',
|
||||
String(!defaultDanmakuAutoLoad)
|
||||
);
|
||||
localStorage.setItem('danmakuMaxCount', '0');
|
||||
localStorage.setItem('danmaku_heatmap_disabled', 'false');
|
||||
localStorage.setItem('homeBannerEnabled', 'true');
|
||||
|
||||
Reference in New Issue
Block a user