mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
alert orientation fix
This commit is contained in:
parent
2d5b1263b5
commit
5804959ddf
11 changed files with 242 additions and 214 deletions
|
|
@ -101,6 +101,7 @@ const AnnouncementOverlay: React.FC<AnnouncementOverlayProps> = ({
|
|||
transparent
|
||||
animationType="none"
|
||||
statusBarTranslucent
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
const isWatched = !!isWatchedProp;
|
||||
const inTraktWatchlist = isAuthenticated && isInWatchlist(item.id, item.type as 'movie' | 'show');
|
||||
const inTraktCollection = isAuthenticated && isInCollection(item.id, item.type as 'movie' | 'show');
|
||||
|
||||
|
||||
let menuOptions = [
|
||||
{
|
||||
icon: 'bookmark',
|
||||
|
|
@ -152,6 +152,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
visible={visible}
|
||||
transparent
|
||||
animationType="none"
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
|
|
@ -162,7 +163,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
<View style={styles.dragHandle} />
|
||||
<View style={styles.menuHeader}>
|
||||
<FastImage
|
||||
source={{
|
||||
source={{
|
||||
uri: item.poster,
|
||||
priority: FastImage.priority.high,
|
||||
cache: FastImage.cacheControl.immutable
|
||||
|
|
|
|||
|
|
@ -1660,6 +1660,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
animationType="fade"
|
||||
onRequestClose={closeEpisodeActionMenu}
|
||||
statusBarTranslucent
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
// Enhanced responsive sizing for tablets and TV screens
|
||||
const deviceWidth = Dimensions.get('window').width;
|
||||
const deviceHeight = Dimensions.get('window').height;
|
||||
|
||||
|
||||
// Determine device type based on width
|
||||
const getDeviceType = useCallback(() => {
|
||||
if (deviceWidth >= BREAKPOINTS.tv) return 'tv';
|
||||
|
|
@ -82,13 +82,13 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet';
|
||||
return 'phone';
|
||||
}, [deviceWidth]);
|
||||
|
||||
|
||||
const deviceType = getDeviceType();
|
||||
const isTablet = deviceType === 'tablet';
|
||||
const isLargeTablet = deviceType === 'largeTablet';
|
||||
const isTV = deviceType === 'tv';
|
||||
const isLargeScreen = isTablet || isLargeTablet || isTV;
|
||||
|
||||
|
||||
// Enhanced spacing and padding
|
||||
const horizontalPadding = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
|
|
@ -102,7 +102,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
return 16; // phone
|
||||
}
|
||||
}, [deviceType]);
|
||||
|
||||
|
||||
// Enhanced trailer card sizing
|
||||
const trailerCardWidth = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
|
|
@ -116,7 +116,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
return 200; // phone
|
||||
}
|
||||
}, [deviceType]);
|
||||
|
||||
|
||||
const trailerCardSpacing = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
case 'tv':
|
||||
|
|
@ -293,7 +293,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
// Auto-select the first available category, preferring "Trailer"
|
||||
const availableCategories = Object.keys(categorized);
|
||||
const preferredCategory = availableCategories.includes('Trailer') ? 'Trailer' :
|
||||
availableCategories.includes('Teaser') ? 'Teaser' : availableCategories[0];
|
||||
availableCategories.includes('Teaser') ? 'Teaser' : availableCategories[0];
|
||||
setSelectedCategory(preferredCategory);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -379,7 +379,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
} catch (error) {
|
||||
logger.warn('TrailersSection', 'Error pausing hero trailer:', error);
|
||||
}
|
||||
|
||||
|
||||
setSelectedTrailer(trailer);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
|
@ -499,15 +499,15 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
|
||||
return (
|
||||
<Animated.View style={[
|
||||
styles.container,
|
||||
styles.container,
|
||||
sectionAnimatedStyle,
|
||||
{ paddingHorizontal: horizontalPadding }
|
||||
]}>
|
||||
{/* Enhanced Header with Category Selector */}
|
||||
<View style={styles.header}>
|
||||
<Text style={[
|
||||
styles.headerTitle,
|
||||
{
|
||||
styles.headerTitle,
|
||||
{
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 20
|
||||
}
|
||||
|
|
@ -519,8 +519,8 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
{trailerCategories.length > 0 && selectedCategory && (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.categorySelector,
|
||||
{
|
||||
styles.categorySelector,
|
||||
{
|
||||
borderColor: 'rgba(255,255,255,0.6)',
|
||||
paddingHorizontal: isTV ? 14 : isLargeTablet ? 12 : isTablet ? 10 : 10,
|
||||
paddingVertical: isTV ? 8 : isLargeTablet ? 6 : isTablet ? 5 : 5,
|
||||
|
|
@ -533,8 +533,8 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.categorySelectorText,
|
||||
{
|
||||
styles.categorySelectorText,
|
||||
{
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12,
|
||||
maxWidth: isTV ? 150 : isLargeTablet ? 130 : isTablet ? 120 : 120
|
||||
|
|
@ -559,6 +559,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
visible={dropdownVisible}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={() => setDropdownVisible(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
|
|
@ -587,7 +588,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
>
|
||||
<View style={styles.dropdownItemContent}>
|
||||
<View style={[
|
||||
styles.categoryIconContainer,
|
||||
styles.categoryIconContainer,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.primary + '15',
|
||||
width: isTV ? 36 : isLargeTablet ? 32 : isTablet ? 28 : 28,
|
||||
|
|
@ -601,18 +602,18 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
color={currentTheme.colors.primary}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.dropdownItemText,
|
||||
{
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.dropdownItemText,
|
||||
{
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
{formatTrailerType(category)}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.dropdownItemCount,
|
||||
{
|
||||
styles.dropdownItemCount,
|
||||
{
|
||||
color: currentTheme.colors.textMuted,
|
||||
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12,
|
||||
paddingHorizontal: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8,
|
||||
|
|
@ -690,8 +691,8 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
<View style={styles.trailerInfoBelow}>
|
||||
<Text
|
||||
style={[
|
||||
styles.trailerTitle,
|
||||
{
|
||||
styles.trailerTitle,
|
||||
{
|
||||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12,
|
||||
lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 16,
|
||||
|
|
@ -704,8 +705,8 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
{trailer.displayName || trailer.name}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.trailerMeta,
|
||||
{
|
||||
styles.trailerMeta,
|
||||
{
|
||||
color: currentTheme.colors.textMuted,
|
||||
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 10
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,10 +135,10 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
// Helper to get dynamic volume icon
|
||||
const getVolumeIcon = (value: number) => {
|
||||
if (value === 0) return 'volume-off';
|
||||
if (value < 0.3) return 'volume-mute';
|
||||
if (value < 0.6) return 'volume-down';
|
||||
return 'volume-up';
|
||||
if (value === 0) return 'volume-off';
|
||||
if (value < 0.3) return 'volume-mute';
|
||||
if (value < 0.6) return 'volume-down';
|
||||
return 'volume-up';
|
||||
};
|
||||
|
||||
// Helper to get dynamic brightness icon
|
||||
|
|
@ -3432,8 +3432,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
buffered={buffered}
|
||||
formatTime={formatTime}
|
||||
playerBackend={useVLC ? 'VLC' : 'ExoPlayer'}
|
||||
nextLoadingTitle={nextLoadingTitle}
|
||||
controlsFixedOffset={Math.min(Dimensions.get('window').width, Dimensions.get('window').height) >= 768 ? 120 : 100}
|
||||
/>
|
||||
|
||||
{/* Combined Volume & Brightness Gesture Indicator - NEW PILL STYLE (No Bar) */}
|
||||
|
|
@ -3441,45 +3439,45 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
<View style={localStyles.gestureIndicatorContainer}>
|
||||
{/* Dynamic Icon */}
|
||||
<View
|
||||
style={[
|
||||
localStyles.iconWrapper,
|
||||
{
|
||||
// Conditional Background Color Logic
|
||||
backgroundColor: gestureControls.showVolumeOverlay && volume === 0
|
||||
? 'rgba(242, 184, 181)'
|
||||
: 'rgba(59, 59, 59)'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={
|
||||
gestureControls.showVolumeOverlay
|
||||
? getVolumeIcon(volume)
|
||||
: getBrightnessIcon(brightness)
|
||||
}
|
||||
size={24} // Reduced size to fit inside a 32-40px circle better
|
||||
color={
|
||||
gestureControls.showVolumeOverlay && volume === 0
|
||||
? 'rgba(96, 20, 16)' // Bright RED for MUTE icon itself
|
||||
: 'rgba(255, 255, 255)' // White for all other states
|
||||
}
|
||||
/>
|
||||
style={[
|
||||
localStyles.iconWrapper,
|
||||
{
|
||||
// Conditional Background Color Logic
|
||||
backgroundColor: gestureControls.showVolumeOverlay && volume === 0
|
||||
? 'rgba(242, 184, 181)'
|
||||
: 'rgba(59, 59, 59)'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={
|
||||
gestureControls.showVolumeOverlay
|
||||
? getVolumeIcon(volume)
|
||||
: getBrightnessIcon(brightness)
|
||||
}
|
||||
size={24} // Reduced size to fit inside a 32-40px circle better
|
||||
color={
|
||||
gestureControls.showVolumeOverlay && volume === 0
|
||||
? 'rgba(96, 20, 16)' // Bright RED for MUTE icon itself
|
||||
: 'rgba(255, 255, 255)' // White for all other states
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Text Label: Shows "Muted" or percentage */}
|
||||
<Text
|
||||
style={[
|
||||
localStyles.gestureText,
|
||||
// Conditional Text Color Logic
|
||||
gestureControls.showVolumeOverlay && volume === 0 && { color: 'rgba(242, 184, 181)' } // Light RED for "Muted"
|
||||
]}
|
||||
>
|
||||
{/* Conditional Text Content Logic */}
|
||||
{gestureControls.showVolumeOverlay && volume === 0
|
||||
? "Muted" // Display "Muted" when volume is 0
|
||||
: `${Math.round((gestureControls.showVolumeOverlay ? volume : brightness) * 100)}%` // Display percentage otherwise
|
||||
}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
localStyles.gestureText,
|
||||
// Conditional Text Color Logic
|
||||
gestureControls.showVolumeOverlay && volume === 0 && { color: 'rgba(242, 184, 181)' } // Light RED for "Muted"
|
||||
]}
|
||||
>
|
||||
{/* Conditional Text Content Logic */}
|
||||
{gestureControls.showVolumeOverlay && volume === 0
|
||||
? "Muted" // Display "Muted" when volume is 0
|
||||
: `${Math.round((gestureControls.showVolumeOverlay ? volume : brightness) * 100)}%` // Display percentage otherwise
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
|
@ -4067,32 +4065,32 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
// New styles for the gesture indicator
|
||||
const localStyles = StyleSheet.create({
|
||||
gestureIndicatorContainer: {
|
||||
position: 'absolute',
|
||||
top: '4%', // Adjust this for vertical position
|
||||
alignSelf: 'center', // Adjust this for horizontal position
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(25, 25, 25)', // Dark pill background
|
||||
borderRadius: 70,
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 15,
|
||||
zIndex: 2000, // Very high z-index to ensure visibility
|
||||
minWidth: 120, // Adjusted min width since bar is removed
|
||||
position: 'absolute',
|
||||
top: '4%', // Adjust this for vertical position
|
||||
alignSelf: 'center', // Adjust this for horizontal position
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(25, 25, 25)', // Dark pill background
|
||||
borderRadius: 70,
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 15,
|
||||
zIndex: 2000, // Very high z-index to ensure visibility
|
||||
minWidth: 120, // Adjusted min width since bar is removed
|
||||
},
|
||||
iconWrapper: {
|
||||
borderRadius: 50, // Makes it a perfect circle (set to a high number)
|
||||
width: 40, // Define the diameter of the circle
|
||||
height: 40, // Define the diameter of the circle
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12, // Margin to separate icon circle from percentage text
|
||||
borderRadius: 50, // Makes it a perfect circle (set to a high number)
|
||||
width: 40, // Define the diameter of the circle
|
||||
height: 40, // Define the diameter of the circle
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12, // Margin to separate icon circle from percentage text
|
||||
},
|
||||
gestureText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: 'normal',
|
||||
minWidth: 35,
|
||||
textAlign: 'right',
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: 'normal',
|
||||
minWidth: 35,
|
||||
textAlign: 'right',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1413,6 +1413,7 @@ const AddonsScreen = () => {
|
|||
visible={showConfirmModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={() => {
|
||||
setShowConfirmModal(false);
|
||||
setAddonDetails(null);
|
||||
|
|
|
|||
|
|
@ -685,6 +685,7 @@ const CatalogSettingsScreen = () => {
|
|||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={isRenameModalVisible}
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={() => {
|
||||
setIsRenameModalVisible(false);
|
||||
setCatalogToRename(null);
|
||||
|
|
|
|||
|
|
@ -1946,6 +1946,7 @@ const PluginsScreen: React.FC = () => {
|
|||
visible={showHelpModal}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={() => setShowHelpModal(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
|
|
@ -1978,6 +1979,7 @@ const PluginsScreen: React.FC = () => {
|
|||
visible={showAddRepositoryModal}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={() => setShowAddRepositoryModal(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
const navigation = useNavigation();
|
||||
const { currentTheme } = useTheme();
|
||||
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
|
||||
|
||||
|
||||
const [profiles, setProfiles] = useState<Profile[]>([]);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [newProfileName, setNewProfileName] = useState('');
|
||||
|
|
@ -52,7 +52,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
) => {
|
||||
setAlertTitle(title);
|
||||
setAlertMessage(message);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
|
||||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return unsubscribe;
|
||||
}, [navigation, refreshAuthStatus, isAuthenticated, loadProfiles]);
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
navigation.goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
loadProfiles();
|
||||
}, [isAuthenticated, loadProfiles, navigation]);
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
...profile,
|
||||
isActive: profile.id === id
|
||||
}));
|
||||
|
||||
|
||||
setProfiles(updatedProfiles);
|
||||
saveProfiles(updatedProfiles);
|
||||
}, [profiles, saveProfiles]);
|
||||
|
|
@ -164,14 +164,14 @@ const ProfilesScreen: React.FC = () => {
|
|||
'Delete Profile',
|
||||
'Are you sure you want to delete this profile? This action cannot be undone.',
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{
|
||||
label: 'Delete',
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{
|
||||
label: 'Delete',
|
||||
onPress: () => {
|
||||
const updatedProfiles = profiles.filter(profile => profile.id !== id);
|
||||
setProfiles(updatedProfiles);
|
||||
saveProfiles(updatedProfiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
|
@ -183,10 +183,10 @@ const ProfilesScreen: React.FC = () => {
|
|||
|
||||
const renderItem = ({ item }: { item: Profile }) => (
|
||||
<View style={styles.profileItem}>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.profileContent,
|
||||
item.isActive && {
|
||||
item.isActive && {
|
||||
backgroundColor: `${currentTheme.colors.primary}30`,
|
||||
borderColor: currentTheme.colors.primary
|
||||
}
|
||||
|
|
@ -194,10 +194,10 @@ const ProfilesScreen: React.FC = () => {
|
|||
onPress={() => handleSelectProfile(item.id)}
|
||||
>
|
||||
<View style={styles.avatarContainer}>
|
||||
<MaterialIcons
|
||||
name="account-circle"
|
||||
size={40}
|
||||
color={item.isActive ? currentTheme.colors.primary : currentTheme.colors.text}
|
||||
<MaterialIcons
|
||||
name="account-circle"
|
||||
size={40}
|
||||
color={item.isActive ? currentTheme.colors.primary : currentTheme.colors.text}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.profileInfo}>
|
||||
|
|
@ -211,7 +211,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
)}
|
||||
</View>
|
||||
{!item.isActive && (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleDeleteProfile(item.id)}
|
||||
>
|
||||
|
|
@ -225,7 +225,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
||||
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
onPress={handleBack}
|
||||
|
|
@ -281,6 +281,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
visible={showAddModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={() => setShowAddModal(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
|
|
@ -288,11 +289,11 @@ const ProfilesScreen: React.FC = () => {
|
|||
<Text style={[styles.modalTitle, { color: currentTheme.colors.text }]}>
|
||||
Create New Profile
|
||||
</Text>
|
||||
|
||||
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
{
|
||||
backgroundColor: `${currentTheme.colors.textMuted}20`,
|
||||
color: currentTheme.colors.text,
|
||||
borderColor: currentTheme.colors.border
|
||||
|
|
@ -304,9 +305,9 @@ const ProfilesScreen: React.FC = () => {
|
|||
onChangeText={setNewProfileName}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[styles.modalButton, styles.cancelButton]}
|
||||
onPress={() => {
|
||||
setNewProfileName('');
|
||||
|
|
@ -315,9 +316,9 @@ const ProfilesScreen: React.FC = () => {
|
|||
>
|
||||
<Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.modalButton,
|
||||
styles.modalButton,
|
||||
styles.createButton,
|
||||
{ backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -132,17 +132,27 @@ export const StreamsScreen = () => {
|
|||
const { showSuccess, showInfo } = useToast();
|
||||
|
||||
// Add dimension listener and tablet detection
|
||||
// Use a ref to track previous dimensions to avoid unnecessary re-renders
|
||||
const [dimensions, setDimensions] = useState(Dimensions.get('window'));
|
||||
const prevDimensionsRef = useRef({ width: dimensions.width, height: dimensions.height });
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = Dimensions.addEventListener('change', ({ window }) => {
|
||||
setDimensions(window);
|
||||
// Only update state if dimensions actually changed (with 1px tolerance)
|
||||
const widthChanged = Math.abs(window.width - prevDimensionsRef.current.width) > 1;
|
||||
const heightChanged = Math.abs(window.height - prevDimensionsRef.current.height) > 1;
|
||||
|
||||
if (widthChanged || heightChanged) {
|
||||
prevDimensionsRef.current = { width: window.width, height: window.height };
|
||||
setDimensions(window);
|
||||
}
|
||||
});
|
||||
return () => subscription?.remove();
|
||||
}, []);
|
||||
|
||||
// Memoize tablet detection to prevent recalculation on every render
|
||||
const deviceWidth = dimensions.width;
|
||||
const isTablet = deviceWidth >= 768;
|
||||
const isTablet = useMemo(() => deviceWidth >= 768, [deviceWidth]);
|
||||
|
||||
// Add refs to prevent excessive updates and duplicate loads
|
||||
const isMounted = useRef(true);
|
||||
|
|
@ -303,6 +313,9 @@ export const StreamsScreen = () => {
|
|||
}, []);
|
||||
|
||||
// Monitor streams loading and update available providers immediately
|
||||
// Use a ref to track the previous providers to avoid unnecessary state updates
|
||||
const prevProvidersRef = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
// Skip processing if component is unmounting
|
||||
if (!isMounted.current) return;
|
||||
|
|
@ -317,14 +330,21 @@ export const StreamsScreen = () => {
|
|||
|
||||
if (providersWithStreams.length > 0) {
|
||||
logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`);
|
||||
const providersWithStreamsSet = new Set(providersWithStreams);
|
||||
|
||||
// Only update if we have new providers, don't remove existing ones during loading
|
||||
setAvailableProviders(prevProviders => {
|
||||
const newProviders = new Set([...prevProviders, ...providersWithStreamsSet]);
|
||||
if (__DEV__) console.log('[StreamsScreen] availableProviders ->', Array.from(newProviders));
|
||||
return newProviders;
|
||||
});
|
||||
// Check if we actually have new providers before triggering state update
|
||||
const hasNewProviders = providersWithStreams.some(
|
||||
provider => !prevProvidersRef.current.has(provider)
|
||||
);
|
||||
|
||||
if (hasNewProviders) {
|
||||
setAvailableProviders(prevProviders => {
|
||||
const newProviders = new Set([...prevProviders, ...providersWithStreams]);
|
||||
// Update ref to track current providers
|
||||
prevProvidersRef.current = newProviders;
|
||||
if (__DEV__) console.log('[StreamsScreen] availableProviders ->', Array.from(newProviders));
|
||||
return newProviders;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update loading states for individual providers
|
||||
|
|
|
|||
|
|
@ -36,27 +36,27 @@ const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c';
|
|||
|
||||
// Define example shows with their IMDB IDs and TMDB IDs
|
||||
const EXAMPLE_SHOWS = [
|
||||
{
|
||||
name: 'Breaking Bad',
|
||||
imdbId: 'tt0903747',
|
||||
{
|
||||
name: 'Breaking Bad',
|
||||
imdbId: 'tt0903747',
|
||||
tmdbId: '1396',
|
||||
type: 'tv' as const
|
||||
},
|
||||
{
|
||||
name: 'Friends',
|
||||
imdbId: 'tt0108778',
|
||||
{
|
||||
name: 'Friends',
|
||||
imdbId: 'tt0108778',
|
||||
tmdbId: '1668',
|
||||
type: 'tv' as const
|
||||
},
|
||||
{
|
||||
name: 'Stranger Things',
|
||||
imdbId: 'tt4574334',
|
||||
{
|
||||
name: 'Stranger Things',
|
||||
imdbId: 'tt4574334',
|
||||
tmdbId: '66732',
|
||||
type: 'tv' as const
|
||||
},
|
||||
{
|
||||
name: 'Avatar',
|
||||
imdbId: 'tt0499549',
|
||||
{
|
||||
name: 'Avatar',
|
||||
imdbId: 'tt0499549',
|
||||
tmdbId: '19995',
|
||||
type: 'movie' as const
|
||||
},
|
||||
|
|
@ -82,7 +82,7 @@ const TMDBSettingsScreen = () => {
|
|||
const { settings, updateSetting } = useSettings();
|
||||
const [languagePickerVisible, setLanguagePickerVisible] = useState(false);
|
||||
const [languageSearch, setLanguageSearch] = useState('');
|
||||
|
||||
|
||||
// Logo preview state
|
||||
const [selectedShow, setSelectedShow] = useState(EXAMPLE_SHOWS[0]);
|
||||
const [tmdbLogo, setTmdbLogo] = useState<string | null>(null);
|
||||
|
|
@ -126,7 +126,7 @@ const TMDBSettingsScreen = () => {
|
|||
try {
|
||||
const keys = await mmkvStorage.getAllKeys();
|
||||
const tmdbKeys = keys.filter(key => key.startsWith('tmdb_cache_'));
|
||||
|
||||
|
||||
let totalSize = 0;
|
||||
for (const key of tmdbKeys) {
|
||||
const value = mmkvStorage.getString(key);
|
||||
|
|
@ -134,7 +134,7 @@ const TMDBSettingsScreen = () => {
|
|||
totalSize += value.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Convert to KB/MB
|
||||
let sizeStr = '';
|
||||
if (totalSize < 1024) {
|
||||
|
|
@ -144,7 +144,7 @@ const TMDBSettingsScreen = () => {
|
|||
} else {
|
||||
sizeStr = `${(totalSize / (1024 * 1024)).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
|
||||
setCacheSize(sizeStr);
|
||||
} catch (error) {
|
||||
logger.error('[TMDBSettingsScreen] Error calculating cache size:', error);
|
||||
|
|
@ -187,17 +187,17 @@ const TMDBSettingsScreen = () => {
|
|||
mmkvStorage.getItem(TMDB_API_KEY_STORAGE_KEY),
|
||||
mmkvStorage.getItem(USE_CUSTOM_TMDB_API_KEY)
|
||||
]);
|
||||
|
||||
|
||||
logger.log('[TMDBSettingsScreen] API key status:', savedKey ? 'Found' : 'Not found');
|
||||
logger.log('[TMDBSettingsScreen] Use custom API setting:', savedUseCustomKey);
|
||||
|
||||
|
||||
if (savedKey) {
|
||||
setApiKey(savedKey);
|
||||
setIsKeySet(true);
|
||||
} else {
|
||||
setIsKeySet(false);
|
||||
}
|
||||
|
||||
|
||||
setUseCustomKey(savedUseCustomKey === 'true');
|
||||
} catch (error) {
|
||||
logger.error('[TMDBSettingsScreen] Failed to load settings:', error);
|
||||
|
|
@ -212,7 +212,7 @@ const TMDBSettingsScreen = () => {
|
|||
const saveApiKey = async () => {
|
||||
logger.log('[TMDBSettingsScreen] Starting API key save');
|
||||
Keyboard.dismiss();
|
||||
|
||||
|
||||
try {
|
||||
const trimmedKey = apiKey.trim();
|
||||
if (!trimmedKey) {
|
||||
|
|
@ -299,27 +299,27 @@ const TMDBSettingsScreen = () => {
|
|||
try {
|
||||
await mmkvStorage.setItem(USE_CUSTOM_TMDB_API_KEY, value ? 'true' : 'false');
|
||||
setUseCustomKey(value);
|
||||
|
||||
|
||||
if (!value) {
|
||||
// If switching to built-in key, show confirmation
|
||||
logger.log('[TMDBSettingsScreen] Switching to built-in API key');
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: 'Now using the built-in TMDb API key.'
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: 'Now using the built-in TMDb API key.'
|
||||
});
|
||||
} else if (apiKey && isKeySet) {
|
||||
// If switching to custom key and we have a key
|
||||
logger.log('[TMDBSettingsScreen] Switching to custom API key');
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: 'Now using your custom TMDb API key.'
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: 'Now using your custom TMDb API key.'
|
||||
});
|
||||
} else {
|
||||
// If switching to custom key but don't have a key yet
|
||||
logger.log('[TMDBSettingsScreen] No custom key available yet');
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: 'Please enter and save your custom TMDb API key.'
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: 'Please enter and save your custom TMDb API key.'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -355,27 +355,27 @@ const TMDBSettingsScreen = () => {
|
|||
setLoadingLogos(true);
|
||||
setTmdbLogo(null);
|
||||
setTmdbBanner(null);
|
||||
|
||||
|
||||
try {
|
||||
const tmdbId = show.tmdbId;
|
||||
const contentType = show.type;
|
||||
|
||||
|
||||
logger.log(`[TMDBSettingsScreen] Fetching ${show.name} with TMDB ID: ${tmdbId}`);
|
||||
|
||||
|
||||
const preferredTmdbLanguage = settings.tmdbLanguagePreference || 'en';
|
||||
|
||||
|
||||
const apiKey = TMDB_API_KEY;
|
||||
const endpoint = contentType === 'tv' ? 'tv' : 'movie';
|
||||
const response = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}/images?api_key=${apiKey}`);
|
||||
const imagesData = await response.json();
|
||||
|
||||
|
||||
if (imagesData.logos && imagesData.logos.length > 0) {
|
||||
let logoPath: string | null = null;
|
||||
let logoLanguage = preferredTmdbLanguage;
|
||||
|
||||
|
||||
// Try to find logo in preferred language
|
||||
const preferredLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === preferredTmdbLanguage);
|
||||
|
||||
|
||||
if (preferredLogo) {
|
||||
logoPath = preferredLogo.file_path;
|
||||
logoLanguage = preferredTmdbLanguage;
|
||||
|
|
@ -383,7 +383,7 @@ const TMDBSettingsScreen = () => {
|
|||
} else {
|
||||
// Fallback to English
|
||||
const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === 'en');
|
||||
|
||||
|
||||
if (englishLogo) {
|
||||
logoPath = englishLogo.file_path;
|
||||
logoLanguage = 'en';
|
||||
|
|
@ -395,7 +395,7 @@ const TMDBSettingsScreen = () => {
|
|||
setIsPreviewFallback(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (logoPath) {
|
||||
setTmdbLogo(`https://image.tmdb.org/t/p/original${logoPath}`);
|
||||
setPreviewLanguage(logoLanguage);
|
||||
|
|
@ -407,7 +407,7 @@ const TMDBSettingsScreen = () => {
|
|||
setPreviewLanguage('');
|
||||
setIsPreviewFallback(false);
|
||||
}
|
||||
|
||||
|
||||
// Get TMDB banner (backdrop)
|
||||
if (imagesData.backdrops && imagesData.backdrops.length > 0) {
|
||||
const backdropPath = imagesData.backdrops[0].file_path;
|
||||
|
|
@ -415,7 +415,7 @@ const TMDBSettingsScreen = () => {
|
|||
} else {
|
||||
const detailsResponse = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}?api_key=${apiKey}`);
|
||||
const details = await detailsResponse.json();
|
||||
|
||||
|
||||
if (details.backdrop_path) {
|
||||
setTmdbBanner(`https://image.tmdb.org/t/p/original${details.backdrop_path}`);
|
||||
}
|
||||
|
|
@ -444,17 +444,17 @@ const TMDBSettingsScreen = () => {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<View style={styles.bannerContainer}>
|
||||
<FastImage
|
||||
<FastImage
|
||||
source={{ uri: banner || undefined }}
|
||||
style={styles.bannerImage}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
<View style={styles.bannerOverlay} />
|
||||
{logo && (
|
||||
<FastImage
|
||||
<FastImage
|
||||
source={{ uri: logo }}
|
||||
style={styles.logoOverBanner}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
|
|
@ -491,7 +491,7 @@ const TMDBSettingsScreen = () => {
|
|||
if (__DEV__) console.error('Error loading selected show:', e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
loadSelectedShow();
|
||||
}, []);
|
||||
|
||||
|
|
@ -512,7 +512,7 @@ const TMDBSettingsScreen = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
||||
<View style={styles.header}>
|
||||
|
|
@ -520,7 +520,7 @@ const TMDBSettingsScreen = () => {
|
|||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
|
||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -602,7 +602,7 @@ const TMDBSettingsScreen = () => {
|
|||
|
||||
{/* Logo Preview */}
|
||||
<View style={styles.divider} />
|
||||
|
||||
|
||||
<Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 8 }]}>Logo Preview</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis, marginBottom: 12 }]}>
|
||||
Preview shows how localized logos will appear in the selected language.
|
||||
|
|
@ -610,8 +610,8 @@ const TMDBSettingsScreen = () => {
|
|||
|
||||
{/* Show selector */}
|
||||
<Text style={[styles.selectorLabel, { color: currentTheme.colors.mediumEmphasis }]}>Example:</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.showsScrollContent}
|
||||
style={styles.showsScrollView}
|
||||
|
|
@ -627,7 +627,7 @@ const TMDBSettingsScreen = () => {
|
|||
onPress={() => handleShowSelect(show)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
<Text
|
||||
style={[
|
||||
styles.showItemText,
|
||||
{ color: currentTheme.colors.mediumEmphasis },
|
||||
|
|
@ -795,7 +795,7 @@ const TMDBSettingsScreen = () => {
|
|||
|
||||
{/* Cache Management Section */}
|
||||
<View style={styles.divider} />
|
||||
|
||||
|
||||
<View style={styles.settingRow}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Cache Size</Text>
|
||||
|
|
@ -828,6 +828,7 @@ const TMDBSettingsScreen = () => {
|
|||
visible={languagePickerVisible}
|
||||
transparent
|
||||
animationType="slide"
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={() => setLanguagePickerVisible(false)}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={() => setLanguagePickerVisible(false)}>
|
||||
|
|
@ -955,42 +956,42 @@ const TMDBSettingsScreen = () => {
|
|||
return (
|
||||
<>
|
||||
{filteredLanguages.map(({ code, label, native }) => (
|
||||
<TouchableOpacity
|
||||
key={code}
|
||||
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
|
||||
style={[
|
||||
styles.languageItem,
|
||||
settings.tmdbLanguagePreference === code && styles.selectedLanguageItem
|
||||
]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.languageContent}>
|
||||
<View style={styles.languageInfo}>
|
||||
<Text style={[
|
||||
styles.languageName,
|
||||
settings.tmdbLanguagePreference === code && styles.selectedLanguageName,
|
||||
{
|
||||
color: settings.tmdbLanguagePreference === code ? currentTheme.colors.primary : currentTheme.colors.text,
|
||||
}
|
||||
]}>
|
||||
{native}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.languageCode,
|
||||
settings.tmdbLanguagePreference === code && styles.selectedLanguageCode,
|
||||
{
|
||||
color: settings.tmdbLanguagePreference === code ? currentTheme.colors.primary : currentTheme.colors.mediumEmphasis,
|
||||
}
|
||||
]}>
|
||||
{label} • {code.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
{settings.tmdbLanguagePreference === code && (
|
||||
<View style={styles.checkmarkContainer}>
|
||||
<MaterialIcons name="check-circle" size={24} color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
key={code}
|
||||
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
|
||||
style={[
|
||||
styles.languageItem,
|
||||
settings.tmdbLanguagePreference === code && styles.selectedLanguageItem
|
||||
]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.languageContent}>
|
||||
<View style={styles.languageInfo}>
|
||||
<Text style={[
|
||||
styles.languageName,
|
||||
settings.tmdbLanguagePreference === code && styles.selectedLanguageName,
|
||||
{
|
||||
color: settings.tmdbLanguagePreference === code ? currentTheme.colors.primary : currentTheme.colors.text,
|
||||
}
|
||||
]}>
|
||||
{native}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.languageCode,
|
||||
settings.tmdbLanguagePreference === code && styles.selectedLanguageCode,
|
||||
{
|
||||
color: settings.tmdbLanguagePreference === code ? currentTheme.colors.primary : currentTheme.colors.mediumEmphasis,
|
||||
}
|
||||
]}>
|
||||
{label} • {code.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
{settings.tmdbLanguagePreference === code && (
|
||||
<View style={styles.checkmarkContainer}>
|
||||
<MaterialIcons name="check-circle" size={24} color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
{languageSearch.length > 0 && filteredLanguages.length === 0 && (
|
||||
|
|
|
|||
Loading…
Reference in a new issue