some videoplayer enhancemnets

This commit is contained in:
tapframe 2025-09-16 02:42:18 +05:30
parent 680a1b1ea6
commit 3c839c5ea1
14 changed files with 831 additions and 236 deletions

View file

@ -271,6 +271,8 @@ const AndroidVideoPlayer: React.FC = () => {
const [currentQuality, setCurrentQuality] = useState<string | undefined>(quality);
const [currentStreamProvider, setCurrentStreamProvider] = useState<string | undefined>(streamProvider);
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
const [lastAudioTrackCheck, setLastAudioTrackCheck] = useState<number>(0);
const [audioTrackFallbackAttempts, setAudioTrackFallbackAttempts] = useState<number>(0);
const isMounted = useRef(true);
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
@ -961,6 +963,54 @@ const AndroidVideoPlayer: React.FC = () => {
const bufferedTime = data.playableDuration || currentTimeInSeconds;
safeSetState(() => setBuffered(bufferedTime));
}
// Periodic check for disabled audio track (every 3 seconds, max 3 attempts)
const now = Date.now();
if (now - lastAudioTrackCheck > 3000 && !paused && duration > 0 && audioTrackFallbackAttempts < 3) {
setLastAudioTrackCheck(now);
// Check if audio track is disabled (-1) and we have available tracks
if (selectedAudioTrack === -1 && rnVideoAudioTracks.length > 1) {
logger.warn('[AndroidVideoPlayer] Detected disabled audio track, attempting fallback');
// Find a fallback audio track (prefer stereo/standard formats)
const fallbackTrack = rnVideoAudioTracks.find((track, index) => {
const trackName = (track.name || '').toLowerCase();
const trackLang = (track.language || '').toLowerCase();
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
return !trackName.includes('truehd') &&
!trackName.includes('dts') &&
!trackName.includes('dolby') &&
!trackName.includes('atmos') &&
!trackName.includes('7.1') &&
!trackName.includes('5.1') &&
index !== selectedAudioTrack; // Don't select the same track
});
if (fallbackTrack) {
const fallbackIndex = rnVideoAudioTracks.indexOf(fallbackTrack);
logger.warn(`[AndroidVideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (index: ${fallbackIndex})`);
// Increment fallback attempts counter
setAudioTrackFallbackAttempts(prev => prev + 1);
// Switch to fallback audio track
setSelectedAudioTrack(fallbackIndex);
// Brief pause to allow track switching
setPaused(true);
setTimeout(() => {
if (isMounted.current) {
setPaused(false);
}
}, 500);
} else {
logger.warn('[AndroidVideoPlayer] No suitable fallback audio track found');
// Increment attempts even if no fallback found to prevent infinite checking
setAudioTrackFallbackAttempts(prev => prev + 1);
}
}
}
};
const onLoad = (data: any) => {
@ -1003,13 +1053,132 @@ const AndroidVideoPlayer: React.FC = () => {
// Handle audio tracks
if (data.audioTracks && data.audioTracks.length > 0) {
// Enhanced debug logging to see all available fields
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Raw audio tracks data:`, data.audioTracks);
data.audioTracks.forEach((track: any, idx: number) => {
logger.log(`[AndroidVideoPlayer] Track ${idx} raw data:`, {
index: track.index,
title: track.title,
language: track.language,
type: track.type,
channels: track.channels,
bitrate: track.bitrate,
codec: track.codec,
sampleRate: track.sampleRate,
name: track.name,
label: track.label,
allKeys: Object.keys(track),
fullTrackObject: track
});
});
}
const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => {
const trackIndex = track.index !== undefined ? track.index : index;
const trackName = track.title || track.language || `Audio ${index + 1}`;
const trackLanguage = track.language || 'Unknown';
// Build comprehensive track name from available fields
let trackName = '';
const parts = [];
// Add language if available (try multiple possible fields)
let language = track.language || track.lang || track.languageCode;
// If no language field, try to extract from track name (e.g., "[Russian]", "[English]")
if ((!language || language === 'Unknown' || language === 'und' || language === '') && track.name) {
const languageMatch = track.name.match(/\[([^\]]+)\]/);
if (languageMatch && languageMatch[1]) {
language = languageMatch[1].trim();
}
}
if (language && language !== 'Unknown' && language !== 'und' && language !== '') {
parts.push(language.toUpperCase());
}
// Add codec information if available (try multiple possible fields)
const codec = track.type || track.codec || track.format;
if (codec && codec !== 'Unknown') {
parts.push(codec.toUpperCase());
}
// Add channel information if available
const channels = track.channels || track.channelCount;
if (channels && channels > 0) {
if (channels === 1) {
parts.push('MONO');
} else if (channels === 2) {
parts.push('STEREO');
} else if (channels === 6) {
parts.push('5.1CH');
} else if (channels === 8) {
parts.push('7.1CH');
} else {
parts.push(`${channels}CH`);
}
}
// Add bitrate if available
const bitrate = track.bitrate || track.bitRate;
if (bitrate && bitrate > 0) {
parts.push(`${Math.round(bitrate / 1000)}kbps`);
}
// Add sample rate if available
const sampleRate = track.sampleRate || track.sample_rate;
if (sampleRate && sampleRate > 0) {
parts.push(`${Math.round(sampleRate / 1000)}kHz`);
}
// Add title if available and not generic
let title = track.title || track.name || track.label;
if (title && !title.match(/^(Audio|Track)\s*\d*$/i) && title !== 'Unknown') {
// Clean up title by removing language brackets and trailing punctuation
title = title.replace(/\s*\[[^\]]+\]\s*[-–—]*\s*$/, '').trim();
if (title && title !== 'Unknown') {
parts.push(title);
}
}
// Combine parts or fallback to generic name
if (parts.length > 0) {
trackName = parts.join(' • ');
} else {
// For simple track names like "Track 1", "Audio 1", etc., use them as-is
const simpleName = track.name || track.title || track.label;
if (simpleName && simpleName.match(/^(Track|Audio)\s*\d*$/i)) {
trackName = simpleName;
} else {
// Try to extract any meaningful info from the track object
const meaningfulFields: string[] = [];
Object.keys(track).forEach(key => {
const value = track[key];
if (value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1) {
meaningfulFields.push(`${key}: ${value}`);
}
});
if (meaningfulFields.length > 0) {
trackName = `Audio ${index + 1} (${meaningfulFields.slice(0, 2).join(', ')})`;
} else {
trackName = `Audio ${index + 1}`;
}
}
}
const trackLanguage = language || 'Unknown';
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Audio track ${index}: index=${trackIndex}, name="${trackName}", language="${trackLanguage}"`);
logger.log(`[AndroidVideoPlayer] Processed track ${index}:`, {
index: trackIndex,
name: trackName,
language: trackLanguage,
parts: parts,
meaningfulFields: Object.keys(track).filter(key => {
const value = track[key];
return value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1;
})
});
}
return {
@ -1020,12 +1189,24 @@ const AndroidVideoPlayer: React.FC = () => {
});
setRnVideoAudioTracks(formattedAudioTracks);
// Auto-select the first audio track if none is selected
// Auto-select English audio track if available, otherwise first track
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
const firstTrack = formattedAudioTracks[0];
setSelectedAudioTrack(firstTrack.id);
// Look for English track first
const englishTrack = formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => {
const lang = (track.language || '').toLowerCase();
return lang === 'english' || lang === 'en' || lang === 'eng' ||
(track.name && track.name.toLowerCase().includes('english'));
});
const selectedTrack = englishTrack || formattedAudioTracks[0];
setSelectedAudioTrack(selectedTrack.id);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Auto-selected first audio track: ${firstTrack.name} (ID: ${firstTrack.id})`);
if (englishTrack) {
logger.log(`[AndroidVideoPlayer] Auto-selected English audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`);
} else {
logger.log(`[AndroidVideoPlayer] No English track found, auto-selected first audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`);
}
}
}
@ -1047,6 +1228,10 @@ const AndroidVideoPlayer: React.FC = () => {
setIsVideoLoaded(true);
setIsPlayerReady(true);
// Reset audio track fallback attempts when new video loads
setAudioTrackFallbackAttempts(0);
setLastAudioTrackCheck(0);
// Start Trakt watching session when video loads with proper duration
if (videoDuration > 0) {
traktAutosync.handlePlaybackStart(currentTime, videoDuration);
@ -1245,27 +1430,33 @@ const AndroidVideoPlayer: React.FC = () => {
return;
}
// Check for Dolby Digital Plus audio codec errors (ExoPlayer)
const isDolbyCodecError = error?.error?.errorCode === '24001' ||
error?.errorCode === '24001' ||
(error?.error?.errorString &&
error.error.errorString.includes('ERROR_CODE_DECODER_INIT_FAILED')) ||
(error?.error?.errorException &&
error.error.errorException.includes('audio/eac3')) ||
(error?.error?.errorException &&
error.error.errorException.includes('Dolby Digital Plus'));
// Check for audio codec errors (TrueHD, DTS, Dolby, etc.)
const isAudioCodecError =
error?.error?.errorCode === '24001' ||
error?.errorCode === '24001' ||
(error?.error?.errorString &&
error.error.errorString.includes('ERROR_CODE_DECODER_INIT_FAILED')) ||
(error?.error?.errorException &&
error.error.errorException.includes('audio/eac3')) ||
(error?.error?.errorException &&
error.error.errorException.includes('Dolby Digital Plus')) ||
(error?.message && /(trhd|truehd|true\s?hd|dts|dolby|atmos|e-ac3|ac3)/i.test(error.message)) ||
(error?.error?.message && /(trhd|truehd|true\s?hd|dts|dolby|atmos|e-ac3|ac3)/i.test(error.error.message)) ||
(error?.title && /codec not supported/i.test(error.title));
// Handle Dolby Digital Plus codec errors with audio track fallback
if (isDolbyCodecError && rnVideoAudioTracks.length > 1) {
logger.warn('[AndroidVideoPlayer] Dolby Digital Plus codec error detected, attempting audio track fallback');
// Handle audio codec errors with automatic fallback
if (isAudioCodecError && rnVideoAudioTracks.length > 1) {
logger.warn('[AndroidVideoPlayer] Audio codec error detected, attempting audio track fallback');
// Find a non-Dolby audio track (usually index 0 is stereo/standard)
// Find a fallback audio track (prefer stereo/standard formats)
const fallbackTrack = rnVideoAudioTracks.find((track, index) => {
const trackName = (track.name || '').toLowerCase();
const trackLang = (track.language || '').toLowerCase();
// Prefer stereo, AAC, or standard audio formats
return !trackName.includes('dolby') &&
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
return !trackName.includes('truehd') &&
!trackName.includes('dts') &&
!trackName.includes('dolby') &&
!trackName.includes('atmos') &&
!trackName.includes('7.1') &&
!trackName.includes('5.1') &&
index !== selectedAudioTrack; // Don't select the same track
@ -1415,8 +1606,8 @@ const AndroidVideoPlayer: React.FC = () => {
// Format error details for user display
let errorMessage = 'An unknown error occurred';
if (error) {
if (isDolbyCodecError) {
errorMessage = 'Audio codec compatibility issue detected. The video contains Dolby Digital Plus audio which is not supported on this device. Please try selecting a different audio track or use an alternative video source.';
if (isAudioCodecError) {
errorMessage = 'Audio codec compatibility issue detected. The video contains unsupported audio codec (TrueHD/DTS/Dolby). Please try selecting a different audio track or use an alternative video source.';
} else if (isHeavyCodecDecoderError) {
errorMessage = 'Audio codec issue (DTS/TrueHD/Atmos). Switching to a stereo/standard audio track may help.';
} else if (isServerConfigError) {

View file

@ -84,17 +84,6 @@ const VideoPlayer: React.FC = () => {
// Use VideoPlayer (VLC) for:
// - MKV files on iOS (unless forceVlc is set)
const shouldUseAndroidPlayer = Platform.OS === 'android' || isXprimeStream || (Platform.OS === 'ios' && !isMkvFile && !forceVlc);
if (__DEV__) {
logger.log('[VideoPlayer] Player selection:', {
platform: Platform.OS,
isXprimeStream,
isMkvFile,
forceVlc: !!forceVlc,
selected: shouldUseAndroidPlayer ? 'AndroidVideoPlayer' : 'VLCPlayer',
streamProvider,
uri
});
}
if (shouldUseAndroidPlayer) {
return <AndroidVideoPlayer />;
}
@ -230,7 +219,6 @@ const VideoPlayer: React.FC = () => {
try {
// Always decode URLs for VLC as it has trouble with encoded characters
const decoded = decodeURIComponent(url);
logger.log('[VideoPlayer] Decoded URL for VLC:', { original: url, decoded });
return decoded;
} catch (e) {
logger.warn('[VideoPlayer] URL decoding failed, using original:', e);
@ -247,6 +235,8 @@ const VideoPlayer: React.FC = () => {
const [currentQuality, setCurrentQuality] = useState<string | undefined>(quality);
const [currentStreamProvider, setCurrentStreamProvider] = useState<string | undefined>(streamProvider);
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
const [lastAudioTrackCheck, setLastAudioTrackCheck] = useState<number>(0);
const [audioTrackFallbackAttempts, setAudioTrackFallbackAttempts] = useState<number>(0);
const isMounted = useRef(true);
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
@ -920,6 +910,54 @@ const VideoPlayer: React.FC = () => {
const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds;
safeSetState(() => setBuffered(bufferedTime));
}
// Periodic check for disabled audio track (every 3 seconds, max 3 attempts)
const now = Date.now();
if (now - lastAudioTrackCheck > 3000 && !paused && duration > 0 && audioTrackFallbackAttempts < 3) {
setLastAudioTrackCheck(now);
// Check if audio track is disabled (-1) and we have available tracks
if (selectedAudioTrack === -1 && vlcAudioTracks.length > 1) {
logger.warn('[VideoPlayer] Detected disabled audio track, attempting fallback');
// Find a fallback audio track (prefer stereo/standard formats)
const fallbackTrack = vlcAudioTracks.find((track, index) => {
const trackName = (track.name || '').toLowerCase();
const trackLang = (track.language || '').toLowerCase();
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
return !trackName.includes('truehd') &&
!trackName.includes('dts') &&
!trackName.includes('dolby') &&
!trackName.includes('atmos') &&
!trackName.includes('7.1') &&
!trackName.includes('5.1') &&
index !== selectedAudioTrack; // Don't select the same track
});
if (fallbackTrack) {
const fallbackIndex = vlcAudioTracks.indexOf(fallbackTrack);
logger.warn(`[VideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (index: ${fallbackIndex})`);
// Increment fallback attempts counter
setAudioTrackFallbackAttempts(prev => prev + 1);
// Switch to fallback audio track
setSelectedAudioTrack(fallbackIndex);
// Brief pause to allow track switching
setPaused(true);
setTimeout(() => {
if (isMounted.current) {
setPaused(false);
}
}, 500);
} else {
logger.warn('[VideoPlayer] No suitable fallback audio track found');
// Increment attempts even if no fallback found to prevent infinite checking
setAudioTrackFallbackAttempts(prev => prev + 1);
}
}
}
};
const onLoad = (data: any) => {
@ -960,13 +998,132 @@ const VideoPlayer: React.FC = () => {
}
if (data.audioTracks && data.audioTracks.length > 0) {
// Enhanced debug logging to see all available fields
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Raw audio tracks data:`, data.audioTracks);
data.audioTracks.forEach((track: any, idx: number) => {
logger.log(`[VideoPlayer] Track ${idx} raw data:`, {
index: track.index,
title: track.title,
language: track.language,
type: track.type,
channels: track.channels,
bitrate: track.bitrate,
codec: track.codec,
sampleRate: track.sampleRate,
name: track.name,
label: track.label,
allKeys: Object.keys(track),
fullTrackObject: track
});
});
}
const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => {
const trackIndex = track.index !== undefined ? track.index : index;
const trackName = track.title || track.language || `Audio ${index + 1}`;
const trackLanguage = track.language || 'Unknown';
// Build comprehensive track name from available fields
let trackName = '';
const parts = [];
// Add language if available (try multiple possible fields)
let language = track.language || track.lang || track.languageCode;
// If no language field, try to extract from track name (e.g., "[Russian]", "[English]")
if ((!language || language === 'Unknown' || language === 'und' || language === '') && track.name) {
const languageMatch = track.name.match(/\[([^\]]+)\]/);
if (languageMatch && languageMatch[1]) {
language = languageMatch[1].trim();
}
}
if (language && language !== 'Unknown' && language !== 'und' && language !== '') {
parts.push(language.toUpperCase());
}
// Add codec information if available (try multiple possible fields)
const codec = track.type || track.codec || track.format;
if (codec && codec !== 'Unknown') {
parts.push(codec.toUpperCase());
}
// Add channel information if available
const channels = track.channels || track.channelCount;
if (channels && channels > 0) {
if (channels === 1) {
parts.push('MONO');
} else if (channels === 2) {
parts.push('STEREO');
} else if (channels === 6) {
parts.push('5.1CH');
} else if (channels === 8) {
parts.push('7.1CH');
} else {
parts.push(`${channels}CH`);
}
}
// Add bitrate if available
const bitrate = track.bitrate || track.bitRate;
if (bitrate && bitrate > 0) {
parts.push(`${Math.round(bitrate / 1000)}kbps`);
}
// Add sample rate if available
const sampleRate = track.sampleRate || track.sample_rate;
if (sampleRate && sampleRate > 0) {
parts.push(`${Math.round(sampleRate / 1000)}kHz`);
}
// Add title if available and not generic
let title = track.title || track.name || track.label;
if (title && !title.match(/^(Audio|Track)\s*\d*$/i) && title !== 'Unknown') {
// Clean up title by removing language brackets and trailing punctuation
title = title.replace(/\s*\[[^\]]+\]\s*[-–—]*\s*$/, '').trim();
if (title && title !== 'Unknown') {
parts.push(title);
}
}
// Combine parts or fallback to generic name
if (parts.length > 0) {
trackName = parts.join(' • ');
} else {
// For simple track names like "Track 1", "Audio 1", etc., use them as-is
const simpleName = track.name || track.title || track.label;
if (simpleName && simpleName.match(/^(Track|Audio)\s*\d*$/i)) {
trackName = simpleName;
} else {
// Try to extract any meaningful info from the track object
const meaningfulFields: string[] = [];
Object.keys(track).forEach(key => {
const value = track[key];
if (value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1) {
meaningfulFields.push(`${key}: ${value}`);
}
});
if (meaningfulFields.length > 0) {
trackName = `Audio ${index + 1} (${meaningfulFields.slice(0, 2).join(', ')})`;
} else {
trackName = `Audio ${index + 1}`;
}
}
}
const trackLanguage = language || 'Unknown';
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Audio track ${index}: index=${trackIndex}, name="${trackName}", language="${trackLanguage}"`);
logger.log(`[VideoPlayer] Processed track ${index}:`, {
index: trackIndex,
name: trackName,
language: trackLanguage,
parts: parts,
meaningfulFields: Object.keys(track).filter(key => {
const value = track[key];
return value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1;
})
});
}
return {
@ -977,12 +1134,24 @@ const VideoPlayer: React.FC = () => {
});
setVlcAudioTracks(formattedAudioTracks);
// Auto-select the first audio track if none is selected
// Auto-select English audio track if available, otherwise first track
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
const firstTrack = formattedAudioTracks[0];
setSelectedAudioTrack(firstTrack.id);
// Look for English track first
const englishTrack = formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => {
const lang = (track.language || '').toLowerCase();
return lang === 'english' || lang === 'en' || lang === 'eng' ||
(track.name && track.name.toLowerCase().includes('english'));
});
const selectedTrack = englishTrack || formattedAudioTracks[0];
setSelectedAudioTrack(selectedTrack.id);
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Auto-selected first audio track: ${firstTrack.name} (ID: ${firstTrack.id})`);
if (englishTrack) {
logger.log(`[VideoPlayer] Auto-selected English audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`);
} else {
logger.log(`[VideoPlayer] No English track found, auto-selected first audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`);
}
}
}
@ -996,6 +1165,10 @@ const VideoPlayer: React.FC = () => {
setIsVideoLoaded(true);
setIsPlayerReady(true);
// Reset audio track fallback attempts when new video loads
setAudioTrackFallbackAttempts(0);
setLastAudioTrackCheck(0);
// Start Trakt watching session when video loads with proper duration
if (videoDuration > 0) {
@ -1196,36 +1369,104 @@ const VideoPlayer: React.FC = () => {
};
const handleError = (error: any) => {
logger.error('[VideoPlayer] Playback Error:', error);
// Format error details for user display
let errorMessage = 'An unknown error occurred';
if (error) {
if (typeof error === 'string') {
errorMessage = error;
} else if (error.message) {
errorMessage = error.message;
} else if (error.error && error.error.message) {
errorMessage = error.error.message;
} else if (error.code) {
errorMessage = `Error Code: ${error.code}`;
} else {
errorMessage = JSON.stringify(error, null, 2);
try {
logger.error('[VideoPlayer] Playback Error:', error);
// Check for audio codec errors (TrueHD, DTS, Dolby, etc.)
const isAudioCodecError =
(error?.message && /(trhd|truehd|true\s?hd|dts|dolby|atmos|e-ac3|ac3)/i.test(error.message)) ||
(error?.error?.message && /(trhd|truehd|true\s?hd|dts|dolby|atmos|e-ac3|ac3)/i.test(error.error.message)) ||
(error?.title && /codec not supported/i.test(error.title));
// Handle audio codec errors with automatic fallback
if (isAudioCodecError && vlcAudioTracks.length > 1) {
logger.warn('[VideoPlayer] Audio codec error detected, attempting audio track fallback');
// Find a fallback audio track (prefer stereo/standard formats)
const fallbackTrack = vlcAudioTracks.find((track, index) => {
const trackName = (track.name || '').toLowerCase();
const trackLang = (track.language || '').toLowerCase();
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
return !trackName.includes('truehd') &&
!trackName.includes('dts') &&
!trackName.includes('dolby') &&
!trackName.includes('atmos') &&
!trackName.includes('7.1') &&
!trackName.includes('5.1') &&
index !== selectedAudioTrack; // Don't select the same track
});
if (fallbackTrack) {
const fallbackIndex = vlcAudioTracks.indexOf(fallbackTrack);
logger.warn(`[VideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (index: ${fallbackIndex})`);
// Clear any existing error state
if (errorTimeoutRef.current) {
clearTimeout(errorTimeoutRef.current);
errorTimeoutRef.current = null;
}
setShowErrorModal(false);
// Switch to fallback audio track
setSelectedAudioTrack(fallbackIndex);
// Brief pause to allow track switching
setPaused(true);
setTimeout(() => {
if (isMounted.current) {
setPaused(false);
}
}, 500);
return; // Don't show error UI, attempt recovery
}
}
// Format error details for user display
let errorMessage = 'An unknown error occurred';
if (error) {
if (isAudioCodecError) {
errorMessage = 'Audio codec compatibility issue detected. The video contains unsupported audio codec (TrueHD/DTS/Dolby). Please try selecting a different audio track or use an alternative video source.';
} else if (typeof error === 'string') {
errorMessage = error;
} else if (error.message) {
errorMessage = error.message;
} else if (error.error && error.error.message) {
errorMessage = error.error.message;
} else if (error.code) {
errorMessage = `Error Code: ${error.code}`;
} else {
errorMessage = JSON.stringify(error, null, 2);
}
}
setErrorDetails(errorMessage);
setShowErrorModal(true);
// Clear any existing timeout
if (errorTimeoutRef.current) {
clearTimeout(errorTimeoutRef.current);
}
// Auto-exit after 5 seconds if user doesn't dismiss
errorTimeoutRef.current = setTimeout(() => {
handleErrorExit();
}, 5000);
} catch (handlerError) {
// Fallback error handling to prevent crashes during error processing
logger.error('[VideoPlayer] Error in error handler:', handlerError);
if (isMounted.current) {
// Minimal safe error handling
setErrorDetails('A critical error occurred');
setShowErrorModal(true);
// Force exit after 3 seconds if error handler itself fails
setTimeout(() => {
if (isMounted.current) {
handleClose();
}
}, 3000);
}
}
setErrorDetails(errorMessage);
setShowErrorModal(true);
// Clear any existing timeout
if (errorTimeoutRef.current) {
clearTimeout(errorTimeoutRef.current);
}
// Auto-exit after 5 seconds if user doesn't dismiss
errorTimeoutRef.current = setTimeout(() => {
handleErrorExit();
}, 5000);
};
const handleErrorExit = () => {

View file

@ -5,7 +5,7 @@ import { SubtitleCue } from './playerTypes';
// Debug flag - set back to false to disable verbose logging
// WARNING: Setting this to true currently causes infinite render loops
// Use selective logging instead if debugging is needed
export const DEBUG_MODE = false;
export const DEBUG_MODE = true;
// Safer debug function that won't cause render loops
// Call this with any debugging info you need instead of using inline DEBUG_MODE checks
@ -93,6 +93,14 @@ export const formatLanguage = (code?: string): string => {
export const getTrackDisplayName = (track: { name?: string, id: number, language?: string }): string => {
if (!track) return 'Unknown Track';
// If no name, use track number
if (!track.name) return `Track ${track.id}`;
// If the name is already well-formatted (contains • separators), use it as-is
if (track.name.includes('•')) {
return track.name;
}
// If we have a language field, use that for better display
if (track.language && track.language !== 'Unknown') {
const formattedLanguage = formatLanguage(track.language);
@ -101,9 +109,6 @@ export const getTrackDisplayName = (track: { name?: string, id: number, language
}
}
// If no name, use track number
if (!track.name) return `Track ${track.id}`;
// Try to extract language from name like "Some Info - [English]"
const languageMatch = track.name.match(/\[(.*?)\]/);
if (languageMatch && languageMatch[1]) {

View file

@ -114,32 +114,34 @@ const AISettingsScreen: React.FC = () => {
Linking.openURL('https://openrouter.ai/keys');
};
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
const headerHeight = headerBaseHeight + topSpacing;
return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
{/* Header */}
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
<View style={styles.headerContent}>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backButton}
>
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.text}
/>
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
AI Assistant
<View style={styles.header}>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backButton}
>
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.text}
/>
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
Settings
</Text>
</TouchableOpacity>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
AI Assistant
</Text>
<ScrollView
style={styles.scrollView}
@ -342,7 +344,7 @@ const AISettingsScreen: React.FC = () => {
<SvgXml xml={OPENROUTER_SVG.replace(/CURRENTCOLOR/g, currentTheme.colors.mediumEmphasis)} width={180} height={60} />
</View>
</ScrollView>
</View>
</SafeAreaView>
);
};
@ -351,33 +353,45 @@ const styles = StyleSheet.create({
flex: 1,
},
header: {
paddingHorizontal: Math.max(16, width * 0.05),
justifyContent: 'flex-end',
paddingBottom: 8,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
},
headerContent: {
backButton: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
backText: {
fontSize: 17,
marginLeft: 8,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
backButton: {
marginRight: 16,
headerButton: {
padding: 8,
marginLeft: 8,
},
headerTitle: {
fontSize: Math.min(28, width * 0.07),
fontWeight: '800',
letterSpacing: 0.3,
fontSize: 34,
fontWeight: 'bold',
paddingHorizontal: 20,
marginBottom: 24,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: Math.max(16, width * 0.05),
paddingBottom: 40,
},
infoCard: {
borderRadius: 16,
padding: 20,
marginHorizontal: 16,
marginBottom: 20,
},
infoHeader: {
@ -410,6 +424,7 @@ const styles = StyleSheet.create({
card: {
borderRadius: 16,
padding: 20,
marginHorizontal: 16,
marginBottom: 20,
},
cardTitle: {

View file

@ -264,11 +264,19 @@ const HomeScreenSettings: React.FC = () => {
size={24}
color={isDarkMode ? colors.highEmphasis : colors.textDark}
/>
<Text style={[styles.backText, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
Settings
</Text>
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
Home Screen Settings
</Text>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
Home Screen Settings
</Text>
{/* Saved indicator */}
<Animated.View
@ -426,18 +434,32 @@ const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 12 : 8,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
},
backButton: {
marginRight: 16,
padding: 4,
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
backText: {
fontSize: 17,
marginLeft: 8,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: 8,
marginLeft: 8,
},
headerTitle: {
fontSize: 22,
fontWeight: '700',
letterSpacing: 0.5,
fontSize: 34,
fontWeight: 'bold',
paddingHorizontal: 16,
marginBottom: 24,
},
scrollView: {
flex: 1,

View file

@ -80,18 +80,34 @@ const createStyles = (colors: any) => StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 16,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 16 : 16,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
backgroundColor: colors.darkBackground,
},
backButton: {
padding: 4,
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
backText: {
fontSize: 17,
marginLeft: 8,
color: colors.white,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: 8,
marginLeft: 8,
},
headerTitle: {
fontSize: 22,
fontWeight: '600',
marginLeft: 16,
fontSize: 34,
fontWeight: 'bold',
paddingHorizontal: 16,
marginBottom: 24,
color: colors.white,
},
headerRight: {
@ -685,10 +701,16 @@ const LogoSourceSettings = () => {
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
<Text style={styles.backText}>Settings</Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>Logo Source</Text>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={styles.headerTitle}>Logo Source</Text>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}

View file

@ -218,17 +218,26 @@ const NotificationSettingsScreen = () => {
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
<View style={[styles.header, { borderBottomColor: currentTheme.colors.border }]}>
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
Settings
</Text>
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>Notification Settings</Text>
<View style={{ width: 40 }} />
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
Notification Settings
</Text>
<ScrollView style={styles.content}>
<Animated.View
entering={FadeIn.duration(300)}
@ -455,16 +464,30 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 12 : 12,
borderBottomWidth: 1,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
backText: {
fontSize: 17,
marginLeft: 8,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: 8,
marginLeft: 8,
},
headerTitle: {
fontSize: 18,
fontSize: 34,
fontWeight: 'bold',
paddingHorizontal: 16,
marginBottom: 24,
},
content: {
flex: 1,

View file

@ -165,16 +165,19 @@ const PlayerSettingsScreen: React.FC = () => {
size={24}
color={currentTheme.colors.text}
/>
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
Settings
</Text>
</TouchableOpacity>
<Text
style={[
styles.headerTitle,
{ color: currentTheme.colors.text },
]}
>
Video Player
</Text>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
Video Player
</Text>
<ScrollView
style={styles.scrollView}
@ -327,18 +330,32 @@ const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
paddingBottom: 8,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
marginRight: 16,
borderRadius: 20,
},
backText: {
fontSize: 17,
marginLeft: 8,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: 8,
marginLeft: 8,
},
headerTitle: {
fontSize: 20,
fontWeight: '600',
fontSize: 34,
fontWeight: 'bold',
paddingHorizontal: 16,
marginBottom: 24,
},
scrollView: {
flex: 1,

View file

@ -36,15 +36,24 @@ const createStyles = (colors: any) => StyleSheet.create({
backgroundColor: colors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'ios' ? 44 : ANDROID_STATUSBAR_HEIGHT + 16,
paddingBottom: 16,
},
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: 8,
marginLeft: 8,
},
backText: {
fontSize: 17,
@ -472,16 +481,6 @@ const createStyles = (colors: any) => StyleSheet.create({
fontSize: 14,
fontWeight: '500',
},
helpButton: {
position: 'absolute',
top: Platform.OS === 'ios' ? 44 : ANDROID_STATUSBAR_HEIGHT + 16,
right: 16,
backgroundColor: 'transparent',
borderRadius: 20,
padding: 8,
borderWidth: 1,
borderColor: colors.elevation3,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
@ -1163,11 +1162,8 @@ const PluginsScreen: React.FC = () => {
return (
<View style={styles.container}>
<StatusBar
barStyle={Platform.OS === 'ios' ? 'light-content' : 'light-content'}
backgroundColor={colors.background}
/>
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
{/* Header */}
<View style={styles.header}>
@ -1179,13 +1175,15 @@ const PluginsScreen: React.FC = () => {
<Text style={styles.backText}>Settings</Text>
</TouchableOpacity>
{/* Help Button */}
<TouchableOpacity
style={styles.helpButton}
onPress={() => setShowHelpModal(true)}
>
<Ionicons name="help-circle-outline" size={20} color={colors.primary} />
</TouchableOpacity>
<View style={styles.headerActions}>
{/* Help Button */}
<TouchableOpacity
style={styles.headerButton}
onPress={() => setShowHelpModal(true)}
>
<Ionicons name="help-circle-outline" size={20} color={colors.primary} />
</TouchableOpacity>
</View>
</View>
<Text style={styles.headerTitle}>Plugins</Text>
@ -1769,7 +1767,7 @@ const PluginsScreen: React.FC = () => {
</View>
</View>
</Modal>
</View>
</SafeAreaView>
);
};

View file

@ -12,6 +12,7 @@ import {
Dimensions,
StatusBar,
FlatList,
SafeAreaView,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
@ -438,42 +439,51 @@ const ThemeScreen: React.FC = () => {
};
return (
<View style={[
<SafeAreaView style={[
styles.container,
{
backgroundColor: currentTheme.colors.darkBackground,
paddingTop: insets.top,
paddingBottom: insets.bottom,
}
]}>
<StatusBar barStyle="light-content" />
<ThemeColorEditor
initialColors={initialColors}
onSave={handleSaveTheme}
onCancel={handleCancelEdit}
/>
</View>
</SafeAreaView>
);
}
return (
<View style={[
<SafeAreaView style={[
styles.container,
{
backgroundColor: currentTheme.colors.darkBackground,
paddingTop: insets.top,
paddingBottom: insets.bottom,
}
]}>
<StatusBar barStyle="light-content" />
<View style={styles.header}>
<TouchableOpacity
style={[styles.backButton, styles.buttonShadow]}
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
Settings
</Text>
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>App Themes</Text>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
App Themes
</Text>
{/* Category filter */}
<View style={styles.filterContainer}>
<FlatList
@ -543,7 +553,7 @@ const ThemeScreen: React.FC = () => {
/>
</View>
</ScrollView>
</View>
</SafeAreaView>
);
};
@ -554,18 +564,32 @@ const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 8,
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight || 0 + 8 : 8,
},
backButton: {
padding: 6,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
backText: {
fontSize: 17,
marginLeft: 8,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: 8,
marginLeft: 8,
},
headerTitle: {
fontSize: 18,
fontSize: 34,
fontWeight: 'bold',
marginLeft: 12,
paddingHorizontal: 16,
marginBottom: 24,
},
content: {
flex: 1,

View file

@ -197,14 +197,19 @@ const TraktSettingsScreen: React.FC = () => {
size={24}
color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
/>
<Text style={[styles.backText, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
Settings
</Text>
</TouchableOpacity>
<Text style={[
styles.headerTitle,
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
]}>
Trakt Settings
</Text>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
Trakt Settings
</Text>
<ScrollView
style={styles.scrollView}
@ -427,17 +432,32 @@ const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 16,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
},
backButton: {
padding: 4,
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
backText: {
fontSize: 17,
marginLeft: 8,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: 8,
marginLeft: 8,
},
headerTitle: {
fontSize: 22,
fontWeight: '600',
marginLeft: 16,
fontSize: 34,
fontWeight: 'bold',
paddingHorizontal: 16,
marginBottom: 24,
},
scrollView: {
flex: 1,

View file

@ -274,30 +274,34 @@ const UpdateScreen: React.FC = () => {
}
};
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
const headerHeight = headerBaseHeight + topSpacing;
return (
<View style={[
<SafeAreaView style={[
styles.container,
{ backgroundColor: currentTheme.colors.darkBackground }
]}>
<StatusBar barStyle={'light-content'} />
<View style={{ flex: 1 }}>
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
activeOpacity={0.7}
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
App Updates
<StatusBar barStyle="light-content" />
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
activeOpacity={0.7}
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
<Text style={[styles.backText, { color: currentTheme.colors.highEmphasis }]}>
Settings
</Text>
<View style={styles.headerSpacer} />
</TouchableOpacity>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
App Updates
</Text>
<View style={styles.contentContainer}>
<ScrollView
@ -547,8 +551,7 @@ const UpdateScreen: React.FC = () => {
)}
</ScrollView>
</View>
</View>
</View>
</SafeAreaView>
);
};
@ -557,27 +560,34 @@ const styles = StyleSheet.create({
flex: 1,
},
header: {
paddingHorizontal: Math.max(12, width * 0.04),
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingBottom: 8,
backgroundColor: 'transparent',
zIndex: 2,
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
marginLeft: -8,
},
backText: {
fontSize: 17,
marginLeft: 8,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: 8,
marginLeft: 8,
},
headerTitle: {
fontSize: Math.min(24, width * 0.06),
fontWeight: '800',
letterSpacing: 0.3,
flex: 1,
textAlign: 'center',
},
headerSpacer: {
width: 40, // Same width as back button to center the title
fontSize: 34,
fontWeight: 'bold',
paddingHorizontal: 16,
marginBottom: 24,
},
contentContainer: {
flex: 1,

View file

@ -292,7 +292,7 @@ class SyncService {
migrations.push(moveKey('app_settings', `@user:${userId}:app_settings`));
} else if (k === '@user:local:app_settings') {
migrations.push(moveKey(k, `@user:${userId}:app_settings`));
} else if (k === '@user:local:stremio-addons') {
} else if (k === '@user:local:stremio-addons' || k === 'stremio-addons') {
migrations.push(moveKey(k, `@user:${userId}:stremio-addons`));
} else if (k === '@user:local:stremio-addon-order') {
migrations.push(moveKey(k, `@user:${userId}:stremio-addon-order`));

View file

@ -200,7 +200,10 @@ class StremioService {
try {
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
const storedAddons = await AsyncStorage.getItem(`@user:${scope}:${this.STORAGE_KEY}`);
// Prefer scoped storage, but fall back to legacy keys to preserve older installs
let storedAddons = await AsyncStorage.getItem(`@user:${scope}:${this.STORAGE_KEY}`);
if (!storedAddons) storedAddons = await AsyncStorage.getItem(this.STORAGE_KEY);
if (!storedAddons) storedAddons = await AsyncStorage.getItem(`@user:local:${this.STORAGE_KEY}`);
if (storedAddons) {
const parsed = JSON.parse(storedAddons);
@ -375,7 +378,11 @@ class StremioService {
try {
const addonsArray = Array.from(this.installedAddons.values());
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
await AsyncStorage.setItem(`@user:${scope}:${this.STORAGE_KEY}`, JSON.stringify(addonsArray));
// Write to both scoped and legacy keys for compatibility
await Promise.all([
AsyncStorage.setItem(`@user:${scope}:${this.STORAGE_KEY}`, JSON.stringify(addonsArray)),
AsyncStorage.setItem(this.STORAGE_KEY, JSON.stringify(addonsArray)),
]);
} catch (error) {
// Continue even if save fails
}