From 9246b2649371e5d4f02d27f9c6fad21ca46a9067 Mon Sep 17 00:00:00 2001 From: tapframe Date: Mon, 11 Aug 2025 16:04:50 +0530 Subject: [PATCH] some changes --- local-scrapers-repo | 2 +- src/components/home/FeaturedContent.tsx | 4 +- src/components/metadata/HeroSection.tsx | 170 +++++++++++++++++++--- src/components/metadata/SeriesContent.tsx | 147 +++++++++++++++---- src/screens/PluginsScreen.tsx | 8 +- src/services/localScraperService.ts | 90 +++++++++--- 6 files changed, 346 insertions(+), 75 deletions(-) diff --git a/local-scrapers-repo b/local-scrapers-repo index cfd669d..a623997 160000 --- a/local-scrapers-repo +++ b/local-scrapers-repo @@ -1 +1 @@ -Subproject commit cfd669df2258d217c90c58ce9455676c60abd1ac +Subproject commit a6239977e8725a631df20154c45bff9572c3ff98 diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index c6687f9..b9d0971 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -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); \ No newline at end of file +export default React.memo(FeaturedContent); \ No newline at end of file diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index c117db8..26e3694 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -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 ( - + @@ -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"} /> - {finalPlayButtonText} + {finalPlayButtonText} @@ -271,17 +272,17 @@ const ActionButtons = memo(({ )} - + {inLibrary ? 'Saved' : 'Save'} {type === 'series' && ( @@ -292,7 +293,7 @@ const ActionButtons = memo(({ )} @@ -534,9 +535,9 @@ const WatchProgressDisplay = memo(({ const isCompleted = progressData.isWatched || progressData.progressPercent >= 85; return ( - + {/* Glass morphism background with entrance animation */} - + {Platform.OS === 'ios' ? ( ) : ( @@ -580,9 +581,9 @@ const WatchProgressDisplay = memo(({ {/* Enhanced text container with better typography */} - {progressData.displayText} @@ -590,7 +591,7 @@ const WatchProgressDisplay = memo(({ - {progressData.episodeInfo} • Last watched {progressData.formattedTime} @@ -839,11 +840,11 @@ const HeroSection: React.FC = memo(({ entering={FadeIn.duration(400).delay(200 + index * 100)} style={{ flexDirection: 'row', alignItems: 'center' }} > - + {genreName} {index < array.length - 1 && ( - + )} )); @@ -966,14 +967,14 @@ const HeroSection: React.FC = memo(({ style={styles.bottomFadeGradient} pointerEvents="none" /> - + {/* Optimized Title/Logo */} {shouldLoadSecondaryData && metadata.logo && !logoLoadError ? ( { @@ -981,7 +982,7 @@ const HeroSection: React.FC = memo(({ }} /> ) : ( - + {metadata.name} )} @@ -999,7 +1000,7 @@ const HeroSection: React.FC = memo(({ {/* Optimized genre display with lazy loading */} {shouldLoadSecondaryData && genreElements && ( - + {genreElements} )} @@ -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; \ No newline at end of file diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index 21e1106..6d0137b 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -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 = ({ // Add refs for the scroll views const seasonScrollViewRef = useRef(null); - const episodeScrollViewRef = useRef(null); + const episodeScrollViewRef = useRef>(null); @@ -224,15 +224,19 @@ export const SeriesContent: React.FC = ({ const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b); return ( - - Seasons + + Seasons >} 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 = ({ key={season} style={[ styles.seasonButton, + isTablet && styles.seasonButtonTablet, selectedSeason === season && [styles.selectedSeasonButton, { borderColor: currentTheme.colors.primary }] ]} onPress={() => onSeasonChange(season)} > - + {selectedSeason === season && ( - + )} {/* Show episode count badge, including when there are no episodes */} @@ -274,8 +283,13 @@ export const SeriesContent: React.FC = ({ Season {season} @@ -536,19 +550,19 @@ export const SeriesContent: React.FC = ({ style={styles.episodeGradient} > {/* Content Container */} - + {/* Episode Number Badge */} - - {episodeString} + + {episodeString} {/* Episode Title */} - + {episode.name} {/* Episode Description */} - + {episode.overview || 'No description available'} @@ -636,7 +650,7 @@ export const SeriesContent: React.FC = ({ (settings?.episodeLayoutStyle === 'horizontal') ? ( // Horizontal Layout (Netflix-style) >} + ref={episodeScrollViewRef} data={currentSeasonEpisodes} renderItem={({ item: episode, index }) => ( = ({ 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) >} + ref={episodeScrollViewRef} data={currentSeasonEpisodes} renderItem={({ item: episode, index }) => ( = ({ )} 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, diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx index 460c86f..1e26d30 100644 --- a/src/screens/PluginsScreen.tsx +++ b/src/screens/PluginsScreen.tsx @@ -730,6 +730,10 @@ const PluginsScreen: React.FC = () => { Disabled + ) : scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android') ? ( + + Platform Disabled + ) : !scraper.enabled && ( Available @@ -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 }} /> ); diff --git a/src/services/localScraperService.ts b/src/services/localScraperService.ts index ea8b24c..c77489e 100644 --- a/src/services/localScraperService.ts +++ b/src/services/localScraperService.ts @@ -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 { 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();