consistent theming across player modals

This commit is contained in:
tapframe 2025-12-20 20:59:19 +05:30
parent a8dfe30546
commit 79b0cfc990
6 changed files with 545 additions and 790 deletions

View file

@ -1,14 +1,13 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity, ScrollView, useWindowDimensions, StyleSheet, Platform } from 'react-native'; import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Platform, useWindowDimensions } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
FadeOut, FadeOut,
SlideInDown, SlideInRight,
SlideOutDown, SlideOutRight,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils'; import { getTrackDisplayName } from '../utils/playerUtils';
import { logger } from '../../../utils/logger';
interface AudioTrackModalProps { interface AudioTrackModalProps {
showAudioModal: boolean; showAudioModal: boolean;
@ -25,53 +24,42 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
selectedAudioTrack, selectedAudioTrack,
selectAudioTrack, selectAudioTrack,
}) => { }) => {
const { width, height } = useWindowDimensions(); const { width } = useWindowDimensions();
const MENU_WIDTH = Math.min(width * 0.85, 400);
const menuWidth = Math.min(width * 0.9, 420);
const menuMaxHeight = height * 0.9;
const handleClose = () => setShowAudioModal(false); const handleClose = () => setShowAudioModal(false);
if (!showAudioModal) return null; if (!showAudioModal) return null;
return ( return (
<View style={StyleSheet.absoluteFill} zIndex={9999}> <View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
{/* Backdrop matching SubtitleModal */} <TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={handleClose}>
<TouchableOpacity <Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
style={StyleSheet.absoluteFill}
activeOpacity={1}
onPress={handleClose}
>
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(150)}
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }}
/>
</TouchableOpacity> </TouchableOpacity>
{/* Center Alignment Container */}
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} pointerEvents="box-none">
<Animated.View <Animated.View
entering={SlideInDown.duration(300)} entering={SlideInRight.duration(300)}
exiting={SlideOutDown.duration(250)} exiting={SlideOutRight.duration(250)}
style={{ style={{
width: menuWidth, position: 'absolute',
maxHeight: menuMaxHeight, top: 0,
backgroundColor: 'rgba(15, 15, 15, 0.98)', // Matches SubtitleModal right: 0,
borderRadius: 24, bottom: 0,
borderWidth: 1, width: MENU_WIDTH,
backgroundColor: '#0f0f0f',
borderLeftWidth: 1,
borderColor: 'rgba(255,255,255,0.1)', borderColor: 'rgba(255,255,255,0.1)',
overflow: 'hidden'
}} }}
> >
{/* Header with shared aesthetics */} <View style={{ paddingTop: Platform.OS === 'ios' ? 60 : 15, paddingHorizontal: 20 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', padding: 20, position: 'relative' }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
<Text style={{ color: 'white', fontSize: 18, fontWeight: '700' }}>Audio Tracks</Text> <Text style={{ color: 'white', fontSize: 22, fontWeight: '700' }}>Audio Tracks</Text>
</View>
</View> </View>
<ScrollView <ScrollView
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 20, paddingBottom: 20 }} contentContainerStyle={{ padding: 15, paddingBottom: 40 }}
> >
<View style={{ gap: 8 }}> <View style={{ gap: 8 }}>
{ksAudioTracks.map((track) => { {ksAudioTracks.map((track) => {
@ -85,9 +73,12 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
setTimeout(handleClose, 200); setTimeout(handleClose, 200);
}} }}
style={{ style={{
padding: 10, paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 12, borderRadius: 12,
backgroundColor: isSelected ? 'white' : 'rgba(255,255,255,0.05)', // Matches SubtitleModal item colors backgroundColor: isSelected ? 'white' : 'rgba(255,255,255,0.06)',
borderWidth: 1,
borderColor: isSelected ? 'white' : 'rgba(255,255,255,0.1)',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center' alignItems: 'center'
@ -96,7 +87,7 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={{ <Text style={{
color: isSelected ? 'black' : 'white', color: isSelected ? 'black' : 'white',
fontWeight: isSelected ? '700' : '400', fontWeight: isSelected ? '700' : '500',
fontSize: 15 fontSize: 15
}}> }}>
{getTrackDisplayName(track)} {getTrackDisplayName(track)}
@ -117,6 +108,5 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
</ScrollView> </ScrollView>
</Animated.View> </Animated.View>
</View> </View>
</View>
); );
}; };

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
@ -20,9 +20,6 @@ interface EpisodeStreamsModalProps {
metadata?: { id?: string; name?: string }; metadata?: { id?: string; name?: string };
} }
const { width } = Dimensions.get('window');
const MENU_WIDTH = Math.min(width * 0.85, 400);
const QualityBadge = ({ quality }: { quality: string | null }) => { const QualityBadge = ({ quality }: { quality: string | null }) => {
if (!quality) return null; if (!quality) return null;
@ -73,6 +70,9 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
onSelectStream, onSelectStream,
metadata, metadata,
}) => { }) => {
const { width } = useWindowDimensions();
const MENU_WIDTH = Math.min(width * 0.85, 400);
const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: Stream[]; addonName: string } }>({}); const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: Stream[]; addonName: string } }>({});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [hasErrors, setHasErrors] = useState<string[]>([]); const [hasErrors, setHasErrors] = useState<string[]>([]);
@ -117,7 +117,6 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
logger.warn(`[EpisodeStreamsModal] Error from ${addonName || addonId}:`, error); logger.warn(`[EpisodeStreamsModal] Error from ${addonName || addonId}:`, error);
setHasErrors(prev => [...prev, `${addonName || addonId}: ${error.message || 'Unknown error'}`]); setHasErrors(prev => [...prev, `${addonName || addonId}: ${error.message || 'Unknown error'}`]);
} else if (streams && streams.length > 0) { } else if (streams && streams.length > 0) {
// Update state incrementally for each provider
setAvailableStreams(prev => ({ setAvailableStreams(prev => ({
...prev, ...prev,
[addonId]: { [addonId]: {
@ -158,38 +157,16 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
return match ? match[1] : null; return match ? match[1] : null;
}; };
const handleClose = () => {
onClose();
};
if (!visible) return null; if (!visible) return null;
const sortedProviders = Object.entries(availableStreams); const sortedProviders = Object.entries(availableStreams);
return ( return (
<> <View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
{/* Backdrop */} <TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={onClose}>
<Animated.View <Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
entering={FadeIn.duration(200)} </TouchableOpacity>
exiting={FadeOut.duration(150)}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 9998,
}}
>
<TouchableOpacity
style={{ flex: 1 }}
onPress={handleClose}
activeOpacity={1}
/>
</Animated.View>
{/* Side Menu */}
<Animated.View <Animated.View
entering={SlideInRight.duration(300)} entering={SlideInRight.duration(300)}
exiting={SlideOutRight.duration(250)} exiting={SlideOutRight.duration(250)}
@ -199,66 +176,29 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
right: 0, right: 0,
bottom: 0, bottom: 0,
width: MENU_WIDTH, width: MENU_WIDTH,
backgroundColor: '#1A1A1A', backgroundColor: '#0f0f0f',
zIndex: 9999, borderLeftWidth: 1,
elevation: 20, borderColor: 'rgba(255,255,255,0.1)',
shadowColor: '#000',
shadowOffset: { width: -5, height: 0 },
shadowOpacity: 0.3,
shadowRadius: 10,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
}} }}
> >
{/* Header */} <View style={{ paddingTop: Platform.OS === 'ios' ? 60 : 15, paddingHorizontal: 20 }}>
<View style={{ <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 60,
paddingBottom: 20,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.08)',
}}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={{ <Text style={{ color: 'white', fontSize: 22, fontWeight: '700' }} numberOfLines={1}>
color: '#FFFFFF',
fontSize: 18,
fontWeight: '700',
}}>
{episode?.name || 'Select Stream'} {episode?.name || 'Select Stream'}
</Text> </Text>
{episode && ( {episode && (
<Text style={{ <Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, marginTop: 4 }}>
color: 'rgba(255, 255, 255, 0.6)',
fontSize: 12,
marginTop: 4,
}}>
S{episode.season_number}E{episode.episode_number} S{episode.season_number}E{episode.episode_number}
</Text> </Text>
)} )}
</View> </View>
<TouchableOpacity </View>
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
justifyContent: 'center',
alignItems: 'center',
}}
onPress={handleClose}
activeOpacity={0.7}
>
<MaterialIcons name="close" size={20} color="#FFFFFF" />
</TouchableOpacity>
</View> </View>
<ScrollView <ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={{ padding: 15, paddingBottom: 40 }}
> >
{isLoading && ( {isLoading && (
<View style={{ <View style={{
@ -301,9 +241,9 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
<TouchableOpacity <TouchableOpacity
key={`${providerId}-${index}`} key={`${providerId}-${index}`}
style={{ style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)', backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 16, borderRadius: 12,
padding: 16, padding: 12,
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)', borderColor: 'rgba(255,255,255,0.1)',
}} }}
@ -319,7 +259,7 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
gap: 8, gap: 8,
}}> }}>
<Text style={{ <Text style={{
color: '#FFFFFF', color: 'white',
fontSize: 15, fontSize: 15,
fontWeight: '500', fontWeight: '500',
flex: 1, flex: 1,
@ -333,9 +273,9 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
{stream.size && ( {stream.size && (
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="storage" size={14} color="rgba(107, 114, 128, 0.8)" /> <MaterialIcons name="storage" size={14} color="rgba(255,255,255,0.5)" />
<Text style={{ <Text style={{
color: 'rgba(107, 114, 128, 0.8)', color: 'rgba(255,255,255,0.5)',
fontSize: 12, fontSize: 12,
fontWeight: '600', fontWeight: '600',
marginLeft: 4, marginLeft: 4,
@ -361,10 +301,7 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
)} )}
</View> </View>
<View style={{ <View style={{ marginLeft: 12, alignItems: 'center' }}>
marginLeft: 12,
alignItems: 'center',
}}>
<MaterialIcons name="play-arrow" size={20} color="rgba(255,255,255,0.4)" /> <MaterialIcons name="play-arrow" size={20} color="rgba(255,255,255,0.4)" />
</View> </View>
</View> </View>
@ -434,7 +371,6 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
)} )}
</ScrollView> </ScrollView>
</Animated.View> </Animated.View>
</> </View>
); );
}; };

View file

@ -55,11 +55,21 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
if (showEpisodesModal && metadata?.id) { if (showEpisodesModal && metadata?.id) {
setIsLoadingProgress(true); setIsLoadingProgress(true);
try { try {
const progress = await storageService.getShowProgress(metadata.id); const allProgress = await storageService.getAllWatchProgress();
setEpisodeProgress(progress || {}); const progress: { [key: string]: any } = {};
// Filter progress for current show's episodes
Object.entries(allProgress).forEach(([key, value]) => {
if (key.includes(metadata.id!)) {
progress[key] = value;
}
});
setEpisodeProgress(progress);
// Trakt sync logic preserved // Trakt sync logic preserved
if (await TraktService.isAuthenticated()) { const traktService = TraktService.getInstance();
if (await traktService.isAuthenticated()) {
// Optional: background sync logic // Optional: background sync logic
} }
} catch (err) { } catch (err) {
@ -84,7 +94,7 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || []; const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || [];
return ( return (
<View style={StyleSheet.absoluteFill} zIndex={9999}> <View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
<TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={() => setShowEpisodesModal(false)}> <TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={() => setShowEpisodesModal(false)}>
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} /> <Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
</TouchableOpacity> </TouchableOpacity>

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
@ -18,9 +18,6 @@ interface SourcesModalProps {
isChangingSource?: boolean; isChangingSource?: boolean;
} }
const { width } = Dimensions.get('window');
const MENU_WIDTH = Math.min(width * 0.85, 400);
const QualityBadge = ({ quality }: { quality: string | null }) => { const QualityBadge = ({ quality }: { quality: string | null }) => {
if (!quality) return null; if (!quality) return null;
@ -72,6 +69,9 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
onSelectStream, onSelectStream,
isChangingSource = false, isChangingSource = false,
}) => { }) => {
const { width } = useWindowDimensions();
const MENU_WIDTH = Math.min(width * 0.85, 400);
const handleClose = () => { const handleClose = () => {
setShowSourcesModal(false); setShowSourcesModal(false);
}; };
@ -97,29 +97,11 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
}; };
return ( return (
<> <View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
{/* Backdrop */} <TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={handleClose}>
<Animated.View <Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
entering={FadeIn.duration(200)} </TouchableOpacity>
exiting={FadeOut.duration(150)}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 9998,
}}
>
<TouchableOpacity
style={{ flex: 1 }}
onPress={handleClose}
activeOpacity={1}
/>
</Animated.View>
{/* Side Menu */}
<Animated.View <Animated.View
entering={SlideInRight.duration(300)} entering={SlideInRight.duration(300)}
exiting={SlideOutRight.duration(250)} exiting={SlideOutRight.duration(250)}
@ -129,55 +111,20 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
right: 0, right: 0,
bottom: 0, bottom: 0,
width: MENU_WIDTH, width: MENU_WIDTH,
backgroundColor: '#1A1A1A', backgroundColor: '#0f0f0f',
zIndex: 9999, borderLeftWidth: 1,
elevation: 20, borderColor: 'rgba(255,255,255,0.1)',
shadowColor: '#000',
shadowOffset: { width: -5, height: 0 },
shadowOpacity: 0.3,
shadowRadius: 10,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
}} }}
> >
{/* Header */} <View style={{ paddingTop: Platform.OS === 'ios' ? 60 : 15, paddingHorizontal: 20 }}>
<View style={{ <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
flexDirection: 'row', <Text style={{ color: 'white', fontSize: 22, fontWeight: '700' }}>Change Source</Text>
alignItems: 'center', </View>
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 60,
paddingBottom: 20,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.08)',
}}>
<Text style={{
color: '#FFFFFF',
fontSize: 22,
fontWeight: '700',
}}>
Change Source
</Text>
<TouchableOpacity
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
justifyContent: 'center',
alignItems: 'center',
}}
onPress={handleClose}
activeOpacity={0.7}
>
<MaterialIcons name="close" size={20} color="#FFFFFF" />
</TouchableOpacity>
</View> </View>
<ScrollView <ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={{ padding: 15, paddingBottom: 40 }}
> >
{isChangingSource && ( {isChangingSource && (
<View style={{ <View style={{
@ -223,11 +170,11 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
<TouchableOpacity <TouchableOpacity
key={`${providerId}-${index}`} key={`${providerId}-${index}`}
style={{ style={{
backgroundColor: isSelected ? 'rgba(59, 130, 246, 0.15)' : 'rgba(255, 255, 255, 0.05)', backgroundColor: isSelected ? 'white' : 'rgba(255,255,255,0.06)',
borderRadius: 16, borderRadius: 12,
padding: 16, padding: 12,
borderWidth: 1, borderWidth: 1,
borderColor: isSelected ? 'rgba(59, 130, 246, 0.3)' : 'rgba(255, 255, 255, 0.1)', borderColor: isSelected ? 'white' : 'rgba(255,255,255,0.1)',
opacity: (isChangingSource && !isSelected) ? 0.6 : 1, opacity: (isChangingSource && !isSelected) ? 0.6 : 1,
}} }}
onPress={() => handleStreamSelect(stream)} onPress={() => handleStreamSelect(stream)}
@ -243,9 +190,9 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
gap: 8, gap: 8,
}}> }}>
<Text style={{ <Text style={{
color: '#FFFFFF', color: isSelected ? 'black' : 'white',
fontSize: 15, fontSize: 15,
fontWeight: '500', fontWeight: isSelected ? '700' : '500',
flex: 1, flex: 1,
}}> }}>
{stream.title || stream.name || `Stream ${index + 1}`} {stream.title || stream.name || `Stream ${index + 1}`}
@ -257,9 +204,9 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
{stream.size && ( {stream.size && (
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="storage" size={14} color="rgba(107, 114, 128, 0.8)" /> <MaterialIcons name="storage" size={14} color={isSelected ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.5)'} />
<Text style={{ <Text style={{
color: 'rgba(107, 114, 128, 0.8)', color: isSelected ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.5)',
fontSize: 12, fontSize: 12,
fontWeight: '600', fontWeight: '600',
marginLeft: 4, marginLeft: 4,
@ -270,9 +217,9 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
)} )}
{stream.lang && ( {stream.lang && (
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="language" size={14} color="rgba(59, 130, 246, 0.8)" /> <MaterialIcons name="language" size={14} color={isSelected ? 'rgba(0,0,0,0.6)' : 'rgba(59,130,246,0.8)'} />
<Text style={{ <Text style={{
color: 'rgba(59, 130, 246, 0.8)', color: isSelected ? 'rgba(0,0,0,0.6)' : 'rgba(59,130,246,0.8)',
fontSize: 12, fontSize: 12,
fontWeight: '600', fontWeight: '600',
marginLeft: 4, marginLeft: 4,
@ -285,12 +232,9 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
)} )}
</View> </View>
<View style={{ <View style={{ marginLeft: 12, alignItems: 'center' }}>
marginLeft: 12,
alignItems: 'center',
}}>
{isSelected ? ( {isSelected ? (
<MaterialIcons name="check" size={20} color="#3B82F6" /> <MaterialIcons name="check" size={18} color="black" />
) : ( ) : (
<MaterialIcons name="play-arrow" size={20} color="rgba(255,255,255,0.4)" /> <MaterialIcons name="play-arrow" size={20} color="rgba(255,255,255,0.4)" />
)} )}
@ -330,6 +274,6 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
)} )}
</ScrollView> </ScrollView>
</Animated.View> </Animated.View>
</> </View>
); );
}; };

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity, Platform, useWindowDimensions, ScrollView } from 'react-native'; import { View, Text, TouchableOpacity, Platform, useWindowDimensions, ScrollView, StyleSheet } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
@ -7,7 +7,6 @@ import Animated, {
SlideInRight, SlideInRight,
SlideOutRight, SlideOutRight,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface SpeedModalProps { interface SpeedModalProps {
showSpeedModal: boolean; showSpeedModal: boolean;
@ -30,20 +29,8 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
holdToSpeedValue, holdToSpeedValue,
setHoldToSpeedValue, setHoldToSpeedValue,
}) => { }) => {
const insets = useSafeAreaInsets(); const { width } = useWindowDimensions();
const { width, height } = useWindowDimensions(); const MENU_WIDTH = Math.min(width * 0.85, 400);
const isIos = Platform.OS === 'ios';
const isLandscape = width > height;
// Responsive tuning - more aggressive compacting
const isCompact = width < 360 || height < 640;
const sectionPad = isCompact ? 6 : 8;
const chipPadH = isCompact ? 4 : 6;
const chipPadV = isCompact ? 3 : 4;
const menuWidth = Math.min(
width * (isIos ? (isLandscape ? 0.55 : 0.8) : 0.85),
isIos ? 380 : 360
);
const speedPresets = [0.5, 1.0, 1.5, 2.0, 2.5]; const speedPresets = [0.5, 1.0, 1.5, 2.0, 2.5];
const holdSpeedOptions = [1.5, 2.0]; const holdSpeedOptions = [1.5, 2.0];
@ -60,33 +47,14 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
setHoldToSpeedValue(speed); setHoldToSpeedValue(speed);
}; };
const renderSpeedModal = () => {
if (!showSpeedModal) return null; if (!showSpeedModal) return null;
return ( return (
<> <View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
{/* Backdrop */} <TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={handleClose}>
<Animated.View <Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
entering={FadeIn.duration(200)} </TouchableOpacity>
exiting={FadeOut.duration(150)}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 9998,
}}
>
<TouchableOpacity
style={{ flex: 1 }}
onPress={handleClose}
activeOpacity={1}
/>
</Animated.View>
{/* Side Menu */}
<Animated.View <Animated.View
entering={SlideInRight.duration(300)} entering={SlideInRight.duration(300)}
exiting={SlideOutRight.duration(250)} exiting={SlideOutRight.duration(250)}
@ -95,65 +63,28 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
top: 0, top: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
width: menuWidth, width: MENU_WIDTH,
backgroundColor: '#1A1A1A', backgroundColor: '#0f0f0f',
zIndex: 9999, borderLeftWidth: 1,
elevation: 20, borderColor: 'rgba(255,255,255,0.1)',
shadowColor: '#000',
shadowOffset: { width: -5, height: 0 },
shadowOpacity: 0.3,
shadowRadius: 10,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
paddingRight: 0,
}} }}
> >
{/* Header */} <View style={{ paddingTop: Platform.OS === 'ios' ? 60 : 15, paddingHorizontal: 20 }}>
<View style={{ <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
flexDirection: 'row', <Text style={{ color: 'white', fontSize: 22, fontWeight: '700' }}>Playback Speed</Text>
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: insets.top + (isCompact ? 4 : 6),
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.08)',
}}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<MaterialIcons name="speed" size={20} color="#FFFFFF" />
<Text style={{ color: '#FFFFFF', fontSize: 18, fontWeight: '700' }}>Playback Speed</Text>
</View> </View>
<TouchableOpacity
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
justifyContent: 'center',
alignItems: 'center',
}}
onPress={handleClose}
activeOpacity={0.7}
>
<MaterialIcons name="close" size={20} color="#FFFFFF" />
</TouchableOpacity>
</View> </View>
<ScrollView <ScrollView
style={{ flex: 1 }}
contentContainerStyle={{
padding: 12,
paddingBottom: (isCompact ? 16 : 20) + (isIos ? insets.bottom : 0)
}}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={{ padding: 15, paddingBottom: 40 }}
> >
{/* Current Speed Display */} {/* Current Speed Display */}
<View style={{ <View style={{
backgroundColor: 'rgba(59, 130, 246, 0.15)', backgroundColor: 'rgba(59, 130, 246, 0.15)',
borderRadius: 12, borderRadius: 12,
padding: sectionPad * 0.75, padding: 12,
marginBottom: 12, marginBottom: 20,
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(59, 130, 246, 0.3)', borderColor: 'rgba(59, 130, 246, 0.3)',
}}> }}>
@ -161,7 +92,7 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
<MaterialIcons name="play-arrow" size={20} color="#3B82F6" /> <MaterialIcons name="play-arrow" size={20} color="#3B82F6" />
<Text style={{ <Text style={{
color: '#3B82F6', color: '#3B82F6',
fontSize: isCompact ? 16 : 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
marginLeft: 6 marginLeft: 6
}}> }}>
@ -171,19 +102,19 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
</View> </View>
{/* Speed Presets */} {/* Speed Presets */}
<View style={{ marginBottom: 16 }}> <View style={{ marginBottom: 20 }}>
<Text style={{ <Text style={{
color: 'rgba(255, 255, 255, 0.7)', color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
marginBottom: 10, marginBottom: 12,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.5, letterSpacing: 0.5,
}}> }}>
Speed Presets Speed Presets
</Text> </Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}> <View style={{ gap: 8 }}>
{speedPresets.map((speed) => { {speedPresets.map((speed) => {
const isSelected = currentSpeed === speed; const isSelected = currentSpeed === speed;
return ( return (
@ -191,24 +122,26 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
key={speed} key={speed}
onPress={() => handleSpeedSelect(speed)} onPress={() => handleSpeedSelect(speed)}
style={{ style={{
paddingHorizontal: chipPadH, paddingHorizontal: 16,
paddingVertical: chipPadV, paddingVertical: 12,
borderRadius: 12, borderRadius: 12,
backgroundColor: isSelected ? 'rgba(59, 130, 246, 0.15)' : 'rgba(255, 255, 255, 0.05)', backgroundColor: isSelected ? 'white' : 'rgba(255,255,255,0.06)',
borderWidth: 1, borderWidth: 1,
borderColor: isSelected ? 'rgba(59, 130, 246, 0.3)' : 'rgba(255, 255, 255, 0.1)', borderColor: isSelected ? 'white' : 'rgba(255,255,255,0.1)',
minWidth: 50, flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
}} }}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={{ <Text style={{
color: isSelected ? '#3B82F6' : '#FFFFFF', color: isSelected ? 'black' : 'white',
fontWeight: '600', fontWeight: isSelected ? '700' : '500',
fontSize: isCompact ? 11 : 12 fontSize: 15
}}> }}>
{speed}x {speed}x
</Text> </Text>
{isSelected && <MaterialIcons name="check" size={18} color="black" />}
</TouchableOpacity> </TouchableOpacity>
); );
})} })}
@ -216,22 +149,21 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
</View> </View>
{/* Hold-to-Speed Settings */} {/* Hold-to-Speed Settings */}
<View style={{ gap: isCompact ? 8 : 12 }}> <View style={{ backgroundColor: 'rgba(255,255,255,0.06)', borderRadius: 12, padding: 16, borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)' }}>
<View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 12, padding: sectionPad }}> <View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 16 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 10 }}> <MaterialIcons name="touch-app" size={18} color="rgba(255,255,255,0.7)" />
<MaterialIcons name="touch-app" size={14} color="rgba(255,255,255,0.7)" /> <Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14, marginLeft: 6, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 0.5 }}>
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 11, marginLeft: 4, fontWeight: '600' }}>
Hold-to-Speed Hold-to-Speed
</Text> </Text>
</View> </View>
{/* Enable Toggle */} {/* Enable Toggle */}
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<Text style={{ color: '#FFFFFF', fontWeight: '600', fontSize: 13 }}>Enable Hold Speed</Text> <Text style={{ color: 'white', fontWeight: '600', fontSize: 15 }}>Enable Hold Speed</Text>
<TouchableOpacity <TouchableOpacity
style={{ style={{
width: isCompact ? 48 : 54, width: 54,
height: isCompact ? 28 : 30, height: 30,
backgroundColor: holdToSpeedEnabled ? '#22C55E' : 'rgba(255,255,255,0.25)', backgroundColor: holdToSpeedEnabled ? '#22C55E' : 'rgba(255,255,255,0.25)',
borderRadius: 15, borderRadius: 15,
justifyContent: 'center', justifyContent: 'center',
@ -246,9 +178,9 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
{/* Hold Speed Selector */} {/* Hold Speed Selector */}
{holdToSpeedEnabled && ( {holdToSpeedEnabled && (
<View style={{ marginBottom: 8 }}> <View style={{ marginBottom: 16 }}>
<Text style={{ color: '#FFFFFF', fontWeight: '600', marginBottom: 6, fontSize: 13 }}>Hold Speed</Text> <Text style={{ color: 'white', fontWeight: '600', marginBottom: 10, fontSize: 15 }}>Hold Speed</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}> <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8 }}>
{holdSpeedOptions.map((speed) => { {holdSpeedOptions.map((speed) => {
const isSelected = holdToSpeedValue === speed; const isSelected = holdToSpeedValue === speed;
return ( return (
@ -256,28 +188,26 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
key={speed} key={speed}
onPress={() => handleHoldSpeedSelect(speed)} onPress={() => handleHoldSpeedSelect(speed)}
style={{ style={{
paddingHorizontal: chipPadH, paddingHorizontal: 16,
paddingVertical: chipPadV, paddingVertical: 8,
borderRadius: 10, borderRadius: 20,
backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)', backgroundColor: isSelected ? 'white' : 'rgba(255,255,255,0.06)',
borderWidth: 1, borderWidth: 1,
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)', borderColor: isSelected ? 'white' : 'rgba(255,255,255,0.1)',
minWidth: 45,
alignItems: 'center',
}} }}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={{ <Text style={{
color: isSelected ? '#22C55E' : '#FFFFFF', color: isSelected ? 'black' : 'white',
fontWeight: '600', fontWeight: isSelected ? '700' : '500',
fontSize: isCompact ? 10 : 11 fontSize: 14
}}> }}>
{speed}x {speed}x
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
); );
})} })}
</View> </ScrollView>
</View> </View>
)} )}
@ -285,16 +215,16 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
<View style={{ <View style={{
backgroundColor: 'rgba(34, 197, 94, 0.1)', backgroundColor: 'rgba(34, 197, 94, 0.1)',
borderRadius: 10, borderRadius: 10,
padding: sectionPad * 0.75, padding: 12,
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(34, 197, 94, 0.3)', borderColor: 'rgba(34, 197, 94, 0.3)',
}}> }}>
<View style={{ flexDirection: 'row', alignItems: 'flex-start', gap: 8 }}> <View style={{ flexDirection: 'row', alignItems: 'flex-start', gap: 8 }}>
<MaterialIcons name="info" size={14} color="#22C55E" /> <MaterialIcons name="info" size={16} color="#22C55E" />
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={{ <Text style={{
color: '#22C55E', color: '#22C55E',
fontSize: isCompact ? 12 : 13, fontSize: 13,
fontWeight: '600', fontWeight: '600',
marginBottom: 4, marginBottom: 4,
}}> }}>
@ -302,8 +232,8 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
</Text> </Text>
<Text style={{ <Text style={{
color: 'rgba(255, 255, 255, 0.8)', color: 'rgba(255, 255, 255, 0.8)',
fontSize: isCompact ? 11 : 12, fontSize: 12,
lineHeight: isCompact ? 14 : 16, lineHeight: 16,
}}> }}>
Hold and press the left or right side of the video player to temporarily boost playback speed. Hold and press the left or right side of the video player to temporarily boost playback speed.
</Text> </Text>
@ -311,17 +241,9 @@ export const SpeedModal: React.FC<SpeedModalProps> = ({
</View> </View>
</View> </View>
</View> </View>
</View>
</ScrollView> </ScrollView>
</Animated.View> </Animated.View>
</> </View>
);
};
return (
<>
{renderSpeedModal()}
</>
); );
}; };

