some videoplayer enhancemnets
This commit is contained in:
parent
680a1b1ea6
commit
3c839c5ea1
14 changed files with 831 additions and 236 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue