mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
added subtitle addon support
This commit is contained in:
parent
583db67853
commit
7d9f8fba86
4 changed files with 300 additions and 243 deletions
|
|
@ -15,14 +15,13 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
||||||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||||
import { useMetadata } from '../../hooks/useMetadata';
|
import { useMetadata } from '../../hooks/useMetadata';
|
||||||
import { useSettings } from '../../hooks/useSettings';
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
import { testVideoStreamUrl } from '../../utils/httpInterceptor';
|
|
||||||
import { stremioService, Subtitle } from '../../services/stremioService';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_SUBTITLE_SIZE,
|
DEFAULT_SUBTITLE_SIZE,
|
||||||
AudioTrack,
|
AudioTrack,
|
||||||
TextTrack,
|
TextTrack,
|
||||||
ResizeModeType,
|
ResizeModeType,
|
||||||
|
WyzieSubtitle,
|
||||||
SubtitleCue,
|
SubtitleCue,
|
||||||
RESUME_PREF_KEY,
|
RESUME_PREF_KEY,
|
||||||
RESUME_PREF,
|
RESUME_PREF,
|
||||||
|
|
@ -36,6 +35,8 @@ import ResumeOverlay from './modals/ResumeOverlay';
|
||||||
import PlayerControls from './controls/PlayerControls';
|
import PlayerControls from './controls/PlayerControls';
|
||||||
import CustomSubtitles from './subtitles/CustomSubtitles';
|
import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||||
import { SourcesModal } from './modals/SourcesModal';
|
import { SourcesModal } from './modals/SourcesModal';
|
||||||
|
import { stremioService } from '../../services/stremioService';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
// Map VLC resize modes to react-native-video resize modes
|
// Map VLC resize modes to react-native-video resize modes
|
||||||
const getVideoResizeMode = (resizeMode: ResizeModeType) => {
|
const getVideoResizeMode = (resizeMode: ResizeModeType) => {
|
||||||
|
|
@ -145,11 +146,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const pinchRef = useRef<PinchGestureHandler>(null);
|
const pinchRef = useRef<PinchGestureHandler>(null);
|
||||||
const [customSubtitles, setCustomSubtitles] = useState<SubtitleCue[]>([]);
|
const [customSubtitles, setCustomSubtitles] = useState<SubtitleCue[]>([]);
|
||||||
const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
|
const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
|
||||||
|
const [customSubtitleVersion, setCustomSubtitleVersion] = useState<number>(0);
|
||||||
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
|
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
|
||||||
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
|
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
|
||||||
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
||||||
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
||||||
const [availableSubtitles, setAvailableSubtitles] = useState<Subtitle[]>([]);
|
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
||||||
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
|
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
|
||||||
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
|
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
|
||||||
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
|
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
|
||||||
|
|
@ -403,8 +405,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
saveWatchProgress();
|
saveWatchProgress();
|
||||||
}, syncInterval);
|
}, syncInterval);
|
||||||
|
|
||||||
// Removed excessive logging for watch progress save interval
|
|
||||||
|
|
||||||
setProgressSaveInterval(interval);
|
setProgressSaveInterval(interval);
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|
@ -498,8 +498,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
const onLoad = (data: any) => {
|
const onLoad = (data: any) => {
|
||||||
try {
|
try {
|
||||||
// HTTP response logging removed
|
|
||||||
|
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
logger.log('[AndroidVideoPlayer] Video loaded:', data);
|
logger.log('[AndroidVideoPlayer] Video loaded:', data);
|
||||||
}
|
}
|
||||||
|
|
@ -622,7 +620,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
NativeModules.StatusBarManager.setHidden(true);
|
NativeModules.StatusBarManager.setHidden(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Immersive mode error - silently handled
|
console.log('Immersive mode error:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -715,8 +713,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
const handleError = (error: any) => {
|
const handleError = (error: any) => {
|
||||||
try {
|
try {
|
||||||
// HTTP error response logging removed
|
|
||||||
|
|
||||||
logger.error('AndroidVideoPlayer error: ', error);
|
logger.error('AndroidVideoPlayer error: ', error);
|
||||||
|
|
||||||
// Early return if component is unmounted to prevent iOS crashes
|
// Early return if component is unmounted to prevent iOS crashes
|
||||||
|
|
@ -891,100 +887,183 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
setIsLoadingSubtitleList(true);
|
setIsLoadingSubtitleList(true);
|
||||||
try {
|
try {
|
||||||
// Determine content type and ID format for Stremio
|
// Fetch from installed OpenSubtitles v3 addon via Stremio only
|
||||||
let contentType = 'movie';
|
const stremioType = type === 'series' ? 'series' : 'movie';
|
||||||
let contentId = targetImdbId;
|
const stremioVideoId = stremioType === 'series' && season && episode
|
||||||
let videoId: string | undefined;
|
? `series:${targetImdbId}:${season}:${episode}`
|
||||||
|
: undefined;
|
||||||
if (season && episode) {
|
const stremioResults = await stremioService.getSubtitles(stremioType, targetImdbId, stremioVideoId);
|
||||||
contentType = 'series';
|
const stremioSubs: WyzieSubtitle[] = (stremioResults || []).map(sub => ({
|
||||||
videoId = `series:${targetImdbId}:${season}:${episode}`;
|
id: sub.id || `${sub.lang}-${sub.url}`,
|
||||||
}
|
url: sub.url,
|
||||||
|
flagUrl: '',
|
||||||
logger.log(`[AndroidVideoPlayer] Fetching subtitles for ${contentType}: ${contentId}${videoId ? ` (${videoId})` : ''}`);
|
format: 'srt',
|
||||||
|
encoding: 'utf-8',
|
||||||
const subtitles = await stremioService.getSubtitles(contentType, contentId, videoId);
|
media: 'opensubtitles',
|
||||||
|
display: sub.lang || 'Unknown',
|
||||||
// Remove duplicates based on language
|
language: (sub.lang || '').toLowerCase(),
|
||||||
const uniqueSubtitles = subtitles.reduce((acc: Subtitle[], current: Subtitle) => {
|
isHearingImpaired: false,
|
||||||
const exists = acc.find((item: Subtitle) => item.lang === current.lang);
|
source: sub.addonName || 'OpenSubtitles v3',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// De-duplicate by language
|
||||||
|
const uniqueSubtitles = stremioSubs.reduce((acc, current) => {
|
||||||
|
const exists = acc.find(item => item.language === current.language);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
acc.push(current);
|
acc.push(current);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as Subtitle[]);
|
}, [] as WyzieSubtitle[]);
|
||||||
|
uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display));
|
||||||
// Sort by language
|
|
||||||
uniqueSubtitles.sort((a: Subtitle, b: Subtitle) => a.lang.localeCompare(b.lang));
|
|
||||||
setAvailableSubtitles(uniqueSubtitles);
|
setAvailableSubtitles(uniqueSubtitles);
|
||||||
|
|
||||||
if (autoSelectEnglish) {
|
if (autoSelectEnglish) {
|
||||||
const englishSubtitle = uniqueSubtitles.find((sub: Subtitle) =>
|
const englishSubtitle = uniqueSubtitles.find(sub =>
|
||||||
sub.lang.toLowerCase() === 'eng' ||
|
sub.language.toLowerCase() === 'eng' ||
|
||||||
sub.lang.toLowerCase() === 'en' ||
|
sub.language.toLowerCase() === 'en' ||
|
||||||
sub.lang.toLowerCase().includes('english')
|
sub.display.toLowerCase().includes('english')
|
||||||
);
|
);
|
||||||
if (englishSubtitle) {
|
if (englishSubtitle) {
|
||||||
loadStremioSubtitle(englishSubtitle);
|
loadWyzieSubtitle(englishSubtitle);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!autoSelectEnglish) {
|
||||||
if (!autoSelectEnglish && uniqueSubtitles.length > 0) {
|
|
||||||
setShowSubtitleLanguageModal(true);
|
setShowSubtitleLanguageModal(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[AndroidVideoPlayer] Error fetching subtitles from Stremio addons:', error);
|
logger.error('[AndroidVideoPlayer] Error fetching subtitles from OpenSubtitles addon:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingSubtitleList(false);
|
setIsLoadingSubtitleList(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadStremioSubtitle = async (subtitle: Subtitle) => {
|
const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => {
|
||||||
console.log('[AndroidVideoPlayer] Starting subtitle load, setting isLoadingSubtitles to true');
|
logger.log(`[AndroidVideoPlayer] Subtitle click received: id=${subtitle.id}, lang=${subtitle.language}, url=${subtitle.url}`);
|
||||||
setShowSubtitleLanguageModal(false);
|
setShowSubtitleLanguageModal(false);
|
||||||
|
logger.log('[AndroidVideoPlayer] setShowSubtitleLanguageModal(false)');
|
||||||
setIsLoadingSubtitles(true);
|
setIsLoadingSubtitles(true);
|
||||||
|
logger.log('[AndroidVideoPlayer] isLoadingSubtitles -> true');
|
||||||
try {
|
try {
|
||||||
const response = await fetch(subtitle.url);
|
logger.log('[AndroidVideoPlayer] Fetching subtitle SRT start');
|
||||||
const srtContent = await response.text();
|
let srtContent = '';
|
||||||
|
try {
|
||||||
|
const axiosResp = await axios.get(subtitle.url, {
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'text/plain, */*',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Nuvio/1.0'
|
||||||
|
},
|
||||||
|
responseType: 'text',
|
||||||
|
transitional: {
|
||||||
|
clarifyTimeoutError: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
srtContent = typeof axiosResp.data === 'string' ? axiosResp.data : String(axiosResp.data || '');
|
||||||
|
} catch (axiosErr: any) {
|
||||||
|
logger.warn('[AndroidVideoPlayer] Axios subtitle fetch failed, falling back to fetch()', {
|
||||||
|
message: axiosErr?.message,
|
||||||
|
code: axiosErr?.code
|
||||||
|
});
|
||||||
|
// Fallback with explicit timeout using AbortController
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||||
|
try {
|
||||||
|
const resp = await fetch(subtitle.url, { signal: controller.signal });
|
||||||
|
srtContent = await resp.text();
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.log(`[AndroidVideoPlayer] Fetching subtitle SRT done, size=${srtContent.length}`);
|
||||||
const parsedCues = parseSRT(srtContent);
|
const parsedCues = parseSRT(srtContent);
|
||||||
|
logger.log(`[AndroidVideoPlayer] Parsed cues count=${parsedCues.length}`);
|
||||||
|
|
||||||
// Force a microtask delay before updating subtitle state
|
// iOS AVPlayer workaround: clear subtitle state first, then apply
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
if (Platform.OS === 'ios') {
|
||||||
|
logger.log('[AndroidVideoPlayer] iOS detected; clearing subtitle state before apply');
|
||||||
setCustomSubtitles(parsedCues);
|
// Immediately stop spinner so UI doesn't get stuck
|
||||||
setUseCustomSubtitles(true);
|
setIsLoadingSubtitles(false);
|
||||||
setSelectedTextTrack(-1);
|
logger.log('[AndroidVideoPlayer] isLoadingSubtitles -> false (early stop for iOS)');
|
||||||
logger.log(`[AndroidVideoPlayer] Loaded subtitle: ${subtitle.lang} from ${subtitle.addonName}`);
|
// Step 1: Clear any existing subtitle state
|
||||||
console.log('[AndroidVideoPlayer] Subtitle loaded successfully, about to seek');
|
setUseCustomSubtitles(false);
|
||||||
|
logger.log('[AndroidVideoPlayer] useCustomSubtitles -> false');
|
||||||
// Force a state update by triggering a seek
|
setCustomSubtitles([]);
|
||||||
if (videoRef.current && duration > 0 && !isSeeking.current) {
|
logger.log('[AndroidVideoPlayer] customSubtitles -> []');
|
||||||
const currentPos = currentTime;
|
setSelectedTextTrack(-1);
|
||||||
console.log(`[AndroidVideoPlayer] Forcing a micro-seek to refresh subtitle state from ${currentPos}s`);
|
logger.log('[AndroidVideoPlayer] selectedTextTrack -> -1');
|
||||||
seekToTime(currentPos);
|
|
||||||
|
|
||||||
// Wait for seek to complete before clearing loading state
|
// Step 2: Apply immediately (no scheduling), then do a small micro-nudge
|
||||||
setTimeout(() => {
|
logger.log('[AndroidVideoPlayer] Applying parsed cues immediately (iOS)');
|
||||||
console.log('[AndroidVideoPlayer] Clearing isLoadingSubtitles after seek timeout');
|
setCustomSubtitles(parsedCues);
|
||||||
setIsLoadingSubtitles(false);
|
logger.log('[AndroidVideoPlayer] customSubtitles <- parsedCues');
|
||||||
}, 600); // Wait longer than seekToTime's internal timeout (500ms)
|
setUseCustomSubtitles(true);
|
||||||
|
logger.log('[AndroidVideoPlayer] useCustomSubtitles -> true');
|
||||||
|
setSelectedTextTrack(-1);
|
||||||
|
logger.log('[AndroidVideoPlayer] selectedTextTrack -> -1 (disable native while using custom)');
|
||||||
|
setCustomSubtitleVersion(v => v + 1);
|
||||||
|
logger.log('[AndroidVideoPlayer] customSubtitleVersion incremented');
|
||||||
|
|
||||||
|
// Immediately set current subtitle based on currentTime to avoid waiting for next onProgress
|
||||||
|
try {
|
||||||
|
const cueNow = parsedCues.find(cue => currentTime >= cue.start && currentTime <= cue.end);
|
||||||
|
const textNow = cueNow ? cueNow.text : '';
|
||||||
|
setCurrentSubtitle(textNow);
|
||||||
|
logger.log('[AndroidVideoPlayer] currentSubtitle set immediately after apply (iOS)');
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[AndroidVideoPlayer] Error setting immediate subtitle', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Micro-seek nudge to force AVPlayer to refresh rendering
|
||||||
|
try {
|
||||||
|
if (videoRef.current && duration > 0) {
|
||||||
|
const wasPaused = paused;
|
||||||
|
const original = currentTime;
|
||||||
|
const forward = Math.min(original + 0.05, Math.max(duration - 0.1, 0));
|
||||||
|
logger.log('[AndroidVideoPlayer] Performing micro-seek nudge (iOS)', { original, forward });
|
||||||
|
if (wasPaused) {
|
||||||
|
setPaused(false);
|
||||||
|
}
|
||||||
|
// Give state a moment to apply before seeking
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
videoRef.current?.seek(forward);
|
||||||
|
setTimeout(() => {
|
||||||
|
videoRef.current?.seek(original);
|
||||||
|
if (wasPaused) {
|
||||||
|
setPaused(true);
|
||||||
|
}
|
||||||
|
logger.log('[AndroidVideoPlayer] Micro-seek nudge complete (iOS)');
|
||||||
|
}, 150);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[AndroidVideoPlayer] Inner micro-seek failed (iOS)', e);
|
||||||
|
if (wasPaused) setPaused(true);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
logger.warn('[AndroidVideoPlayer] Outer micro-seek failed (iOS)', e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[AndroidVideoPlayer] Cannot seek to refresh subtitles: videoRef=${!!videoRef.current}, duration=${duration}, seeking=${isSeeking.current}`);
|
// Android works immediately
|
||||||
// Clear loading state immediately if we can't seek
|
setCustomSubtitles(parsedCues);
|
||||||
setTimeout(() => {
|
logger.log('[AndroidVideoPlayer] (Android) customSubtitles <- parsedCues');
|
||||||
setIsLoadingSubtitles(false);
|
setUseCustomSubtitles(true);
|
||||||
console.log('[AndroidVideoPlayer] isLoadingSubtitles set to false (no seek)');
|
logger.log('[AndroidVideoPlayer] (Android) useCustomSubtitles -> true');
|
||||||
}, 100);
|
setSelectedTextTrack(-1);
|
||||||
|
logger.log('[AndroidVideoPlayer] (Android) selectedTextTrack -> -1');
|
||||||
|
setIsLoadingSubtitles(false);
|
||||||
|
logger.log('[AndroidVideoPlayer] (Android) isLoadingSubtitles -> false');
|
||||||
|
try {
|
||||||
|
const cueNow = parsedCues.find(cue => currentTime >= cue.start && currentTime <= cue.end);
|
||||||
|
const textNow = cueNow ? cueNow.text : '';
|
||||||
|
setCurrentSubtitle(textNow);
|
||||||
|
logger.log('[AndroidVideoPlayer] currentSubtitle set immediately after apply (Android)');
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[AndroidVideoPlayer] Error loading Stremio subtitle:', error);
|
logger.error('[AndroidVideoPlayer] Error loading Wyzie subtitle:', error);
|
||||||
console.log('[AndroidVideoPlayer] Subtitle loading failed:', error);
|
setIsLoadingSubtitles(false);
|
||||||
// Clear loading state on error
|
logger.log('[AndroidVideoPlayer] isLoadingSubtitles -> false (error path)');
|
||||||
setTimeout(() => {
|
|
||||||
setIsLoadingSubtitles(false);
|
|
||||||
console.log('[AndroidVideoPlayer] isLoadingSubtitles set to false (error)');
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -996,7 +1075,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
// Send a forced pause update to Trakt immediately when user pauses
|
// Send a forced pause update to Trakt immediately when user pauses
|
||||||
if (newPausedState && duration > 0) {
|
if (newPausedState && duration > 0) {
|
||||||
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
||||||
logger.log('[AndroidVideoPlayer] Sent forced pause update to Trakt');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1031,10 +1109,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
currentTime >= cue.start && currentTime <= cue.end
|
currentTime >= cue.start && currentTime <= cue.end
|
||||||
);
|
);
|
||||||
const newSubtitle = currentCue ? currentCue.text : '';
|
const newSubtitle = currentCue ? currentCue.text : '';
|
||||||
if (newSubtitle !== currentSubtitle) {
|
setCurrentSubtitle(newSubtitle);
|
||||||
setCurrentSubtitle(newSubtitle);
|
}, [currentTime, customSubtitles, useCustomSubtitles]);
|
||||||
}
|
|
||||||
}, [currentTime, customSubtitles, useCustomSubtitles, currentSubtitle]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSubtitleSize();
|
loadSubtitleSize();
|
||||||
|
|
@ -1097,23 +1173,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [pendingSeek, isPlayerReady, isVideoLoaded, duration]);
|
}, [pendingSeek, isPlayerReady, isVideoLoaded, duration]);
|
||||||
|
|
||||||
// HTTP stream testing with logging
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentStreamUrl && currentStreamUrl.trim() !== '') {
|
|
||||||
const testStream = async () => {
|
|
||||||
try {
|
|
||||||
// Stream testing without verbose logging
|
|
||||||
await testVideoStreamUrl(currentStreamUrl, headers || {});
|
|
||||||
} catch (error) {
|
|
||||||
// Stream test failed silently
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test the stream URL when it changes
|
|
||||||
testStream();
|
|
||||||
}
|
|
||||||
}, [currentStreamUrl, headers]);
|
|
||||||
|
|
||||||
const handleSelectStream = async (newStream: any) => {
|
const handleSelectStream = async (newStream: any) => {
|
||||||
if (newStream.url === currentStreamUrl) {
|
if (newStream.url === currentStreamUrl) {
|
||||||
setShowSourcesModal(false);
|
setShowSourcesModal(false);
|
||||||
|
|
@ -1315,16 +1374,25 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
headers: headers
|
headers: headers
|
||||||
} : { uri: currentStreamUrl };
|
} : { uri: currentStreamUrl };
|
||||||
|
|
||||||
// HTTP request logging removed
|
// HTTP request logging removed; source prepared
|
||||||
|
|
||||||
return sourceWithHeaders;
|
return sourceWithHeaders;
|
||||||
})()}
|
})()}
|
||||||
paused={paused}
|
paused={paused}
|
||||||
onProgress={handleProgress}
|
onProgress={handleProgress}
|
||||||
onLoad={onLoad}
|
onLoad={(e) => {
|
||||||
|
logger.log('[AndroidVideoPlayer] onLoad fired', { duration: e?.duration });
|
||||||
|
onLoad(e);
|
||||||
|
}}
|
||||||
onEnd={onEnd}
|
onEnd={onEnd}
|
||||||
onError={handleError}
|
onError={(err) => {
|
||||||
onBuffer={onBuffer}
|
logger.error('[AndroidVideoPlayer] onError', err);
|
||||||
|
handleError(err);
|
||||||
|
}}
|
||||||
|
onBuffer={(buf) => {
|
||||||
|
logger.log('[AndroidVideoPlayer] onBuffer', buf);
|
||||||
|
onBuffer(buf);
|
||||||
|
}}
|
||||||
resizeMode={getVideoResizeMode(resizeMode)}
|
resizeMode={getVideoResizeMode(resizeMode)}
|
||||||
selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined}
|
selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined}
|
||||||
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
|
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
|
||||||
|
|
@ -1376,6 +1444,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CustomSubtitles
|
<CustomSubtitles
|
||||||
|
key={customSubtitleVersion}
|
||||||
useCustomSubtitles={useCustomSubtitles}
|
useCustomSubtitles={useCustomSubtitles}
|
||||||
currentSubtitle={currentSubtitle}
|
currentSubtitle={currentSubtitle}
|
||||||
subtitleSize={subtitleSize}
|
subtitleSize={subtitleSize}
|
||||||
|
|
@ -1418,7 +1487,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
subtitleSize={subtitleSize}
|
subtitleSize={subtitleSize}
|
||||||
subtitleBackground={subtitleBackground}
|
subtitleBackground={subtitleBackground}
|
||||||
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
||||||
loadStremioSubtitle={loadStremioSubtitle}
|
loadWyzieSubtitle={loadWyzieSubtitle}
|
||||||
selectTextTrack={selectTextTrack}
|
selectTextTrack={selectTextTrack}
|
||||||
increaseSubtitleSize={increaseSubtitleSize}
|
increaseSubtitleSize={increaseSubtitleSize}
|
||||||
decreaseSubtitleSize={decreaseSubtitleSize}
|
decreaseSubtitleSize={decreaseSubtitleSize}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,13 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
||||||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||||
import { useMetadata } from '../../hooks/useMetadata';
|
import { useMetadata } from '../../hooks/useMetadata';
|
||||||
import { useSettings } from '../../hooks/useSettings';
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
import { stremioService, Subtitle } from '../../services/stremioService';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_SUBTITLE_SIZE,
|
DEFAULT_SUBTITLE_SIZE,
|
||||||
AudioTrack,
|
AudioTrack,
|
||||||
TextTrack,
|
TextTrack,
|
||||||
ResizeModeType,
|
ResizeModeType,
|
||||||
|
WyzieSubtitle,
|
||||||
SubtitleCue,
|
SubtitleCue,
|
||||||
RESUME_PREF_KEY,
|
RESUME_PREF_KEY,
|
||||||
RESUME_PREF,
|
RESUME_PREF,
|
||||||
|
|
@ -37,6 +37,7 @@ import ResumeOverlay from './modals/ResumeOverlay';
|
||||||
import PlayerControls from './controls/PlayerControls';
|
import PlayerControls from './controls/PlayerControls';
|
||||||
import CustomSubtitles from './subtitles/CustomSubtitles';
|
import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||||
import { SourcesModal } from './modals/SourcesModal';
|
import { SourcesModal } from './modals/SourcesModal';
|
||||||
|
import { stremioService } from '../../services/stremioService';
|
||||||
|
|
||||||
const VideoPlayer: React.FC = () => {
|
const VideoPlayer: React.FC = () => {
|
||||||
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
|
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
|
||||||
|
|
@ -51,8 +52,8 @@ const VideoPlayer: React.FC = () => {
|
||||||
// Use AndroidVideoPlayer for:
|
// Use AndroidVideoPlayer for:
|
||||||
// - Android devices
|
// - Android devices
|
||||||
// - Xprime streams on any platform
|
// - Xprime streams on any platform
|
||||||
// - MKV files on iOS are now handled by VideoPlayer.tsx (VLCPlayer)
|
// - Non-MKV files on iOS
|
||||||
if (Platform.OS === 'android' || isXprimeStream) {
|
if (Platform.OS === 'android' || isXprimeStream || (Platform.OS === 'ios' && !isMkvFile)) {
|
||||||
return <AndroidVideoPlayer />;
|
return <AndroidVideoPlayer />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,7 +161,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
|
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
|
||||||
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
||||||
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
||||||
const [availableSubtitles, setAvailableSubtitles] = useState<Subtitle[]>([]);
|
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
||||||
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
|
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
|
||||||
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
|
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
|
||||||
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
|
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
|
||||||
|
|
@ -434,8 +435,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
saveWatchProgress();
|
saveWatchProgress();
|
||||||
}, syncInterval);
|
}, syncInterval);
|
||||||
|
|
||||||
// Removed excessive logging for watch progress save interval
|
|
||||||
|
|
||||||
setProgressSaveInterval(interval);
|
setProgressSaveInterval(interval);
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|
@ -470,7 +469,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
// Send a forced pause update to Trakt immediately when user pauses
|
// Send a forced pause update to Trakt immediately when user pauses
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
||||||
logger.log('[VideoPlayer] Sent forced pause update to Trakt');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -567,7 +565,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
const onLoad = (data: any) => {
|
const onLoad = (data: any) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
logger.log('[VideoPlayer] Video loaded:', data);
|
logger.log('[VideoPlayer] Video loaded:', data);
|
||||||
}
|
}
|
||||||
|
|
@ -797,8 +794,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (error: any) => {
|
const handleError = (error: any) => {
|
||||||
// HTTP error response logging removed
|
|
||||||
|
|
||||||
logger.error('[VideoPlayer] Playback Error:', error);
|
logger.error('[VideoPlayer] Playback Error:', error);
|
||||||
|
|
||||||
// Format error details for user display
|
// Format error details for user display
|
||||||
|
|
@ -907,56 +902,57 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
setIsLoadingSubtitleList(true);
|
setIsLoadingSubtitleList(true);
|
||||||
try {
|
try {
|
||||||
// Determine content type and ID format for Stremio
|
// Fetch from installed OpenSubtitles v3 addon via Stremio only
|
||||||
let contentType = 'movie';
|
const stremioType = type === 'series' ? 'series' : 'movie';
|
||||||
let contentId = targetImdbId;
|
const stremioVideoId = stremioType === 'series' && season && episode
|
||||||
let videoId: string | undefined;
|
? `series:${targetImdbId}:${season}:${episode}`
|
||||||
|
: undefined;
|
||||||
if (season && episode) {
|
const stremioResults = await stremioService.getSubtitles(stremioType, targetImdbId, stremioVideoId);
|
||||||
contentType = 'series';
|
const stremioSubs: WyzieSubtitle[] = (stremioResults || []).map(sub => ({
|
||||||
videoId = `series:${targetImdbId}:${season}:${episode}`;
|
id: sub.id || `${sub.lang}-${sub.url}`,
|
||||||
}
|
url: sub.url,
|
||||||
|
flagUrl: '',
|
||||||
logger.log(`[VideoPlayer] Fetching subtitles for ${contentType}: ${contentId}${videoId ? ` (${videoId})` : ''}`);
|
format: 'srt',
|
||||||
|
encoding: 'utf-8',
|
||||||
const subtitles = await stremioService.getSubtitles(contentType, contentId, videoId);
|
media: 'opensubtitles',
|
||||||
|
display: sub.lang || 'Unknown',
|
||||||
// Remove duplicates based on language
|
language: (sub.lang || '').toLowerCase(),
|
||||||
const uniqueSubtitles = subtitles.reduce((acc: Subtitle[], current: Subtitle) => {
|
isHearingImpaired: false,
|
||||||
const exists = acc.find((item: Subtitle) => item.lang === current.lang);
|
source: sub.addonName || 'OpenSubtitles v3',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// De-duplicate by language
|
||||||
|
const uniqueSubtitles = stremioSubs.reduce((acc, current) => {
|
||||||
|
const exists = acc.find(item => item.language === current.language);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
acc.push(current);
|
acc.push(current);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as Subtitle[]);
|
}, [] as WyzieSubtitle[]);
|
||||||
|
uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display));
|
||||||
// Sort by language
|
|
||||||
uniqueSubtitles.sort((a: Subtitle, b: Subtitle) => a.lang.localeCompare(b.lang));
|
|
||||||
setAvailableSubtitles(uniqueSubtitles);
|
setAvailableSubtitles(uniqueSubtitles);
|
||||||
|
|
||||||
if (autoSelectEnglish) {
|
if (autoSelectEnglish) {
|
||||||
const englishSubtitle = uniqueSubtitles.find((sub: Subtitle) =>
|
const englishSubtitle = uniqueSubtitles.find(sub =>
|
||||||
sub.lang.toLowerCase() === 'eng' ||
|
sub.language.toLowerCase() === 'eng' ||
|
||||||
sub.lang.toLowerCase() === 'en' ||
|
sub.language.toLowerCase() === 'en' ||
|
||||||
sub.lang.toLowerCase().includes('english')
|
sub.display.toLowerCase().includes('english')
|
||||||
);
|
);
|
||||||
if (englishSubtitle) {
|
if (englishSubtitle) {
|
||||||
loadStremioSubtitle(englishSubtitle);
|
loadWyzieSubtitle(englishSubtitle);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!autoSelectEnglish) {
|
||||||
if (!autoSelectEnglish && uniqueSubtitles.length > 0) {
|
|
||||||
setShowSubtitleLanguageModal(true);
|
setShowSubtitleLanguageModal(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[VideoPlayer] Error fetching subtitles from Stremio addons:', error);
|
logger.error('[VideoPlayer] Error fetching subtitles from OpenSubtitles addon:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingSubtitleList(false);
|
setIsLoadingSubtitleList(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadStremioSubtitle = async (subtitle: Subtitle) => {
|
const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => {
|
||||||
setShowSubtitleLanguageModal(false);
|
setShowSubtitleLanguageModal(false);
|
||||||
setIsLoadingSubtitles(true);
|
setIsLoadingSubtitles(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -966,14 +962,10 @@ const VideoPlayer: React.FC = () => {
|
||||||
setCustomSubtitles(parsedCues);
|
setCustomSubtitles(parsedCues);
|
||||||
setUseCustomSubtitles(true);
|
setUseCustomSubtitles(true);
|
||||||
setSelectedTextTrack(-1);
|
setSelectedTextTrack(-1);
|
||||||
logger.log(`[VideoPlayer] Loaded subtitle: ${subtitle.lang} from ${subtitle.addonName}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[VideoPlayer] Error loading Stremio subtitle:', error);
|
logger.error('[VideoPlayer] Error loading Wyzie subtitle:', error);
|
||||||
} finally {
|
} finally {
|
||||||
// Add a small delay to ensure state updates are processed
|
setIsLoadingSubtitles(false);
|
||||||
setTimeout(() => {
|
|
||||||
setIsLoadingSubtitles(false);
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1013,10 +1005,8 @@ const VideoPlayer: React.FC = () => {
|
||||||
currentTime >= cue.start && currentTime <= cue.end
|
currentTime >= cue.start && currentTime <= cue.end
|
||||||
);
|
);
|
||||||
const newSubtitle = currentCue ? currentCue.text : '';
|
const newSubtitle = currentCue ? currentCue.text : '';
|
||||||
if (newSubtitle !== currentSubtitle) {
|
setCurrentSubtitle(newSubtitle);
|
||||||
setCurrentSubtitle(newSubtitle);
|
}, [currentTime, customSubtitles, useCustomSubtitles]);
|
||||||
}
|
|
||||||
}, [currentTime, customSubtitles, useCustomSubtitles, currentSubtitle]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSubtitleSize();
|
loadSubtitleSize();
|
||||||
|
|
@ -1285,17 +1275,15 @@ const VideoPlayer: React.FC = () => {
|
||||||
<VLCPlayer
|
<VLCPlayer
|
||||||
ref={vlcRef}
|
ref={vlcRef}
|
||||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
||||||
source={(() => {
|
source={(() => {
|
||||||
// FORCEFULLY use headers from route params if available - no filtering or modification
|
// FORCEFULLY use headers from route params if available - no filtering or modification
|
||||||
const sourceWithHeaders = headers ? {
|
const sourceWithHeaders = headers ? {
|
||||||
uri: currentStreamUrl,
|
uri: currentStreamUrl,
|
||||||
headers: headers
|
headers: headers
|
||||||
} : { uri: currentStreamUrl };
|
} : { uri: currentStreamUrl };
|
||||||
|
|
||||||
// HTTP request logging removed
|
return sourceWithHeaders;
|
||||||
|
})()}
|
||||||
return sourceWithHeaders;
|
|
||||||
})()}
|
|
||||||
paused={paused}
|
paused={paused}
|
||||||
onProgress={handleProgress}
|
onProgress={handleProgress}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
|
|
@ -1388,7 +1376,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
subtitleSize={subtitleSize}
|
subtitleSize={subtitleSize}
|
||||||
subtitleBackground={subtitleBackground}
|
subtitleBackground={subtitleBackground}
|
||||||
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
||||||
loadStremioSubtitle={loadStremioSubtitle}
|
loadWyzieSubtitle={loadWyzieSubtitle}
|
||||||
selectTextTrack={selectTextTrack}
|
selectTextTrack={selectTextTrack}
|
||||||
increaseSubtitleSize={increaseSubtitleSize}
|
increaseSubtitleSize={increaseSubtitleSize}
|
||||||
decreaseSubtitleSize={decreaseSubtitleSize}
|
decreaseSubtitleSize={decreaseSubtitleSize}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ import Animated, {
|
||||||
SlideOutRight,
|
SlideOutRight,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { styles } from '../utils/playerStyles';
|
import { styles } from '../utils/playerStyles';
|
||||||
import { SubtitleCue } from '../utils/playerTypes';
|
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
|
||||||
import { Subtitle } from '../../../services/stremioService';
|
|
||||||
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
|
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
|
||||||
|
|
||||||
interface SubtitleModalsProps {
|
interface SubtitleModalsProps {
|
||||||
|
|
@ -20,14 +19,14 @@ interface SubtitleModalsProps {
|
||||||
isLoadingSubtitleList: boolean;
|
isLoadingSubtitleList: boolean;
|
||||||
isLoadingSubtitles: boolean;
|
isLoadingSubtitles: boolean;
|
||||||
customSubtitles: SubtitleCue[];
|
customSubtitles: SubtitleCue[];
|
||||||
availableSubtitles: Subtitle[];
|
availableSubtitles: WyzieSubtitle[];
|
||||||
vlcTextTracks: Array<{id: number, name: string, language?: string}>;
|
vlcTextTracks: Array<{id: number, name: string, language?: string}>;
|
||||||
selectedTextTrack: number;
|
selectedTextTrack: number;
|
||||||
useCustomSubtitles: boolean;
|
useCustomSubtitles: boolean;
|
||||||
subtitleSize: number;
|
subtitleSize: number;
|
||||||
subtitleBackground: boolean;
|
subtitleBackground: boolean;
|
||||||
fetchAvailableSubtitles: () => void;
|
fetchAvailableSubtitles: () => void;
|
||||||
loadStremioSubtitle: (subtitle: Subtitle) => void;
|
loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void;
|
||||||
selectTextTrack: (trackId: number) => void;
|
selectTextTrack: (trackId: number) => void;
|
||||||
increaseSubtitleSize: () => void;
|
increaseSubtitleSize: () => void;
|
||||||
decreaseSubtitleSize: () => void;
|
decreaseSubtitleSize: () => void;
|
||||||
|
|
@ -52,7 +51,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
subtitleSize,
|
subtitleSize,
|
||||||
subtitleBackground,
|
subtitleBackground,
|
||||||
fetchAvailableSubtitles,
|
fetchAvailableSubtitles,
|
||||||
loadStremioSubtitle,
|
loadWyzieSubtitle,
|
||||||
selectTextTrack,
|
selectTextTrack,
|
||||||
increaseSubtitleSize,
|
increaseSubtitleSize,
|
||||||
decreaseSubtitleSize,
|
decreaseSubtitleSize,
|
||||||
|
|
@ -60,7 +59,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
// Track which specific online subtitle is currently loaded
|
// Track which specific online subtitle is currently loaded
|
||||||
const [selectedOnlineSubtitleId, setSelectedOnlineSubtitleId] = React.useState<string | null>(null);
|
const [selectedOnlineSubtitleId, setSelectedOnlineSubtitleId] = React.useState<string | null>(null);
|
||||||
// Track which subtitle is currently being loaded
|
// Track which online subtitle is currently loading to show spinner per-item
|
||||||
const [loadingSubtitleId, setLoadingSubtitleId] = React.useState<string | null>(null);
|
const [loadingSubtitleId, setLoadingSubtitleId] = React.useState<string | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -76,30 +75,25 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
}
|
}
|
||||||
}, [useCustomSubtitles]);
|
}, [useCustomSubtitles]);
|
||||||
|
|
||||||
|
// Clear loading state when subtitles have finished loading
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isLoadingSubtitles) {
|
||||||
|
setLoadingSubtitleId(null);
|
||||||
|
}
|
||||||
|
}, [isLoadingSubtitles]);
|
||||||
|
|
||||||
|
// Only OpenSubtitles are provided now; render as a single list
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setShowSubtitleModal(false);
|
setShowSubtitleModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadStremioSubtitle = (subtitle: Subtitle) => {
|
const handleLoadWyzieSubtitle = (subtitle: WyzieSubtitle) => {
|
||||||
console.log('[SubtitleModals] Starting to load subtitle:', subtitle.id);
|
|
||||||
setLoadingSubtitleId(subtitle.id);
|
|
||||||
setSelectedOnlineSubtitleId(subtitle.id);
|
setSelectedOnlineSubtitleId(subtitle.id);
|
||||||
loadStremioSubtitle(subtitle);
|
setLoadingSubtitleId(subtitle.id);
|
||||||
|
loadWyzieSubtitle(subtitle);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear loading state when subtitle loading is complete
|
|
||||||
React.useEffect(() => {
|
|
||||||
console.log('[SubtitleModals] isLoadingSubtitles changed:', isLoadingSubtitles);
|
|
||||||
if (!isLoadingSubtitles) {
|
|
||||||
console.log('[SubtitleModals] Clearing loadingSubtitleId');
|
|
||||||
// Force clear loading state with a small delay to ensure proper re-render
|
|
||||||
setTimeout(() => {
|
|
||||||
setLoadingSubtitleId(null);
|
|
||||||
console.log('[SubtitleModals] loadingSubtitleId cleared');
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
}, [isLoadingSubtitles]);
|
|
||||||
|
|
||||||
// Main subtitle menu
|
// Main subtitle menu
|
||||||
const renderSubtitleMenu = () => {
|
const renderSubtitleMenu = () => {
|
||||||
if (!showSubtitleModal) return null;
|
if (!showSubtitleModal) return null;
|
||||||
|
|
@ -398,57 +392,8 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{availableSubtitles.length > 0 ? (
|
{(availableSubtitles.length === 0) && !isLoadingSubtitleList ? (
|
||||||
<View style={{ gap: 8 }}>
|
|
||||||
{availableSubtitles.map((sub) => {
|
|
||||||
const isSelected = useCustomSubtitles && selectedOnlineSubtitleId === sub.id;
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={sub.id}
|
|
||||||
style={{
|
|
||||||
backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)',
|
|
||||||
borderRadius: 16,
|
|
||||||
padding: 16,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
|
|
||||||
}}
|
|
||||||
onPress={() => {
|
|
||||||
handleLoadStremioSubtitle(sub);
|
|
||||||
}}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
disabled={loadingSubtitleId === sub.id}
|
|
||||||
>
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Text style={{
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: '500',
|
|
||||||
marginBottom: 4,
|
|
||||||
}}>
|
|
||||||
{formatLanguage(sub.lang)}
|
|
||||||
</Text>
|
|
||||||
<Text style={{
|
|
||||||
color: 'rgba(255, 255, 255, 0.6)',
|
|
||||||
fontSize: 13,
|
|
||||||
}}>
|
|
||||||
{sub.addonName || 'Stremio Addon'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
{loadingSubtitleId === sub.id ? (
|
|
||||||
<ActivityIndicator size="small" color="#22C55E" />
|
|
||||||
) : isSelected ? (
|
|
||||||
<MaterialIcons name="check" size={20} color="#22C55E" />
|
|
||||||
) : (
|
|
||||||
<MaterialIcons name="download" size={20} color="rgba(255,255,255,0.4)" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
) : !isLoadingSubtitleList ? (
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
|
@ -472,7 +417,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
Tap to search online
|
Tap to search online
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : isLoadingSubtitleList ? (
|
||||||
<View style={{
|
<View style={{
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
|
|
@ -488,6 +433,47 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
Searching...
|
Searching...
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={{ gap: 8 }}>
|
||||||
|
{availableSubtitles.map((sub) => {
|
||||||
|
const isSelected = useCustomSubtitles && selectedOnlineSubtitleId === sub.id;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={sub.id}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
handleLoadWyzieSubtitle(sub);
|
||||||
|
}}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
disabled={isLoadingSubtitles}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={{ color: '#FFFFFF', fontSize: 15, fontWeight: '500', marginBottom: 4 }}>
|
||||||
|
{sub.display}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', fontSize: 13 }}>
|
||||||
|
{formatLanguage(sub.language)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{(isLoadingSubtitles && loadingSubtitleId === sub.id) ? (
|
||||||
|
<ActivityIndicator size="small" color="#22C55E" />
|
||||||
|
) : isSelected ? (
|
||||||
|
<MaterialIcons name="check" size={20} color="#22C55E" />
|
||||||
|
) : (
|
||||||
|
<MaterialIcons name="download" size={20} color="rgba(255,255,255,0.4)" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -557,4 +543,4 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SubtitleModals;
|
export default SubtitleModals;
|
||||||
|
|
@ -71,4 +71,18 @@ export interface SubtitleCue {
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add interface for Wyzie subtitle API response
|
||||||
|
export interface WyzieSubtitle {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
flagUrl: string;
|
||||||
|
format: string;
|
||||||
|
encoding: string;
|
||||||
|
media: string;
|
||||||
|
display: string;
|
||||||
|
language: string;
|
||||||
|
isHearingImpaired: boolean;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue