some changes

This commit is contained in:
tapframe 2025-08-11 16:04:50 +05:30
parent 501bded9ee
commit 9246b26493
6 changed files with 346 additions and 75 deletions

@ -1 +1 @@
Subproject commit cfd669df2258d217c90c58ce9455676c60abd1ac
Subproject commit a6239977e8725a631df20154c45bff9572c3ff98

View file

@ -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);

View file

@ -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;

View file

@ -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,

View file

@ -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>
);

View file

@ -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();