Fix Safari playback recovery and episode progress memory

This commit is contained in:
ShiGuangAlex
2026-04-14 17:40:30 +08:00
parent a9fa674f23
commit 62e0c12d81
9 changed files with 556 additions and 57 deletions
+68 -3
View File
@@ -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) {
+13 -2
View File
@@ -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');