View file

@ -7,7 +7,7 @@ import Animated, {
SlideInRight, SlideInRight,
SlideOutRight, SlideOutRight,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { styles } from '../utils/playerStyles'; import { StyleSheet } from 'react-native';
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes'; import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils'; import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -185,29 +185,11 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
if (!showSubtitleModal) return null; if (!showSubtitleModal) return null;
return ( return (
<> <View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
{/* Backdrop */} <TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={handleClose}>
<Animated.View <Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
entering={FadeIn.duration(200)} </TouchableOpacity>
exiting={FadeOut.duration(150)}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 9998,
}}
>
<TouchableOpacity
style={{ flex: 1 }}
onPress={handleClose}
activeOpacity={1}
/>
</Animated.View>
{/* Side Menu */}
<Animated.View <Animated.View
entering={SlideInRight.duration(300)} entering={SlideInRight.duration(300)}
exiting={SlideOutRight.duration(250)} exiting={SlideOutRight.duration(250)}
@ -217,51 +199,22 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
right: 0, right: 0,
bottom: 0, bottom: 0,
width: menuWidth, width: menuWidth,
backgroundColor: '#1A1A1A', backgroundColor: '#0f0f0f',
zIndex: 9999, borderLeftWidth: 1,
elevation: 20, borderColor: 'rgba(255,255,255,0.1)',
shadowColor: '#000',
shadowOffset: { width: -5, height: 0 },
shadowOpacity: 0.3,
shadowRadius: 10,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
paddingRight: 0,
}} }}
> >
{/* Header */} <View style={{ paddingTop: Platform.OS === 'ios' ? 60 : 15, paddingHorizontal: 20 }}>
<View style={{ <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: insets.top + (isCompact ? 8 : 12),
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.08)',
}}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
<Text style={{ color: '#FFFFFF', fontSize: 22, fontWeight: '700' }}>Subtitles</Text> <Text style={{ color: 'white', fontSize: 22, fontWeight: '700' }}>Subtitles</Text>
<View style={{ paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, backgroundColor: useCustomSubtitles ? 'rgba(34,197,94,0.2)' : 'rgba(59,130,246,0.2)' }}> <View style={{ paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, backgroundColor: useCustomSubtitles ? 'rgba(34,197,94,0.2)' : 'rgba(59,130,246,0.2)' }}>
<Text style={{ color: useCustomSubtitles ? '#22C55E' : '#3B82F6', fontSize: 11, fontWeight: '700' }}> <Text style={{ color: useCustomSubtitles ? '#22C55E' : '#3B82F6', fontSize: 11, fontWeight: '700' }}>
{useCustomSubtitles ? 'Addon in use' : 'Builtin in use'} {useCustomSubtitles ? 'Addon in use' : 'Builtin in use'}
</Text> </Text>
</View> </View>
</View> </View>
<TouchableOpacity </View>
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
justifyContent: 'center',
alignItems: 'center',
}}
onPress={handleClose}
activeOpacity={0.7}
>
<MaterialIcons name="close" size={20} color="#FFFFFF" />
</TouchableOpacity>
</View> </View>
{/* Segmented Tabs */} {/* Segmented Tabs */}
@ -902,7 +855,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</ScrollView> </ScrollView>
</Animated.View> </Animated.View>
</> </View>
); );
}; };