mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-26 02:52:53 +00:00
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 [currentQuality, setCurrentQuality] = useState<string | undefined>(quality);
|
||||||
const [currentStreamProvider, setCurrentStreamProvider] = useState<string | undefined>(streamProvider);
|
const [currentStreamProvider, setCurrentStreamProvider] = useState<string | undefined>(streamProvider);
|
||||||
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
||||||
|
const [lastAudioTrackCheck, setLastAudioTrackCheck] = useState<number>(0);
|
||||||
|
const [audioTrackFallbackAttempts, setAudioTrackFallbackAttempts] = useState<number>(0);
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||||
|
|
@ -961,6 +963,54 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const bufferedTime = data.playableDuration || currentTimeInSeconds;
|
const bufferedTime = data.playableDuration || currentTimeInSeconds;
|
||||||
safeSetState(() => setBuffered(bufferedTime));
|
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) => {
|
const onLoad = (data: any) => {
|
||||||
|
|
@ -1003,13 +1053,132 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
// Handle audio tracks
|
// Handle audio tracks
|
||||||
if (data.audioTracks && data.audioTracks.length > 0) {
|
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 formattedAudioTracks = data.audioTracks.map((track: any, index: number) => {
|
||||||
const trackIndex = track.index !== undefined ? track.index : index;
|
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) {
|
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 {
|
return {
|
||||||
|
|
@ -1020,12 +1189,24 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
});
|
});
|
||||||
setRnVideoAudioTracks(formattedAudioTracks);
|
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) {
|
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
|
||||||
const firstTrack = formattedAudioTracks[0];
|
// Look for English track first
|
||||||
setSelectedAudioTrack(firstTrack.id);
|
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) {
|
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);
|
setIsVideoLoaded(true);
|
||||||
setIsPlayerReady(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
|
// Start Trakt watching session when video loads with proper duration
|
||||||
if (videoDuration > 0) {
|
if (videoDuration > 0) {
|
||||||
traktAutosync.handlePlaybackStart(currentTime, videoDuration);
|
traktAutosync.handlePlaybackStart(currentTime, videoDuration);
|
||||||
|
|
@ -1245,27 +1430,33 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Dolby Digital Plus audio codec errors (ExoPlayer)
|
// Check for audio codec errors (TrueHD, DTS, Dolby, etc.)
|
||||||
const isDolbyCodecError = error?.error?.errorCode === '24001' ||
|
const isAudioCodecError =
|
||||||
|
error?.error?.errorCode === '24001' ||
|
||||||
error?.errorCode === '24001' ||
|
error?.errorCode === '24001' ||
|
||||||
(error?.error?.errorString &&
|
(error?.error?.errorString &&
|
||||||
error.error.errorString.includes('ERROR_CODE_DECODER_INIT_FAILED')) ||
|
error.error.errorString.includes('ERROR_CODE_DECODER_INIT_FAILED')) ||
|
||||||
(error?.error?.errorException &&
|
(error?.error?.errorException &&
|
||||||
error.error.errorException.includes('audio/eac3')) ||
|
error.error.errorException.includes('audio/eac3')) ||
|
||||||
(error?.error?.errorException &&
|
(error?.error?.errorException &&
|
||||||
error.error.errorException.includes('Dolby Digital Plus'));
|
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
|
// Handle audio codec errors with automatic fallback
|
||||||
if (isDolbyCodecError && rnVideoAudioTracks.length > 1) {
|
if (isAudioCodecError && rnVideoAudioTracks.length > 1) {
|
||||||
logger.warn('[AndroidVideoPlayer] Dolby Digital Plus codec error detected, attempting audio track fallback');
|
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 fallbackTrack = rnVideoAudioTracks.find((track, index) => {
|
||||||
const trackName = (track.name || '').toLowerCase();
|
const trackName = (track.name || '').toLowerCase();
|
||||||
const trackLang = (track.language || '').toLowerCase();
|
const trackLang = (track.language || '').toLowerCase();
|
||||||
// Prefer stereo, AAC, or standard audio formats
|
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
|
||||||
return !trackName.includes('dolby') &&
|
return !trackName.includes('truehd') &&
|
||||||
!trackName.includes('dts') &&
|
!trackName.includes('dts') &&
|
||||||
|
!trackName.includes('dolby') &&
|
||||||
|
!trackName.includes('atmos') &&
|
||||||
!trackName.includes('7.1') &&
|
!trackName.includes('7.1') &&
|
||||||
!trackName.includes('5.1') &&
|
!trackName.includes('5.1') &&
|
||||||
index !== selectedAudioTrack; // Don't select the same track
|
index !== selectedAudioTrack; // Don't select the same track
|
||||||
|
|
@ -1415,8 +1606,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
// Format error details for user display
|
// Format error details for user display
|
||||||
let errorMessage = 'An unknown error occurred';
|
let errorMessage = 'An unknown error occurred';
|
||||||
if (error) {
|
if (error) {
|
||||||
if (isDolbyCodecError) {
|
if (isAudioCodecError) {
|
||||||
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.';
|
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) {
|
} else if (isHeavyCodecDecoderError) {
|
||||||
errorMessage = 'Audio codec issue (DTS/TrueHD/Atmos). Switching to a stereo/standard audio track may help.';
|
errorMessage = 'Audio codec issue (DTS/TrueHD/Atmos). Switching to a stereo/standard audio track may help.';
|
||||||
} else if (isServerConfigError) {
|
} else if (isServerConfigError) {
|
||||||
|
|
|
||||||
|
|
@ -84,17 +84,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
// Use VideoPlayer (VLC) for:
|
// Use VideoPlayer (VLC) for:
|
||||||
// - MKV files on iOS (unless forceVlc is set)
|
// - MKV files on iOS (unless forceVlc is set)
|
||||||
const shouldUseAndroidPlayer = Platform.OS === 'android' || isXprimeStream || (Platform.OS === 'ios' && !isMkvFile && !forceVlc);
|
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) {
|
if (shouldUseAndroidPlayer) {
|
||||||
return <AndroidVideoPlayer />;
|
return <AndroidVideoPlayer />;
|
||||||
}
|
}
|
||||||
|
|
@ -230,7 +219,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
// Always decode URLs for VLC as it has trouble with encoded characters
|
// Always decode URLs for VLC as it has trouble with encoded characters
|
||||||
const decoded = decodeURIComponent(url);
|
const decoded = decodeURIComponent(url);
|
||||||
logger.log('[VideoPlayer] Decoded URL for VLC:', { original: url, decoded });
|
|
||||||
return decoded;
|
return decoded;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('[VideoPlayer] URL decoding failed, using original:', 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 [currentQuality, setCurrentQuality] = useState<string | undefined>(quality);
|
||||||
const [currentStreamProvider, setCurrentStreamProvider] = useState<string | undefined>(streamProvider);
|
const [currentStreamProvider, setCurrentStreamProvider] = useState<string | undefined>(streamProvider);
|
||||||
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
||||||
|
const [lastAudioTrackCheck, setLastAudioTrackCheck] = useState<number>(0);
|
||||||
|
const [audioTrackFallbackAttempts, setAudioTrackFallbackAttempts] = useState<number>(0);
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||||
|
|
@ -920,6 +910,54 @@ const VideoPlayer: React.FC = () => {
|
||||||
const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds;
|
const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds;
|
||||||
safeSetState(() => setBuffered(bufferedTime));
|
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) => {
|
const onLoad = (data: any) => {
|
||||||
|
|
@ -960,13 +998,132 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.audioTracks && data.audioTracks.length > 0) {
|
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 formattedAudioTracks = data.audioTracks.map((track: any, index: number) => {
|
||||||
const trackIndex = track.index !== undefined ? track.index : index;
|
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) {
|
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 {
|
return {
|
||||||
|
|
@ -977,12 +1134,24 @@ const VideoPlayer: React.FC = () => {
|
||||||
});
|
});
|
||||||
setVlcAudioTracks(formattedAudioTracks);
|
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) {
|
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
|
||||||
const firstTrack = formattedAudioTracks[0];
|
// Look for English track first
|
||||||
setSelectedAudioTrack(firstTrack.id);
|
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) {
|
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})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -997,6 +1166,10 @@ const VideoPlayer: React.FC = () => {
|
||||||
setIsVideoLoaded(true);
|
setIsVideoLoaded(true);
|
||||||
setIsPlayerReady(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
|
// Start Trakt watching session when video loads with proper duration
|
||||||
if (videoDuration > 0) {
|
if (videoDuration > 0) {
|
||||||
traktAutosync.handlePlaybackStart(currentTime, videoDuration);
|
traktAutosync.handlePlaybackStart(currentTime, videoDuration);
|
||||||
|
|
@ -1196,12 +1369,65 @@ const VideoPlayer: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (error: any) => {
|
const handleError = (error: any) => {
|
||||||
|
try {
|
||||||
logger.error('[VideoPlayer] Playback Error:', error);
|
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
|
// Format error details for user display
|
||||||
let errorMessage = 'An unknown error occurred';
|
let errorMessage = 'An unknown error occurred';
|
||||||
if (error) {
|
if (error) {
|
||||||
if (typeof error === 'string') {
|
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;
|
errorMessage = error;
|
||||||
} else if (error.message) {
|
} else if (error.message) {
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
|
|
@ -1226,6 +1452,21 @@ const VideoPlayer: React.FC = () => {
|
||||||
errorTimeoutRef.current = setTimeout(() => {
|
errorTimeoutRef.current = setTimeout(() => {
|
||||||
handleErrorExit();
|
handleErrorExit();
|
||||||
}, 5000);
|
}, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleErrorExit = () => {
|
const handleErrorExit = () => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { SubtitleCue } from './playerTypes';
|
||||||
// Debug flag - set back to false to disable verbose logging
|
// Debug flag - set back to false to disable verbose logging
|
||||||
// WARNING: Setting this to true currently causes infinite render loops
|
// WARNING: Setting this to true currently causes infinite render loops
|
||||||
// Use selective logging instead if debugging is needed
|
// 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
|
// Safer debug function that won't cause render loops
|
||||||
// Call this with any debugging info you need instead of using inline DEBUG_MODE checks
|
// 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 => {
|
export const getTrackDisplayName = (track: { name?: string, id: number, language?: string }): string => {
|
||||||
if (!track) return 'Unknown Track';
|
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 we have a language field, use that for better display
|
||||||
if (track.language && track.language !== 'Unknown') {
|
if (track.language && track.language !== 'Unknown') {
|
||||||
const formattedLanguage = formatLanguage(track.language);
|
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]"
|
// Try to extract language from name like "Some Info - [English]"
|
||||||
const languageMatch = track.name.match(/\[(.*?)\]/);
|
const languageMatch = track.name.match(/\[(.*?)\]/);
|
||||||
if (languageMatch && languageMatch[1]) {
|
if (languageMatch && languageMatch[1]) {
|
||||||
|
|
|
||||||
|
|
@ -114,17 +114,12 @@ const AISettingsScreen: React.FC = () => {
|
||||||
Linking.openURL('https://openrouter.ai/keys');
|
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 (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
<View style={styles.header}>
|
||||||
<View style={styles.headerContent}>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
|
|
@ -134,12 +129,19 @@ const AISettingsScreen: React.FC = () => {
|
||||||
size={24}
|
size={24}
|
||||||
color={currentTheme.colors.text}
|
color={currentTheme.colors.text}
|
||||||
/>
|
/>
|
||||||
|
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.headerActions}>
|
||||||
|
{/* Empty for now, but ready for future actions */}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||||
AI Assistant
|
AI Assistant
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.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} />
|
<SvgXml xml={OPENROUTER_SVG.replace(/CURRENTCOLOR/g, currentTheme.colors.mediumEmphasis)} width={180} height={60} />
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -351,33 +353,45 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
paddingHorizontal: Math.max(16, width * 0.05),
|
flexDirection: 'row',
|
||||||
justifyContent: 'flex-end',
|
alignItems: 'center',
|
||||||
paddingBottom: 8,
|
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',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
backButton: {
|
headerButton: {
|
||||||
marginRight: 16,
|
|
||||||
padding: 8,
|
padding: 8,
|
||||||
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: Math.min(28, width * 0.07),
|
fontSize: 34,
|
||||||
fontWeight: '800',
|
fontWeight: 'bold',
|
||||||
letterSpacing: 0.3,
|
paddingHorizontal: 20,
|
||||||
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
scrollView: {
|
scrollView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
padding: Math.max(16, width * 0.05),
|
|
||||||
paddingBottom: 40,
|
paddingBottom: 40,
|
||||||
},
|
},
|
||||||
infoCard: {
|
infoCard: {
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
|
marginHorizontal: 16,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
infoHeader: {
|
infoHeader: {
|
||||||
|
|
@ -410,6 +424,7 @@ const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
|
marginHorizontal: 16,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
cardTitle: {
|
cardTitle: {
|
||||||
|
|
|
||||||
|
|
@ -264,11 +264,19 @@ const HomeScreenSettings: React.FC = () => {
|
||||||
size={24}
|
size={24}
|
||||||
color={isDarkMode ? colors.highEmphasis : colors.textDark}
|
color={isDarkMode ? colors.highEmphasis : colors.textDark}
|
||||||
/>
|
/>
|
||||||
|
<Text style={[styles.backText, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.headerActions}>
|
||||||
|
{/* Empty for now, but ready for future actions */}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
|
||||||
Home Screen Settings
|
Home Screen Settings
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Saved indicator */}
|
{/* Saved indicator */}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
|
@ -426,18 +434,32 @@ const styles = StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
|
||||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 12 : 8,
|
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
marginRight: 16,
|
flexDirection: 'row',
|
||||||
padding: 4,
|
alignItems: 'center',
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
backText: {
|
||||||
|
fontSize: 17,
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
headerActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
headerButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 22,
|
fontSize: 34,
|
||||||
fontWeight: '700',
|
fontWeight: 'bold',
|
||||||
letterSpacing: 0.5,
|
paddingHorizontal: 16,
|
||||||
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
scrollView: {
|
scrollView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
||||||
|
|
@ -80,18 +80,34 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 16,
|
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
|
||||||
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 16 : 16,
|
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: colors.darkBackground,
|
||||||
},
|
},
|
||||||
backButton: {
|
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: {
|
headerTitle: {
|
||||||
fontSize: 22,
|
fontSize: 34,
|
||||||
fontWeight: '600',
|
fontWeight: 'bold',
|
||||||
marginLeft: 16,
|
paddingHorizontal: 16,
|
||||||
|
marginBottom: 24,
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
},
|
},
|
||||||
headerRight: {
|
headerRight: {
|
||||||
|
|
@ -685,9 +701,15 @@ const LogoSourceSettings = () => {
|
||||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
||||||
|
<Text style={styles.backText}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>Logo Source</Text>
|
|
||||||
|
<View style={styles.headerActions}>
|
||||||
|
{/* Empty for now, but ready for future actions */}
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.headerTitle}>Logo Source</Text>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
|
|
|
||||||
|
|
@ -218,16 +218,25 @@ const NotificationSettingsScreen = () => {
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
||||||
<View style={[styles.header, { borderBottomColor: currentTheme.colors.border }]}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
|
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
|
||||||
|
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</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>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||||
|
Notification Settings
|
||||||
|
</Text>
|
||||||
|
|
||||||
<ScrollView style={styles.content}>
|
<ScrollView style={styles.content}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
|
@ -455,16 +464,30 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
|
||||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 12 : 12,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
},
|
},
|
||||||
|
backText: {
|
||||||
|
fontSize: 17,
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
headerActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
headerButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 18,
|
fontSize: 34,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
||||||
|
|
@ -165,16 +165,19 @@ const PlayerSettingsScreen: React.FC = () => {
|
||||||
size={24}
|
size={24}
|
||||||
color={currentTheme.colors.text}
|
color={currentTheme.colors.text}
|
||||||
/>
|
/>
|
||||||
|
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text
|
|
||||||
style={[
|
<View style={styles.headerActions}>
|
||||||
styles.headerTitle,
|
{/* Empty for now, but ready for future actions */}
|
||||||
{ color: currentTheme.colors.text },
|
</View>
|
||||||
]}
|
</View>
|
||||||
>
|
|
||||||
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||||
Video Player
|
Video Player
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
|
|
@ -327,18 +330,32 @@ const styles = StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
|
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
|
||||||
paddingBottom: 8,
|
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
marginRight: 16,
|
},
|
||||||
borderRadius: 20,
|
backText: {
|
||||||
|
fontSize: 17,
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
headerActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
headerButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 20,
|
fontSize: 34,
|
||||||
fontWeight: '600',
|
fontWeight: 'bold',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
scrollView: {
|
scrollView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,22 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingTop: Platform.OS === 'ios' ? 44 : ANDROID_STATUSBAR_HEIGHT + 16,
|
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
|
||||||
paddingBottom: 16,
|
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
headerActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
headerButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
backText: {
|
backText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
|
|
@ -472,16 +481,6 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
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: {
|
modalOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
|
@ -1163,11 +1162,8 @@ const PluginsScreen: React.FC = () => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar
|
<StatusBar barStyle="light-content" />
|
||||||
barStyle={Platform.OS === 'ios' ? 'light-content' : 'light-content'}
|
|
||||||
backgroundColor={colors.background}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
|
|
@ -1179,14 +1175,16 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.backText}>Settings</Text>
|
<Text style={styles.backText}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.headerActions}>
|
||||||
{/* Help Button */}
|
{/* Help Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.helpButton}
|
style={styles.headerButton}
|
||||||
onPress={() => setShowHelpModal(true)}
|
onPress={() => setShowHelpModal(true)}
|
||||||
>
|
>
|
||||||
<Ionicons name="help-circle-outline" size={20} color={colors.primary} />
|
<Ionicons name="help-circle-outline" size={20} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={styles.headerTitle}>Plugins</Text>
|
<Text style={styles.headerTitle}>Plugins</Text>
|
||||||
|
|
||||||
|
|
@ -1769,7 +1767,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
</View>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
FlatList,
|
FlatList,
|
||||||
|
SafeAreaView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
|
|
@ -438,41 +439,50 @@ const ThemeScreen: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[
|
<SafeAreaView style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
{
|
{
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
paddingTop: insets.top,
|
|
||||||
paddingBottom: insets.bottom,
|
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
|
<StatusBar barStyle="light-content" />
|
||||||
<ThemeColorEditor
|
<ThemeColorEditor
|
||||||
initialColors={initialColors}
|
initialColors={initialColors}
|
||||||
onSave={handleSaveTheme}
|
onSave={handleSaveTheme}
|
||||||
onCancel={handleCancelEdit}
|
onCancel={handleCancelEdit}
|
||||||
/>
|
/>
|
||||||
</View>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[
|
<SafeAreaView style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
{
|
{
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
paddingTop: insets.top,
|
|
||||||
paddingBottom: insets.bottom,
|
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.backButton, styles.buttonShadow]}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
|
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
|
||||||
|
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</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>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||||
|
App Themes
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* Category filter */}
|
{/* Category filter */}
|
||||||
<View style={styles.filterContainer}>
|
<View style={styles.filterContainer}>
|
||||||
|
|
@ -543,7 +553,7 @@ const ThemeScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -554,18 +564,32 @@ const styles = StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 12,
|
justifyContent: 'space-between',
|
||||||
paddingVertical: 8,
|
paddingHorizontal: 16,
|
||||||
|
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight || 0 + 8 : 8,
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
padding: 6,
|
flexDirection: 'row',
|
||||||
borderRadius: 20,
|
alignItems: 'center',
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
padding: 8,
|
||||||
|
},
|
||||||
|
backText: {
|
||||||
|
fontSize: 17,
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
headerActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
headerButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 18,
|
fontSize: 34,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginLeft: 12,
|
paddingHorizontal: 16,
|
||||||
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
||||||
|
|
@ -197,14 +197,19 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
size={24}
|
size={24}
|
||||||
color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
|
color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
|
||||||
/>
|
/>
|
||||||
|
<Text style={[styles.backText, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={[
|
|
||||||
styles.headerTitle,
|
<View style={styles.headerActions}>
|
||||||
{ color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark}
|
{/* Empty for now, but ready for future actions */}
|
||||||
]}>
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={[styles.headerTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
|
||||||
Trakt Settings
|
Trakt Settings
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
|
|
@ -427,17 +432,32 @@ const styles = StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 16,
|
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
|
||||||
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
|
|
||||||
},
|
},
|
||||||
backButton: {
|
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: {
|
headerTitle: {
|
||||||
fontSize: 22,
|
fontSize: 34,
|
||||||
fontWeight: '600',
|
fontWeight: 'bold',
|
||||||
marginLeft: 16,
|
paddingHorizontal: 16,
|
||||||
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
scrollView: {
|
scrollView: {
|
||||||
flex: 1,
|
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 (
|
return (
|
||||||
<View style={[
|
<SafeAreaView style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
{ backgroundColor: currentTheme.colors.darkBackground }
|
{ backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]}>
|
]}>
|
||||||
<StatusBar barStyle={'light-content'} />
|
<StatusBar barStyle="light-content" />
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
|
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
|
||||||
|
<Text style={[styles.backText, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.headerActions}>
|
||||||
|
{/* Empty for now, but ready for future actions */}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||||
App Updates
|
App Updates
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.headerSpacer} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.contentContainer}>
|
<View style={styles.contentContainer}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|
@ -547,8 +551,7 @@ const UpdateScreen: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</SafeAreaView>
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -557,27 +560,34 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
paddingHorizontal: Math.max(12, width * 0.04),
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingBottom: 8,
|
paddingHorizontal: 16,
|
||||||
backgroundColor: 'transparent',
|
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
|
||||||
zIndex: 2,
|
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
marginLeft: -8,
|
},
|
||||||
|
backText: {
|
||||||
|
fontSize: 17,
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
headerActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
headerButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: Math.min(24, width * 0.06),
|
fontSize: 34,
|
||||||
fontWeight: '800',
|
fontWeight: 'bold',
|
||||||
letterSpacing: 0.3,
|
paddingHorizontal: 16,
|
||||||
flex: 1,
|
marginBottom: 24,
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
headerSpacer: {
|
|
||||||
width: 40, // Same width as back button to center the title
|
|
||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ class SyncService {
|
||||||
migrations.push(moveKey('app_settings', `@user:${userId}:app_settings`));
|
migrations.push(moveKey('app_settings', `@user:${userId}:app_settings`));
|
||||||
} else if (k === '@user:local:app_settings') {
|
} else if (k === '@user:local:app_settings') {
|
||||||
migrations.push(moveKey(k, `@user:${userId}: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`));
|
migrations.push(moveKey(k, `@user:${userId}:stremio-addons`));
|
||||||
} else if (k === '@user:local:stremio-addon-order') {
|
} else if (k === '@user:local:stremio-addon-order') {
|
||||||
migrations.push(moveKey(k, `@user:${userId}:stremio-addon-order`));
|
migrations.push(moveKey(k, `@user:${userId}:stremio-addon-order`));
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,10 @@ class StremioService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
|
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) {
|
if (storedAddons) {
|
||||||
const parsed = JSON.parse(storedAddons);
|
const parsed = JSON.parse(storedAddons);
|
||||||
|
|
@ -375,7 +378,11 @@ class StremioService {
|
||||||
try {
|
try {
|
||||||
const addonsArray = Array.from(this.installedAddons.values());
|
const addonsArray = Array.from(this.installedAddons.values());
|
||||||
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
|
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) {
|
} catch (error) {
|
||||||
// Continue even if save fails
|
// Continue even if save fails
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue