mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +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)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
maxWidth: '70%',
|
||||
textAlign: 'center',
|
||||
},
|
||||
tabletButtons: {
|
||||
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';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
||||
// Ultra-optimized animation constants
|
||||
const PARALLAX_FACTOR = 0.3;
|
||||
|
|
@ -240,9 +241,9 @@ const ActionButtons = memo(({
|
|||
}, [isWatched, playButtonText, type, watchProgress, groupedEpisodes]);
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.actionButtons, animatedStyle]}>
|
||||
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
|
||||
<TouchableOpacity
|
||||
style={playButtonStyle}
|
||||
style={[playButtonStyle, isTablet && styles.tabletPlayButton]}
|
||||
onPress={handleShowStreams}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
|
|
@ -253,14 +254,14 @@ const ActionButtons = memo(({
|
|||
}
|
||||
return playButtonText === 'Resume' ? 'play-circle-outline' : 'play-arrow';
|
||||
})()}
|
||||
size={24}
|
||||
size={isTablet ? 28 : 24}
|
||||
color={isWatched && type === 'movie' ? "#fff" : "#000"}
|
||||
/>
|
||||
<Text style={playButtonTextStyle}>{finalPlayButtonText}</Text>
|
||||
<Text style={[playButtonTextStyle, isTablet && styles.tabletPlayButtonText]}>{finalPlayButtonText}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.infoButton]}
|
||||
style={[styles.actionButton, styles.infoButton, isTablet && styles.tabletInfoButton]}
|
||||
onPress={toggleLibrary}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
|
|
@ -271,17 +272,17 @@ const ActionButtons = memo(({
|
|||
)}
|
||||
<MaterialIcons
|
||||
name={inLibrary ? 'bookmark' : 'bookmark-border'}
|
||||
size={24}
|
||||
size={isTablet ? 28 : 24}
|
||||
color={currentTheme.colors.white}
|
||||
/>
|
||||
<Text style={styles.infoButtonText}>
|
||||
<Text style={[styles.infoButtonText, isTablet && styles.tabletInfoButtonText]}>
|
||||
{inLibrary ? 'Saved' : 'Save'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{type === 'series' && (
|
||||
<TouchableOpacity
|
||||
style={styles.iconButton}
|
||||
style={[styles.iconButton, isTablet && styles.tabletIconButton]}
|
||||
onPress={handleRatingsPress}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
|
|
@ -292,7 +293,7 @@ const ActionButtons = memo(({
|
|||
)}
|
||||
<MaterialIcons
|
||||
name="assessment"
|
||||
size={24}
|
||||
size={isTablet ? 28 : 24}
|
||||
color={currentTheme.colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
|
@ -534,9 +535,9 @@ const WatchProgressDisplay = memo(({
|
|||
const isCompleted = progressData.isWatched || progressData.progressPercent >= 85;
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.watchProgressContainer, animatedStyle]}>
|
||||
<Animated.View style={[isTablet ? styles.tabletWatchProgressContainer : styles.watchProgressContainer, animatedStyle]}>
|
||||
{/* Glass morphism background with entrance animation */}
|
||||
<Animated.View style={[styles.progressGlassBackground, progressBoxAnimatedStyle]}>
|
||||
<Animated.View style={[isTablet ? styles.tabletProgressGlassBackground : styles.progressGlassBackground, progressBoxAnimatedStyle]}>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<ExpoBlurView intensity={20} style={styles.blurBackground} tint="dark" />
|
||||
) : (
|
||||
|
|
@ -580,9 +581,9 @@ const WatchProgressDisplay = memo(({
|
|||
{/* Enhanced text container with better typography */}
|
||||
<View style={styles.watchProgressTextContainer}>
|
||||
<View style={styles.progressInfoMain}>
|
||||
<Text style={[styles.watchProgressMainText, {
|
||||
<Text style={[isTablet ? styles.tabletWatchProgressMainText : styles.watchProgressMainText, {
|
||||
color: isCompleted ? '#00ff88' : currentTheme.colors.white,
|
||||
fontSize: isCompleted ? 13 : 12,
|
||||
fontSize: isCompleted ? (isTablet ? 15 : 13) : (isTablet ? 14 : 12),
|
||||
fontWeight: isCompleted ? '700' : '600'
|
||||
}]}>
|
||||
{progressData.displayText}
|
||||
|
|
@ -590,7 +591,7 @@ const WatchProgressDisplay = memo(({
|
|||
|
||||
</View>
|
||||
|
||||
<Text style={[styles.watchProgressSubText, {
|
||||
<Text style={[isTablet ? styles.tabletWatchProgressSubText : styles.watchProgressSubText, {
|
||||
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
|
||||
}]}>
|
||||
{progressData.episodeInfo} • Last watched {progressData.formattedTime}
|
||||
|
|
@ -839,11 +840,11 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
entering={FadeIn.duration(400).delay(200 + index * 100)}
|
||||
style={{ flexDirection: 'row', alignItems: 'center' }}
|
||||
>
|
||||
<Text style={[styles.genreText, { color: themeColors.text }]}>
|
||||
<Text style={[isTablet ? styles.tabletGenreText : styles.genreText, { color: themeColors.text }]}>
|
||||
{genreName}
|
||||
</Text>
|
||||
{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>
|
||||
));
|
||||
|
|
@ -966,14 +967,14 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
style={styles.bottomFadeGradient}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<View style={styles.heroContent}>
|
||||
<View style={[styles.heroContent, isTablet && { maxWidth: 800, alignSelf: 'center' }]}>
|
||||
{/* Optimized Title/Logo */}
|
||||
<View style={styles.logoContainer}>
|
||||
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
|
||||
{shouldLoadSecondaryData && metadata.logo && !logoLoadError ? (
|
||||
<Image
|
||||
source={{ uri: metadata.logo }}
|
||||
style={styles.titleLogo}
|
||||
style={isTablet ? styles.tabletTitleLogo : styles.titleLogo}
|
||||
contentFit="contain"
|
||||
transition={150}
|
||||
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}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -999,7 +1000,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
|
||||
{/* Optimized genre display with lazy loading */}
|
||||
{shouldLoadSecondaryData && genreElements && (
|
||||
<View style={styles.genreContainer}>
|
||||
<View style={isTablet ? styles.tabletGenreContainer : styles.genreContainer}>
|
||||
{genreElements}
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -1041,7 +1042,7 @@ const styles = StyleSheet.create({
|
|||
backButtonContainer: {
|
||||
position: 'absolute',
|
||||
top: Platform.OS === 'android' ? 40 : 50,
|
||||
left: 16,
|
||||
left: isTablet ? 32 : 16,
|
||||
zIndex: 10,
|
||||
},
|
||||
backButton: {
|
||||
|
|
@ -1066,9 +1067,9 @@ const styles = StyleSheet.create({
|
|||
zIndex: 1,
|
||||
},
|
||||
heroContent: {
|
||||
padding: 16,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
padding: isTablet ? 32 : 16,
|
||||
paddingTop: isTablet ? 16 : 8,
|
||||
paddingBottom: isTablet ? 16 : 8,
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
},
|
||||
|
|
@ -1077,16 +1078,25 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
marginBottom: 4,
|
||||
flex: 0,
|
||||
display: 'flex',
|
||||
maxWidth: isTablet ? 600 : '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
titleLogoContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
flex: 0,
|
||||
display: 'flex',
|
||||
maxWidth: isTablet ? 600 : '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
titleLogo: {
|
||||
width: width * 0.75,
|
||||
height: 90,
|
||||
alignSelf: 'center',
|
||||
textAlign: 'center',
|
||||
},
|
||||
heroTitle: {
|
||||
fontSize: 26,
|
||||
|
|
@ -1106,6 +1116,8 @@ const styles = StyleSheet.create({
|
|||
marginTop: 6,
|
||||
marginBottom: 14,
|
||||
gap: 6,
|
||||
maxWidth: isTablet ? 600 : '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
genreText: {
|
||||
fontSize: 12,
|
||||
|
|
@ -1125,6 +1137,8 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
maxWidth: isTablet ? 600 : '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
actionButton: {
|
||||
flexDirection: 'row',
|
||||
|
|
@ -1177,6 +1191,8 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
minHeight: 36,
|
||||
position: 'relative',
|
||||
maxWidth: isTablet ? 600 : '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
progressGlassBackground: {
|
||||
width: '75%',
|
||||
|
|
@ -1441,6 +1457,112 @@ const styles = StyleSheet.create({
|
|||
alignItems: '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;
|
||||
|
|
@ -3,7 +3,7 @@ import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator
|
|||
import { Image } from 'expo-image';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
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 { useSettings } from '../../hooks/useSettings';
|
||||
import { Episode } from '../../types/metadata';
|
||||
|
|
@ -47,7 +47,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
|
||||
// Add refs for the scroll views
|
||||
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);
|
||||
|
||||
return (
|
||||
<View style={styles.seasonSelectorWrapper}>
|
||||
<Text style={[styles.seasonSelectorTitle, { color: currentTheme.colors.highEmphasis }]}>Seasons</Text>
|
||||
<View style={[styles.seasonSelectorWrapper, isTablet && styles.seasonSelectorWrapperTablet]}>
|
||||
<Text style={[
|
||||
styles.seasonSelectorTitle,
|
||||
isTablet && styles.seasonSelectorTitleTablet,
|
||||
{ color: currentTheme.colors.highEmphasis }
|
||||
]}>Seasons</Text>
|
||||
<FlatList
|
||||
ref={seasonScrollViewRef as React.RefObject<FlatList<any>>}
|
||||
data={seasons}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.seasonSelectorContainer}
|
||||
contentContainerStyle={styles.seasonSelectorContent}
|
||||
contentContainerStyle={[styles.seasonSelectorContent, isTablet && styles.seasonSelectorContentTablet]}
|
||||
initialNumToRender={5}
|
||||
maxToRenderPerBatch={5}
|
||||
windowSize={3}
|
||||
|
|
@ -251,18 +255,23 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
key={season}
|
||||
style={[
|
||||
styles.seasonButton,
|
||||
isTablet && styles.seasonButtonTablet,
|
||||
selectedSeason === season && [styles.selectedSeasonButton, { borderColor: currentTheme.colors.primary }]
|
||||
]}
|
||||
onPress={() => onSeasonChange(season)}
|
||||
>
|
||||
<View style={styles.seasonPosterContainer}>
|
||||
<View style={[styles.seasonPosterContainer, isTablet && styles.seasonPosterContainerTablet]}>
|
||||
<Image
|
||||
source={{ uri: seasonPoster }}
|
||||
style={styles.seasonPoster}
|
||||
contentFit="cover"
|
||||
/>
|
||||
{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 */}
|
||||
<View style={[styles.episodeCountBadge, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
|
|
@ -274,8 +283,13 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
<Text
|
||||
style={[
|
||||
styles.seasonButtonText,
|
||||
isTablet && styles.seasonButtonTextTablet,
|
||||
{ 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}
|
||||
|
|
@ -536,19 +550,19 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
style={styles.episodeGradient}
|
||||
>
|
||||
{/* Content Container */}
|
||||
<View style={styles.episodeContent}>
|
||||
<View style={[styles.episodeContent, isTablet && styles.episodeContentTablet]}>
|
||||
{/* Episode Number Badge */}
|
||||
<View style={styles.episodeNumberBadgeHorizontal}>
|
||||
<Text style={styles.episodeNumberHorizontal}>{episodeString}</Text>
|
||||
<View style={[styles.episodeNumberBadgeHorizontal, isTablet && styles.episodeNumberBadgeHorizontalTablet]}>
|
||||
<Text style={[styles.episodeNumberHorizontal, isTablet && styles.episodeNumberHorizontalTablet]}>{episodeString}</Text>
|
||||
</View>
|
||||
|
||||
{/* Episode Title */}
|
||||
<Text style={styles.episodeTitleHorizontal} numberOfLines={2}>
|
||||
<Text style={[styles.episodeTitleHorizontal, isTablet && styles.episodeTitleHorizontalTablet]} numberOfLines={2}>
|
||||
{episode.name}
|
||||
</Text>
|
||||
|
||||
{/* Episode Description */}
|
||||
<Text style={styles.episodeDescriptionHorizontal} numberOfLines={3}>
|
||||
<Text style={[styles.episodeDescriptionHorizontal, isTablet && styles.episodeDescriptionHorizontalTablet]} numberOfLines={3}>
|
||||
{episode.overview || 'No description available'}
|
||||
</Text>
|
||||
|
||||
|
|
@ -636,7 +650,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
(settings?.episodeLayoutStyle === 'horizontal') ? (
|
||||
// Horizontal Layout (Netflix-style)
|
||||
<FlashList
|
||||
ref={episodeScrollViewRef as React.RefObject<FlatList<any>>}
|
||||
ref={episodeScrollViewRef}
|
||||
data={currentSeasonEpisodes}
|
||||
renderItem={({ item: episode, index }) => (
|
||||
<Animated.View
|
||||
|
|
@ -653,18 +667,12 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
keyExtractor={episode => episode.id.toString()}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.episodeListContentHorizontal}
|
||||
decelerationRate="fast"
|
||||
snapToInterval={isTablet ? width * 0.4 + 16 : width * 0.85 + 16}
|
||||
snapToAlignment="start"
|
||||
initialNumToRender={3}
|
||||
maxToRenderPerBatch={3}
|
||||
windowSize={5}
|
||||
contentContainerStyle={isTablet ? styles.episodeListContentHorizontalTablet : styles.episodeListContentHorizontal}
|
||||
/>
|
||||
) : (
|
||||
// Vertical Layout (Traditional)
|
||||
<FlashList
|
||||
ref={episodeScrollViewRef as React.RefObject<FlatList<any>>}
|
||||
ref={episodeScrollViewRef}
|
||||
data={currentSeasonEpisodes}
|
||||
renderItem={({ item: episode, index }) => (
|
||||
<Animated.View
|
||||
|
|
@ -675,9 +683,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
|
|||
</Animated.View>
|
||||
)}
|
||||
keyExtractor={episode => episode.id.toString()}
|
||||
estimatedItemSize={136}
|
||||
contentContainerStyle={isTablet ? styles.episodeListContentVerticalTablet : styles.episodeListContentVertical}
|
||||
numColumns={isTablet ? 2 : 1}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
@ -750,7 +756,7 @@ const styles = StyleSheet.create({
|
|||
episodeCardVerticalTablet: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
height: 140,
|
||||
height: 160,
|
||||
marginBottom: 16,
|
||||
},
|
||||
episodeImageContainer: {
|
||||
|
|
@ -759,8 +765,8 @@ const styles = StyleSheet.create({
|
|||
height: 120,
|
||||
},
|
||||
episodeImageContainerTablet: {
|
||||
width: 140,
|
||||
height: 140,
|
||||
width: 160,
|
||||
height: 160,
|
||||
},
|
||||
episodeImage: {
|
||||
width: '100%',
|
||||
|
|
@ -891,12 +897,17 @@ const styles = StyleSheet.create({
|
|||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
},
|
||||
episodeListContentHorizontalTablet: {
|
||||
paddingLeft: 24,
|
||||
paddingRight: 24,
|
||||
},
|
||||
episodeCardWrapperHorizontal: {
|
||||
width: Dimensions.get('window').width * 0.85,
|
||||
width: Dimensions.get('window').width * 0.75,
|
||||
marginRight: 16,
|
||||
},
|
||||
episodeCardWrapperHorizontalTablet: {
|
||||
width: Dimensions.get('window').width * 0.4,
|
||||
marginRight: 20,
|
||||
},
|
||||
episodeCardHorizontal: {
|
||||
borderRadius: 16,
|
||||
|
|
@ -914,7 +925,11 @@ const styles = StyleSheet.create({
|
|||
backgroundColor: 'transparent',
|
||||
},
|
||||
episodeCardHorizontalTablet: {
|
||||
height: 180,
|
||||
height: 260,
|
||||
borderRadius: 20,
|
||||
elevation: 12,
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 16,
|
||||
},
|
||||
episodeBackgroundImage: {
|
||||
width: '100%',
|
||||
|
|
@ -934,6 +949,10 @@ const styles = StyleSheet.create({
|
|||
padding: 12,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
episodeContentTablet: {
|
||||
padding: 16,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
episodeNumberBadgeHorizontal: {
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
paddingHorizontal: 6,
|
||||
|
|
@ -942,6 +961,14 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 6,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
episodeNumberBadgeHorizontalTablet: {
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 6,
|
||||
marginBottom: 8,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
episodeNumberHorizontal: {
|
||||
color: 'rgba(255,255,255,0.8)',
|
||||
fontSize: 10,
|
||||
|
|
@ -950,6 +977,14 @@ const styles = StyleSheet.create({
|
|||
textTransform: 'uppercase',
|
||||
marginBottom: 2,
|
||||
},
|
||||
episodeNumberHorizontalTablet: {
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1.0,
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 2,
|
||||
},
|
||||
episodeTitleHorizontal: {
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
|
|
@ -958,6 +993,14 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 4,
|
||||
lineHeight: 18,
|
||||
},
|
||||
episodeTitleHorizontalTablet: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
letterSpacing: -0.4,
|
||||
marginBottom: 6,
|
||||
lineHeight: 22,
|
||||
},
|
||||
episodeDescriptionHorizontal: {
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
fontSize: 12,
|
||||
|
|
@ -965,6 +1008,13 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 8,
|
||||
opacity: 0.9,
|
||||
},
|
||||
episodeDescriptionHorizontalTablet: {
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
fontSize: 14,
|
||||
lineHeight: 18,
|
||||
marginBottom: 10,
|
||||
opacity: 0.95,
|
||||
},
|
||||
episodeMetadataRowHorizontal: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
@ -1027,22 +1077,39 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 20,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
seasonSelectorWrapperTablet: {
|
||||
marginBottom: 24,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
seasonSelectorTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 12,
|
||||
},
|
||||
seasonSelectorTitleTablet: {
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
marginBottom: 16,
|
||||
},
|
||||
seasonSelectorContainer: {
|
||||
flexGrow: 0,
|
||||
},
|
||||
seasonSelectorContent: {
|
||||
paddingBottom: 8,
|
||||
},
|
||||
seasonSelectorContentTablet: {
|
||||
paddingBottom: 12,
|
||||
},
|
||||
seasonButton: {
|
||||
alignItems: 'center',
|
||||
marginRight: 16,
|
||||
width: 100,
|
||||
},
|
||||
seasonButtonTablet: {
|
||||
alignItems: 'center',
|
||||
marginRight: 20,
|
||||
width: 120,
|
||||
},
|
||||
selectedSeasonButton: {
|
||||
opacity: 1,
|
||||
},
|
||||
|
|
@ -1054,6 +1121,14 @@ const styles = StyleSheet.create({
|
|||
overflow: 'hidden',
|
||||
marginBottom: 8,
|
||||
},
|
||||
seasonPosterContainerTablet: {
|
||||
position: 'relative',
|
||||
width: 120,
|
||||
height: 180,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 12,
|
||||
},
|
||||
seasonPoster: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
|
@ -1065,13 +1140,27 @@ const styles = StyleSheet.create({
|
|||
right: 0,
|
||||
height: 4,
|
||||
},
|
||||
selectedSeasonIndicatorTablet: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 6,
|
||||
},
|
||||
seasonButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
seasonButtonTextTablet: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
selectedSeasonButtonText: {
|
||||
fontWeight: '700',
|
||||
},
|
||||
selectedSeasonButtonTextTablet: {
|
||||
fontWeight: '800',
|
||||
},
|
||||
episodeCountBadge: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
|
|
|
|||
|
|
@ -730,6 +730,10 @@ const PluginsScreen: React.FC = () => {
|
|||
<View style={[styles.availableIndicator, { backgroundColor: colors.mediumGray }]}>
|
||||
<Text style={styles.availableIndicatorText}>Disabled</Text>
|
||||
</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 && (
|
||||
<View style={styles.availableIndicator}>
|
||||
<Text style={styles.availableIndicatorText}>Available</Text>
|
||||
|
|
@ -758,8 +762,8 @@ const PluginsScreen: React.FC = () => {
|
|||
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
|
||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false}
|
||||
style={{ opacity: (!settings.enableLocalScrapers || scraper.manifestEnabled === false) ? 0.5 : 1 }}
|
||||
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
|
||||
style={{ opacity: (!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))) ? 0.5 : 1 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import axios from 'axios';
|
||||
import { Platform } from 'react-native';
|
||||
import { logger } from '../utils/logger';
|
||||
import { Stream } from '../types/streams';
|
||||
import { cacheService } from './cacheService';
|
||||
|
|
@ -24,6 +25,8 @@ export interface ScraperInfo {
|
|||
logo?: string;
|
||||
contentLanguage?: string[];
|
||||
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 {
|
||||
|
|
@ -181,6 +184,26 @@ class LocalScraperService {
|
|||
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
|
||||
async refreshRepository(): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
|
@ -244,7 +267,21 @@ class LocalScraperService {
|
|||
|
||||
// Download and install each scraper from manifest
|
||||
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();
|
||||
|
|
@ -269,9 +306,18 @@ class LocalScraperService {
|
|||
const scraperCode = response.data;
|
||||
|
||||
// Store scraper info and code
|
||||
const existingScraper = this.installedScrapers.get(scraperInfo.id);
|
||||
const isPlatformCompatible = this.isPlatformCompatible(scraperInfo);
|
||||
|
||||
const updatedScraperInfo = {
|
||||
...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)
|
||||
|
|
@ -370,22 +416,24 @@ class LocalScraperService {
|
|||
this.repositoryName = manifest.name;
|
||||
}
|
||||
|
||||
// Return scrapers from manifest, respecting manifest's enabled field
|
||||
const availableScrapers = manifest.scrapers.map(scraperInfo => {
|
||||
const installedScraper = this.installedScrapers.get(scraperInfo.id);
|
||||
|
||||
// Create a copy with manifest data
|
||||
const scraperWithManifestData = {
|
||||
...scraperInfo,
|
||||
// Store the manifest's enabled state separately
|
||||
manifestEnabled: scraperInfo.enabled,
|
||||
// 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 scrapers from manifest, respecting manifest's enabled field and platform compatibility
|
||||
const availableScrapers = manifest.scrapers
|
||||
.filter(scraperInfo => this.isPlatformCompatible(scraperInfo))
|
||||
.map(scraperInfo => {
|
||||
const installedScraper = this.installedScrapers.get(scraperInfo.id);
|
||||
|
||||
// Create a copy with manifest data
|
||||
const scraperWithManifestData = {
|
||||
...scraperInfo,
|
||||
// Store the manifest's enabled state separately
|
||||
manifestEnabled: scraperInfo.enabled,
|
||||
// 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;
|
||||
});
|
||||
|
||||
logger.log('[LocalScraperService] Found', availableScrapers.length, 'available scrapers in repository');
|
||||
|
||||
|
|
@ -409,6 +457,12 @@ class LocalScraperService {
|
|||
|
||||
const scraper = this.installedScrapers.get(scraperId);
|
||||
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;
|
||||
this.installedScrapers.set(scraperId, scraper);
|
||||
await this.saveInstalledScrapers();
|
||||
|
|
|
|||
Loading…
Reference in a new issue