mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
some changes
This commit is contained in:
parent
501bded9ee
commit
9246b26493
6 changed files with 346 additions and 75 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit cfd669df2258d217c90c58ce9455676c60abd1ac
|
Subproject commit a6239977e8725a631df20154c45bff9572c3ff98
|
||||||
|
|
@ -977,6 +977,8 @@ const styles = StyleSheet.create({
|
||||||
textShadowColor: 'rgba(0,0,0,0.8)',
|
textShadowColor: 'rgba(0,0,0,0.8)',
|
||||||
textShadowOffset: { width: 0, height: 1 },
|
textShadowOffset: { width: 0, height: 1 },
|
||||||
textShadowRadius: 2,
|
textShadowRadius: 2,
|
||||||
|
maxWidth: '70%',
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
tabletButtons: {
|
tabletButtons: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -1031,4 +1033,4 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default React.memo(FeaturedContent);
|
export default React.memo(FeaturedContent);
|
||||||
|
|
@ -31,6 +31,7 @@ import { logger } from '../../utils/logger';
|
||||||
import { TMDBService } from '../../services/tmdbService';
|
import { TMDBService } from '../../services/tmdbService';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
const isTablet = width >= 768;
|
||||||
|
|
||||||
// Ultra-optimized animation constants
|
// Ultra-optimized animation constants
|
||||||
const PARALLAX_FACTOR = 0.3;
|
const PARALLAX_FACTOR = 0.3;
|
||||||
|
|
@ -240,9 +241,9 @@ const ActionButtons = memo(({
|
||||||
}, [isWatched, playButtonText, type, watchProgress, groupedEpisodes]);
|
}, [isWatched, playButtonText, type, watchProgress, groupedEpisodes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.actionButtons, animatedStyle]}>
|
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={playButtonStyle}
|
style={[playButtonStyle, isTablet && styles.tabletPlayButton]}
|
||||||
onPress={handleShowStreams}
|
onPress={handleShowStreams}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
|
|
@ -253,14 +254,14 @@ const ActionButtons = memo(({
|
||||||
}
|
}
|
||||||
return playButtonText === 'Resume' ? 'play-circle-outline' : 'play-arrow';
|
return playButtonText === 'Resume' ? 'play-circle-outline' : 'play-arrow';
|
||||||
})()}
|
})()}
|
||||||
size={24}
|
size={isTablet ? 28 : 24}
|
||||||
color={isWatched && type === 'movie' ? "#fff" : "#000"}
|
color={isWatched && type === 'movie' ? "#fff" : "#000"}
|
||||||
/>
|
/>
|
||||||
<Text style={playButtonTextStyle}>{finalPlayButtonText}</Text>
|
<Text style={[playButtonTextStyle, isTablet && styles.tabletPlayButtonText]}>{finalPlayButtonText}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.actionButton, styles.infoButton]}
|
style={[styles.actionButton, styles.infoButton, isTablet && styles.tabletInfoButton]}
|
||||||
onPress={toggleLibrary}
|
onPress={toggleLibrary}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
|
|
@ -271,17 +272,17 @@ const ActionButtons = memo(({
|
||||||
)}
|
)}
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={inLibrary ? 'bookmark' : 'bookmark-border'}
|
name={inLibrary ? 'bookmark' : 'bookmark-border'}
|
||||||
size={24}
|
size={isTablet ? 28 : 24}
|
||||||
color={currentTheme.colors.white}
|
color={currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.infoButtonText}>
|
<Text style={[styles.infoButtonText, isTablet && styles.tabletInfoButtonText]}>
|
||||||
{inLibrary ? 'Saved' : 'Save'}
|
{inLibrary ? 'Saved' : 'Save'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{type === 'series' && (
|
{type === 'series' && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.iconButton}
|
style={[styles.iconButton, isTablet && styles.tabletIconButton]}
|
||||||
onPress={handleRatingsPress}
|
onPress={handleRatingsPress}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
|
|
@ -292,7 +293,7 @@ const ActionButtons = memo(({
|
||||||
)}
|
)}
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="assessment"
|
name="assessment"
|
||||||
size={24}
|
size={isTablet ? 28 : 24}
|
||||||
color={currentTheme.colors.white}
|
color={currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
@ -534,9 +535,9 @@ const WatchProgressDisplay = memo(({
|
||||||
const isCompleted = progressData.isWatched || progressData.progressPercent >= 85;
|
const isCompleted = progressData.isWatched || progressData.progressPercent >= 85;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.watchProgressContainer, animatedStyle]}>
|
<Animated.View style={[isTablet ? styles.tabletWatchProgressContainer : styles.watchProgressContainer, animatedStyle]}>
|
||||||
{/* Glass morphism background with entrance animation */}
|
{/* Glass morphism background with entrance animation */}
|
||||||
<Animated.View style={[styles.progressGlassBackground, progressBoxAnimatedStyle]}>
|
<Animated.View style={[isTablet ? styles.tabletProgressGlassBackground : styles.progressGlassBackground, progressBoxAnimatedStyle]}>
|
||||||
{Platform.OS === 'ios' ? (
|
{Platform.OS === 'ios' ? (
|
||||||
<ExpoBlurView intensity={20} style={styles.blurBackground} tint="dark" />
|
<ExpoBlurView intensity={20} style={styles.blurBackground} tint="dark" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -580,9 +581,9 @@ const WatchProgressDisplay = memo(({
|
||||||
{/* Enhanced text container with better typography */}
|
{/* Enhanced text container with better typography */}
|
||||||
<View style={styles.watchProgressTextContainer}>
|
<View style={styles.watchProgressTextContainer}>
|
||||||
<View style={styles.progressInfoMain}>
|
<View style={styles.progressInfoMain}>
|
||||||
<Text style={[styles.watchProgressMainText, {
|
<Text style={[isTablet ? styles.tabletWatchProgressMainText : styles.watchProgressMainText, {
|
||||||
color: isCompleted ? '#00ff88' : currentTheme.colors.white,
|
color: isCompleted ? '#00ff88' : currentTheme.colors.white,
|
||||||
fontSize: isCompleted ? 13 : 12,
|
fontSize: isCompleted ? (isTablet ? 15 : 13) : (isTablet ? 14 : 12),
|
||||||
fontWeight: isCompleted ? '700' : '600'
|
fontWeight: isCompleted ? '700' : '600'
|
||||||
}]}>
|
}]}>
|
||||||
{progressData.displayText}
|
{progressData.displayText}
|
||||||
|
|
@ -590,7 +591,7 @@ const WatchProgressDisplay = memo(({
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={[styles.watchProgressSubText, {
|
<Text style={[isTablet ? styles.tabletWatchProgressSubText : styles.watchProgressSubText, {
|
||||||
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
|
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
|
||||||
}]}>
|
}]}>
|
||||||
{progressData.episodeInfo} • Last watched {progressData.formattedTime}
|
{progressData.episodeInfo} • Last watched {progressData.formattedTime}
|
||||||
|
|
@ -839,11 +840,11 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
entering={FadeIn.duration(400).delay(200 + index * 100)}
|
entering={FadeIn.duration(400).delay(200 + index * 100)}
|
||||||
style={{ flexDirection: 'row', alignItems: 'center' }}
|
style={{ flexDirection: 'row', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<Text style={[styles.genreText, { color: themeColors.text }]}>
|
<Text style={[isTablet ? styles.tabletGenreText : styles.genreText, { color: themeColors.text }]}>
|
||||||
{genreName}
|
{genreName}
|
||||||
</Text>
|
</Text>
|
||||||
{index < array.length - 1 && (
|
{index < array.length - 1 && (
|
||||||
<Text style={[styles.genreDot, { color: themeColors.text }]}>•</Text>
|
<Text style={[isTablet ? styles.tabletGenreDot : styles.genreDot, { color: themeColors.text }]}>•</Text>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
));
|
));
|
||||||
|
|
@ -966,14 +967,14 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
style={styles.bottomFadeGradient}
|
style={styles.bottomFadeGradient}
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
/>
|
/>
|
||||||
<View style={styles.heroContent}>
|
<View style={[styles.heroContent, isTablet && { maxWidth: 800, alignSelf: 'center' }]}>
|
||||||
{/* Optimized Title/Logo */}
|
{/* Optimized Title/Logo */}
|
||||||
<View style={styles.logoContainer}>
|
<View style={styles.logoContainer}>
|
||||||
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
|
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
|
||||||
{shouldLoadSecondaryData && metadata.logo && !logoLoadError ? (
|
{shouldLoadSecondaryData && metadata.logo && !logoLoadError ? (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: metadata.logo }}
|
source={{ uri: metadata.logo }}
|
||||||
style={styles.titleLogo}
|
style={isTablet ? styles.tabletTitleLogo : styles.titleLogo}
|
||||||
contentFit="contain"
|
contentFit="contain"
|
||||||
transition={150}
|
transition={150}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
|
|
@ -981,7 +982,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text style={[styles.heroTitle, { color: themeColors.highEmphasis }]}>
|
<Text style={[isTablet ? styles.tabletHeroTitle : styles.heroTitle, { color: themeColors.highEmphasis }]}>
|
||||||
{metadata.name}
|
{metadata.name}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
@ -999,7 +1000,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
|
|
||||||
{/* Optimized genre display with lazy loading */}
|
{/* Optimized genre display with lazy loading */}
|
||||||
{shouldLoadSecondaryData && genreElements && (
|
{shouldLoadSecondaryData && genreElements && (
|
||||||
<View style={styles.genreContainer}>
|
<View style={isTablet ? styles.tabletGenreContainer : styles.genreContainer}>
|
||||||
{genreElements}
|
{genreElements}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1041,7 +1042,7 @@ const styles = StyleSheet.create({
|
||||||
backButtonContainer: {
|
backButtonContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: Platform.OS === 'android' ? 40 : 50,
|
top: Platform.OS === 'android' ? 40 : 50,
|
||||||
left: 16,
|
left: isTablet ? 32 : 16,
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
|
|
@ -1066,9 +1067,9 @@ const styles = StyleSheet.create({
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
heroContent: {
|
heroContent: {
|
||||||
padding: 16,
|
padding: isTablet ? 32 : 16,
|
||||||
paddingTop: 8,
|
paddingTop: isTablet ? 16 : 8,
|
||||||
paddingBottom: 8,
|
paddingBottom: isTablet ? 16 : 8,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
},
|
},
|
||||||
|
|
@ -1077,16 +1078,25 @@ const styles = StyleSheet.create({
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
|
flex: 0,
|
||||||
|
display: 'flex',
|
||||||
|
maxWidth: isTablet ? 600 : '100%',
|
||||||
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
titleLogoContainer: {
|
titleLogoContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
flex: 0,
|
||||||
|
display: 'flex',
|
||||||
|
maxWidth: isTablet ? 600 : '100%',
|
||||||
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
titleLogo: {
|
titleLogo: {
|
||||||
width: width * 0.75,
|
width: width * 0.75,
|
||||||
height: 90,
|
height: 90,
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
heroTitle: {
|
heroTitle: {
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
|
|
@ -1106,6 +1116,8 @@ const styles = StyleSheet.create({
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
marginBottom: 14,
|
marginBottom: 14,
|
||||||
gap: 6,
|
gap: 6,
|
||||||
|
maxWidth: isTablet ? 600 : '100%',
|
||||||
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
genreText: {
|
genreText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -1125,6 +1137,8 @@ const styles = StyleSheet.create({
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
maxWidth: isTablet ? 600 : '100%',
|
||||||
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
actionButton: {
|
actionButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -1177,6 +1191,8 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: 36,
|
minHeight: 36,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
maxWidth: isTablet ? 600 : '100%',
|
||||||
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
progressGlassBackground: {
|
progressGlassBackground: {
|
||||||
width: '75%',
|
width: '75%',
|
||||||
|
|
@ -1441,6 +1457,112 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Tablet-specific styles
|
||||||
|
tabletActionButtons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
maxWidth: 600,
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
tabletPlayButton: {
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
borderRadius: 32,
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
tabletPlayButtonText: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
tabletInfoButton: {
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
borderRadius: 28,
|
||||||
|
minWidth: 140,
|
||||||
|
},
|
||||||
|
tabletInfoButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
tabletIconButton: {
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
borderRadius: 30,
|
||||||
|
},
|
||||||
|
tabletHeroTitle: {
|
||||||
|
fontSize: 36,
|
||||||
|
fontWeight: '900',
|
||||||
|
marginBottom: 12,
|
||||||
|
textShadowColor: 'rgba(0,0,0,0.8)',
|
||||||
|
textShadowOffset: { width: 0, height: 2 },
|
||||||
|
textShadowRadius: 4,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 42,
|
||||||
|
},
|
||||||
|
tabletTitleLogo: {
|
||||||
|
width: width * 0.5,
|
||||||
|
height: 120,
|
||||||
|
alignSelf: 'center',
|
||||||
|
maxWidth: 400,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
tabletGenreContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 20,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
tabletGenreText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
opacity: 0.9,
|
||||||
|
},
|
||||||
|
tabletGenreDot: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
opacity: 0.6,
|
||||||
|
marginHorizontal: 4,
|
||||||
|
},
|
||||||
|
tabletWatchProgressContainer: {
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 8,
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: 44,
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
tabletProgressGlassBackground: {
|
||||||
|
width: '60%',
|
||||||
|
maxWidth: 500,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255,255,255,0.1)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
tabletWatchProgressMainText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
tabletWatchProgressSubText: {
|
||||||
|
fontSize: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
opacity: 0.8,
|
||||||
|
marginBottom: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default HeroSection;
|
export default HeroSection;
|
||||||
|
|
@ -3,7 +3,7 @@ import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { FlashList } from '@shopify/flash-list';
|
import { FlashList, FlashListRef } from '@shopify/flash-list';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { useSettings } from '../../hooks/useSettings';
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
import { Episode } from '../../types/metadata';
|
import { Episode } from '../../types/metadata';
|
||||||
|
|
@ -47,7 +47,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
||||||
|
|
||||||
// Add refs for the scroll views
|
// Add refs for the scroll views
|
||||||
const seasonScrollViewRef = useRef<ScrollView | null>(null);
|
const seasonScrollViewRef = useRef<ScrollView | null>(null);
|
||||||
const episodeScrollViewRef = useRef<ScrollView | null>(null);
|
const episodeScrollViewRef = useRef<FlashListRef<Episode>>(null);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -224,15 +224,19 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
||||||
const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b);
|
const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.seasonSelectorWrapper}>
|
<View style={[styles.seasonSelectorWrapper, isTablet && styles.seasonSelectorWrapperTablet]}>
|
||||||
<Text style={[styles.seasonSelectorTitle, { color: currentTheme.colors.highEmphasis }]}>Seasons</Text>
|
<Text style={[
|
||||||
|
styles.seasonSelectorTitle,
|
||||||
|
isTablet && styles.seasonSelectorTitleTablet,
|
||||||
|
{ color: currentTheme.colors.highEmphasis }
|
||||||
|
]}>Seasons</Text>
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={seasonScrollViewRef as React.RefObject<FlatList<any>>}
|
ref={seasonScrollViewRef as React.RefObject<FlatList<any>>}
|
||||||
data={seasons}
|
data={seasons}
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
style={styles.seasonSelectorContainer}
|
style={styles.seasonSelectorContainer}
|
||||||
contentContainerStyle={styles.seasonSelectorContent}
|
contentContainerStyle={[styles.seasonSelectorContent, isTablet && styles.seasonSelectorContentTablet]}
|
||||||
initialNumToRender={5}
|
initialNumToRender={5}
|
||||||
maxToRenderPerBatch={5}
|
maxToRenderPerBatch={5}
|
||||||
windowSize={3}
|
windowSize={3}
|
||||||
|
|
@ -251,18 +255,23 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
||||||
key={season}
|
key={season}
|
||||||
style={[
|
style={[
|
||||||
styles.seasonButton,
|
styles.seasonButton,
|
||||||
|
isTablet && styles.seasonButtonTablet,
|
||||||
selectedSeason === season && [styles.selectedSeasonButton, { borderColor: currentTheme.colors.primary }]
|
selectedSeason === season && [styles.selectedSeasonButton, { borderColor: currentTheme.colors.primary }]
|
||||||
]}
|
]}
|
||||||
onPress={() => onSeasonChange(season)}
|
onPress={() => onSeasonChange(season)}
|
||||||
>
|
>
|
||||||
<View style={styles.seasonPosterContainer}>
|
<View style={[styles.seasonPosterContainer, isTablet && styles.seasonPosterContainerTablet]}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: seasonPoster }}
|
source={{ uri: seasonPoster }}
|
||||||
style={styles.seasonPoster}
|
style={styles.seasonPoster}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
/>
|
/>
|
||||||
{selectedSeason === season && (
|
{selectedSeason === season && (
|
||||||
<View style={[styles.selectedSeasonIndicator, { backgroundColor: currentTheme.colors.primary }]} />
|
<View style={[
|
||||||
|
styles.selectedSeasonIndicator,
|
||||||
|
isTablet && styles.selectedSeasonIndicatorTablet,
|
||||||
|
{ backgroundColor: currentTheme.colors.primary }
|
||||||
|
]} />
|
||||||
)}
|
)}
|
||||||
{/* Show episode count badge, including when there are no episodes */}
|
{/* Show episode count badge, including when there are no episodes */}
|
||||||
<View style={[styles.episodeCountBadge, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
<View style={[styles.episodeCountBadge, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||||
|
|
@ -274,8 +283,13 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.seasonButtonText,
|
styles.seasonButtonText,
|
||||||
|
isTablet && styles.seasonButtonTextTablet,
|
||||||
{ color: currentTheme.colors.mediumEmphasis },
|
{ color: currentTheme.colors.mediumEmphasis },
|
||||||
selectedSeason === season && [styles.selectedSeasonButtonText, { color: currentTheme.colors.primary }]
|
selectedSeason === season && [
|
||||||
|
styles.selectedSeasonButtonText,
|
||||||
|
isTablet && styles.selectedSeasonButtonTextTablet,
|
||||||
|
{ color: currentTheme.colors.primary }
|
||||||
|
]
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
Season {season}
|
Season {season}
|
||||||
|
|
@ -536,19 +550,19 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
||||||
style={styles.episodeGradient}
|
style={styles.episodeGradient}
|
||||||
>
|
>
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
<View style={styles.episodeContent}>
|
<View style={[styles.episodeContent, isTablet && styles.episodeContentTablet]}>
|
||||||
{/* Episode Number Badge */}
|
{/* Episode Number Badge */}
|
||||||
<View style={styles.episodeNumberBadgeHorizontal}>
|
<View style={[styles.episodeNumberBadgeHorizontal, isTablet && styles.episodeNumberBadgeHorizontalTablet]}>
|
||||||
<Text style={styles.episodeNumberHorizontal}>{episodeString}</Text>
|
<Text style={[styles.episodeNumberHorizontal, isTablet && styles.episodeNumberHorizontalTablet]}>{episodeString}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Episode Title */}
|
{/* Episode Title */}
|
||||||
<Text style={styles.episodeTitleHorizontal} numberOfLines={2}>
|
<Text style={[styles.episodeTitleHorizontal, isTablet && styles.episodeTitleHorizontalTablet]} numberOfLines={2}>
|
||||||
{episode.name}
|
{episode.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Episode Description */}
|
{/* Episode Description */}
|
||||||
<Text style={styles.episodeDescriptionHorizontal} numberOfLines={3}>
|
<Text style={[styles.episodeDescriptionHorizontal, isTablet && styles.episodeDescriptionHorizontalTablet]} numberOfLines={3}>
|
||||||
{episode.overview || 'No description available'}
|
{episode.overview || 'No description available'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
|
@ -636,7 +650,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
||||||
(settings?.episodeLayoutStyle === 'horizontal') ? (
|
(settings?.episodeLayoutStyle === 'horizontal') ? (
|
||||||
// Horizontal Layout (Netflix-style)
|
// Horizontal Layout (Netflix-style)
|
||||||
<FlashList
|
<FlashList
|
||||||
ref={episodeScrollViewRef as React.RefObject<FlatList<any>>}
|
ref={episodeScrollViewRef}
|
||||||
data={currentSeasonEpisodes}
|
data={currentSeasonEpisodes}
|
||||||
renderItem={({ item: episode, index }) => (
|
renderItem={({ item: episode, index }) => (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
|
@ -653,18 +667,12 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
||||||
keyExtractor={episode => episode.id.toString()}
|
keyExtractor={episode => episode.id.toString()}
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.episodeListContentHorizontal}
|
contentContainerStyle={isTablet ? styles.episodeListContentHorizontalTablet : styles.episodeListContentHorizontal}
|
||||||
decelerationRate="fast"
|
|
||||||
snapToInterval={isTablet ? width * 0.4 + 16 : width * 0.85 + 16}
|
|
||||||
snapToAlignment="start"
|
|
||||||
initialNumToRender={3}
|
|
||||||
maxToRenderPerBatch={3}
|
|
||||||
windowSize={5}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
// Vertical Layout (Traditional)
|
// Vertical Layout (Traditional)
|
||||||
<FlashList
|
<FlashList
|
||||||
ref={episodeScrollViewRef as React.RefObject<FlatList<any>>}
|
ref={episodeScrollViewRef}
|
||||||
data={currentSeasonEpisodes}
|
data={currentSeasonEpisodes}
|
||||||
renderItem={({ item: episode, index }) => (
|
renderItem={({ item: episode, index }) => (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
|
@ -675,9 +683,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
keyExtractor={episode => episode.id.toString()}
|
keyExtractor={episode => episode.id.toString()}
|
||||||
estimatedItemSize={136}
|
|
||||||
contentContainerStyle={isTablet ? styles.episodeListContentVerticalTablet : styles.episodeListContentVertical}
|
contentContainerStyle={isTablet ? styles.episodeListContentVerticalTablet : styles.episodeListContentVertical}
|
||||||
numColumns={isTablet ? 2 : 1}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
@ -750,7 +756,7 @@ const styles = StyleSheet.create({
|
||||||
episodeCardVerticalTablet: {
|
episodeCardVerticalTablet: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
height: 140,
|
height: 160,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
episodeImageContainer: {
|
episodeImageContainer: {
|
||||||
|
|
@ -759,8 +765,8 @@ const styles = StyleSheet.create({
|
||||||
height: 120,
|
height: 120,
|
||||||
},
|
},
|
||||||
episodeImageContainerTablet: {
|
episodeImageContainerTablet: {
|
||||||
width: 140,
|
width: 160,
|
||||||
height: 140,
|
height: 160,
|
||||||
},
|
},
|
||||||
episodeImage: {
|
episodeImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -891,12 +897,17 @@ const styles = StyleSheet.create({
|
||||||
paddingLeft: 16,
|
paddingLeft: 16,
|
||||||
paddingRight: 16,
|
paddingRight: 16,
|
||||||
},
|
},
|
||||||
|
episodeListContentHorizontalTablet: {
|
||||||
|
paddingLeft: 24,
|
||||||
|
paddingRight: 24,
|
||||||
|
},
|
||||||
episodeCardWrapperHorizontal: {
|
episodeCardWrapperHorizontal: {
|
||||||
width: Dimensions.get('window').width * 0.85,
|
width: Dimensions.get('window').width * 0.75,
|
||||||
marginRight: 16,
|
marginRight: 16,
|
||||||
},
|
},
|
||||||
episodeCardWrapperHorizontalTablet: {
|
episodeCardWrapperHorizontalTablet: {
|
||||||
width: Dimensions.get('window').width * 0.4,
|
width: Dimensions.get('window').width * 0.4,
|
||||||
|
marginRight: 20,
|
||||||
},
|
},
|
||||||
episodeCardHorizontal: {
|
episodeCardHorizontal: {
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
|
|
@ -914,7 +925,11 @@ const styles = StyleSheet.create({
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
episodeCardHorizontalTablet: {
|
episodeCardHorizontalTablet: {
|
||||||
height: 180,
|
height: 260,
|
||||||
|
borderRadius: 20,
|
||||||
|
elevation: 12,
|
||||||
|
shadowOpacity: 0.4,
|
||||||
|
shadowRadius: 16,
|
||||||
},
|
},
|
||||||
episodeBackgroundImage: {
|
episodeBackgroundImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -934,6 +949,10 @@ const styles = StyleSheet.create({
|
||||||
padding: 12,
|
padding: 12,
|
||||||
paddingBottom: 16,
|
paddingBottom: 16,
|
||||||
},
|
},
|
||||||
|
episodeContentTablet: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
episodeNumberBadgeHorizontal: {
|
episodeNumberBadgeHorizontal: {
|
||||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
|
|
@ -942,6 +961,14 @@ const styles = StyleSheet.create({
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
alignSelf: 'flex-start',
|
alignSelf: 'flex-start',
|
||||||
},
|
},
|
||||||
|
episodeNumberBadgeHorizontalTablet: {
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 8,
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
},
|
||||||
episodeNumberHorizontal: {
|
episodeNumberHorizontal: {
|
||||||
color: 'rgba(255,255,255,0.8)',
|
color: 'rgba(255,255,255,0.8)',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
|
@ -950,6 +977,14 @@ const styles = StyleSheet.create({
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
marginBottom: 2,
|
marginBottom: 2,
|
||||||
},
|
},
|
||||||
|
episodeNumberHorizontalTablet: {
|
||||||
|
color: 'rgba(255,255,255,0.9)',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: 1.0,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
episodeTitleHorizontal: {
|
episodeTitleHorizontal: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
|
|
@ -958,6 +993,14 @@ const styles = StyleSheet.create({
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
|
episodeTitleHorizontalTablet: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '800',
|
||||||
|
letterSpacing: -0.4,
|
||||||
|
marginBottom: 6,
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
episodeDescriptionHorizontal: {
|
episodeDescriptionHorizontal: {
|
||||||
color: 'rgba(255,255,255,0.85)',
|
color: 'rgba(255,255,255,0.85)',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -965,6 +1008,13 @@ const styles = StyleSheet.create({
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
},
|
},
|
||||||
|
episodeDescriptionHorizontalTablet: {
|
||||||
|
color: 'rgba(255,255,255,0.9)',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 18,
|
||||||
|
marginBottom: 10,
|
||||||
|
opacity: 0.95,
|
||||||
|
},
|
||||||
episodeMetadataRowHorizontal: {
|
episodeMetadataRowHorizontal: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
@ -1027,22 +1077,39 @@ const styles = StyleSheet.create({
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
},
|
},
|
||||||
|
seasonSelectorWrapperTablet: {
|
||||||
|
marginBottom: 24,
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
},
|
||||||
seasonSelectorTitle: {
|
seasonSelectorTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
},
|
},
|
||||||
|
seasonSelectorTitleTablet: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
seasonSelectorContainer: {
|
seasonSelectorContainer: {
|
||||||
flexGrow: 0,
|
flexGrow: 0,
|
||||||
},
|
},
|
||||||
seasonSelectorContent: {
|
seasonSelectorContent: {
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
},
|
},
|
||||||
|
seasonSelectorContentTablet: {
|
||||||
|
paddingBottom: 12,
|
||||||
|
},
|
||||||
seasonButton: {
|
seasonButton: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 16,
|
marginRight: 16,
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
|
seasonButtonTablet: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: 20,
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
selectedSeasonButton: {
|
selectedSeasonButton: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
|
|
@ -1054,6 +1121,14 @@ const styles = StyleSheet.create({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
seasonPosterContainerTablet: {
|
||||||
|
position: 'relative',
|
||||||
|
width: 120,
|
||||||
|
height: 180,
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
seasonPoster: {
|
seasonPoster: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|
@ -1065,13 +1140,27 @@ const styles = StyleSheet.create({
|
||||||
right: 0,
|
right: 0,
|
||||||
height: 4,
|
height: 4,
|
||||||
},
|
},
|
||||||
|
selectedSeasonIndicatorTablet: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 6,
|
||||||
|
},
|
||||||
seasonButtonText: {
|
seasonButtonText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
seasonButtonTextTablet: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
selectedSeasonButtonText: {
|
selectedSeasonButtonText: {
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
|
selectedSeasonButtonTextTablet: {
|
||||||
|
fontWeight: '800',
|
||||||
|
},
|
||||||
episodeCountBadge: {
|
episodeCountBadge: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
|
|
|
||||||
|
|
@ -730,6 +730,10 @@ const PluginsScreen: React.FC = () => {
|
||||||
<View style={[styles.availableIndicator, { backgroundColor: colors.mediumGray }]}>
|
<View style={[styles.availableIndicator, { backgroundColor: colors.mediumGray }]}>
|
||||||
<Text style={styles.availableIndicatorText}>Disabled</Text>
|
<Text style={styles.availableIndicatorText}>Disabled</Text>
|
||||||
</View>
|
</View>
|
||||||
|
) : scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android') ? (
|
||||||
|
<View style={[styles.availableIndicator, { backgroundColor: '#ff9500' }]}>
|
||||||
|
<Text style={styles.availableIndicatorText}>Platform Disabled</Text>
|
||||||
|
</View>
|
||||||
) : !scraper.enabled && (
|
) : !scraper.enabled && (
|
||||||
<View style={styles.availableIndicator}>
|
<View style={styles.availableIndicator}>
|
||||||
<Text style={styles.availableIndicatorText}>Available</Text>
|
<Text style={styles.availableIndicatorText}>Available</Text>
|
||||||
|
|
@ -758,8 +762,8 @@ const PluginsScreen: React.FC = () => {
|
||||||
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
|
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
|
||||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||||
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false}
|
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
|
||||||
style={{ opacity: (!settings.enableLocalScrapers || scraper.manifestEnabled === false) ? 0.5 : 1 }}
|
style={{ opacity: (!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))) ? 0.5 : 1 }}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { Stream } from '../types/streams';
|
import { Stream } from '../types/streams';
|
||||||
import { cacheService } from './cacheService';
|
import { cacheService } from './cacheService';
|
||||||
|
|
@ -24,6 +25,8 @@ export interface ScraperInfo {
|
||||||
logo?: string;
|
logo?: string;
|
||||||
contentLanguage?: string[];
|
contentLanguage?: string[];
|
||||||
manifestEnabled?: boolean; // Whether the scraper is enabled in the manifest
|
manifestEnabled?: boolean; // Whether the scraper is enabled in the manifest
|
||||||
|
supportedPlatforms?: ('ios' | 'android')[]; // Platforms where this scraper is supported
|
||||||
|
disabledPlatforms?: ('ios' | 'android')[]; // Platforms where this scraper is disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalScraperResult {
|
export interface LocalScraperResult {
|
||||||
|
|
@ -181,6 +184,26 @@ class LocalScraperService {
|
||||||
return this.repositoryName || 'Plugins';
|
return this.repositoryName || 'Plugins';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a scraper is compatible with the current platform
|
||||||
|
private isPlatformCompatible(scraper: ScraperInfo): boolean {
|
||||||
|
const currentPlatform = Platform.OS as 'ios' | 'android';
|
||||||
|
|
||||||
|
// If disabledPlatforms is specified and includes current platform, scraper is not compatible
|
||||||
|
if (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(currentPlatform)) {
|
||||||
|
logger.log(`[LocalScraperService] Scraper ${scraper.name} is disabled on ${currentPlatform}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If supportedPlatforms is specified and doesn't include current platform, scraper is not compatible
|
||||||
|
if (scraper.supportedPlatforms && !scraper.supportedPlatforms.includes(currentPlatform)) {
|
||||||
|
logger.log(`[LocalScraperService] Scraper ${scraper.name} is not supported on ${currentPlatform}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If neither supportedPlatforms nor disabledPlatforms is specified, or current platform is supported
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch and install scrapers from repository
|
// Fetch and install scrapers from repository
|
||||||
async refreshRepository(): Promise<void> {
|
async refreshRepository(): Promise<void> {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
@ -244,7 +267,21 @@ class LocalScraperService {
|
||||||
|
|
||||||
// Download and install each scraper from manifest
|
// Download and install each scraper from manifest
|
||||||
for (const scraperInfo of manifest.scrapers) {
|
for (const scraperInfo of manifest.scrapers) {
|
||||||
await this.downloadScraper(scraperInfo);
|
const isPlatformCompatible = this.isPlatformCompatible(scraperInfo);
|
||||||
|
|
||||||
|
if (isPlatformCompatible) {
|
||||||
|
// Download/update the scraper (downloadScraper handles force disabling based on manifest.enabled)
|
||||||
|
await this.downloadScraper(scraperInfo);
|
||||||
|
} else {
|
||||||
|
logger.log('[LocalScraperService] Skipping platform-incompatible scraper:', scraperInfo.name);
|
||||||
|
// Remove if it was previously installed but is now platform-incompatible
|
||||||
|
if (this.installedScrapers.has(scraperInfo.id)) {
|
||||||
|
logger.log('[LocalScraperService] Removing platform-incompatible scraper:', scraperInfo.name);
|
||||||
|
this.installedScrapers.delete(scraperInfo.id);
|
||||||
|
this.scraperCode.delete(scraperInfo.id);
|
||||||
|
await AsyncStorage.removeItem(`scraper-code-${scraperInfo.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.saveInstalledScrapers();
|
await this.saveInstalledScrapers();
|
||||||
|
|
@ -269,9 +306,18 @@ class LocalScraperService {
|
||||||
const scraperCode = response.data;
|
const scraperCode = response.data;
|
||||||
|
|
||||||
// Store scraper info and code
|
// Store scraper info and code
|
||||||
|
const existingScraper = this.installedScrapers.get(scraperInfo.id);
|
||||||
|
const isPlatformCompatible = this.isPlatformCompatible(scraperInfo);
|
||||||
|
|
||||||
const updatedScraperInfo = {
|
const updatedScraperInfo = {
|
||||||
...scraperInfo,
|
...scraperInfo,
|
||||||
enabled: this.installedScrapers.get(scraperInfo.id)?.enabled ?? true // Preserve enabled state
|
// Store the manifest's enabled state separately
|
||||||
|
manifestEnabled: scraperInfo.enabled,
|
||||||
|
// Force disable if:
|
||||||
|
// 1. Manifest says enabled: false (globally disabled)
|
||||||
|
// 2. Platform incompatible
|
||||||
|
// Otherwise, preserve user's enabled state or default to false
|
||||||
|
enabled: scraperInfo.enabled && isPlatformCompatible ? (existingScraper?.enabled ?? false) : false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure contentLanguage is an array (migration for older scrapers)
|
// Ensure contentLanguage is an array (migration for older scrapers)
|
||||||
|
|
@ -370,22 +416,24 @@ class LocalScraperService {
|
||||||
this.repositoryName = manifest.name;
|
this.repositoryName = manifest.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return scrapers from manifest, respecting manifest's enabled field
|
// Return scrapers from manifest, respecting manifest's enabled field and platform compatibility
|
||||||
const availableScrapers = manifest.scrapers.map(scraperInfo => {
|
const availableScrapers = manifest.scrapers
|
||||||
const installedScraper = this.installedScrapers.get(scraperInfo.id);
|
.filter(scraperInfo => this.isPlatformCompatible(scraperInfo))
|
||||||
|
.map(scraperInfo => {
|
||||||
// Create a copy with manifest data
|
const installedScraper = this.installedScrapers.get(scraperInfo.id);
|
||||||
const scraperWithManifestData = {
|
|
||||||
...scraperInfo,
|
// Create a copy with manifest data
|
||||||
// Store the manifest's enabled state separately
|
const scraperWithManifestData = {
|
||||||
manifestEnabled: scraperInfo.enabled,
|
...scraperInfo,
|
||||||
// If manifest says enabled: false, scraper cannot be enabled
|
// Store the manifest's enabled state separately
|
||||||
// If manifest says enabled: true, use installed state or default to false
|
manifestEnabled: scraperInfo.enabled,
|
||||||
enabled: scraperInfo.enabled ? (installedScraper?.enabled ?? false) : false
|
// If manifest says enabled: false, scraper cannot be enabled
|
||||||
};
|
// If manifest says enabled: true, use installed state or default to false
|
||||||
|
enabled: scraperInfo.enabled ? (installedScraper?.enabled ?? false) : false
|
||||||
return scraperWithManifestData;
|
};
|
||||||
});
|
|
||||||
|
return scraperWithManifestData;
|
||||||
|
});
|
||||||
|
|
||||||
logger.log('[LocalScraperService] Found', availableScrapers.length, 'available scrapers in repository');
|
logger.log('[LocalScraperService] Found', availableScrapers.length, 'available scrapers in repository');
|
||||||
|
|
||||||
|
|
@ -409,6 +457,12 @@ class LocalScraperService {
|
||||||
|
|
||||||
const scraper = this.installedScrapers.get(scraperId);
|
const scraper = this.installedScrapers.get(scraperId);
|
||||||
if (scraper) {
|
if (scraper) {
|
||||||
|
// Prevent enabling if manifest has disabled it or if platform-incompatible
|
||||||
|
if (enabled && (scraper.manifestEnabled === false || !this.isPlatformCompatible(scraper))) {
|
||||||
|
logger.log('[LocalScraperService] Cannot enable scraper', scraperId, '- disabled in manifest or platform-incompatible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
scraper.enabled = enabled;
|
scraper.enabled = enabled;
|
||||||
this.installedScrapers.set(scraperId, scraper);
|
this.installedScrapers.set(scraperId, scraper);
|
||||||
await this.saveInstalledScrapers();
|
await this.saveInstalledScrapers();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue