major ui changes

This commit is contained in:
tapframe 2025-07-29 00:09:16 +05:30
parent 9c12f9fc08
commit 494b35b1c0
5 changed files with 211 additions and 66 deletions

@ -1 +1 @@
Subproject commit 63d560d55f1a84a16318525ad4eb1db5162e059c Subproject commit 22ed3a1c96ed2a8adf5bf9f277acd9d8c53c069c

View file

@ -39,7 +39,7 @@ import LogoSourceSettings from '../screens/LogoSourceSettings';
import ThemeScreen from '../screens/ThemeScreen'; import ThemeScreen from '../screens/ThemeScreen';
import ProfilesScreen from '../screens/ProfilesScreen'; import ProfilesScreen from '../screens/ProfilesScreen';
import OnboardingScreen from '../screens/OnboardingScreen'; import OnboardingScreen from '../screens/OnboardingScreen';
import ScraperSettingsScreen from '../screens/ScraperSettingsScreen'; import PluginsScreen from '../screens/PluginsScreen';
// Stack navigator types // Stack navigator types
export type RootStackParamList = { export type RootStackParamList = {
@ -1028,7 +1028,7 @@ const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStack
/> />
<Stack.Screen <Stack.Screen
name="ScraperSettings" name="ScraperSettings"
component={ScraperSettingsScreen} component={PluginsScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade', animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
animationDuration: Platform.OS === 'android' ? 250 : 200, animationDuration: Platform.OS === 'android' ? 250 : 200,

View file

@ -314,11 +314,23 @@ const createStyles = (colors: any) => StyleSheet.create({
opacity: 0.5, opacity: 0.5,
}, },
disabledImage: { disabledImage: {
opacity: 0.3, opacity: 0.3,
}, },
}); availableIndicator: {
backgroundColor: colors.primary,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
marginLeft: 8,
},
availableIndicatorText: {
color: colors.white,
fontSize: 10,
fontWeight: '600',
},
});
const ScraperSettingsScreen: React.FC = () => { const PluginsScreen: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const { settings, updateSetting } = useSettings(); const { settings, updateSetting } = useSettings();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
@ -337,7 +349,7 @@ const ScraperSettingsScreen: React.FC = () => {
const loadScrapers = async () => { const loadScrapers = async () => {
try { try {
const scrapers = await localScraperService.getInstalledScrapers(); const scrapers = await localScraperService.getAvailableScrapers();
setInstalledScrapers(scrapers); setInstalledScrapers(scrapers);
} catch (error) { } catch (error) {
logger.error('[ScraperSettings] Failed to load scrapers:', error); logger.error('[ScraperSettings] Failed to load scrapers:', error);
@ -395,7 +407,7 @@ const ScraperSettingsScreen: React.FC = () => {
try { try {
setIsRefreshing(true); setIsRefreshing(true);
await localScraperService.refreshRepository(); await localScraperService.refreshRepository();
await loadScrapers(); await loadScrapers(); // This will now load available scrapers from manifest
Alert.alert('Success', 'Repository refreshed successfully'); Alert.alert('Success', 'Repository refreshed successfully');
} catch (error) { } catch (error) {
logger.error('[ScraperSettings] Failed to refresh repository:', error); logger.error('[ScraperSettings] Failed to refresh repository:', error);
@ -411,11 +423,25 @@ const ScraperSettingsScreen: React.FC = () => {
const handleToggleScraper = async (scraperId: string, enabled: boolean) => { const handleToggleScraper = async (scraperId: string, enabled: boolean) => {
try { try {
if (enabled) {
// If enabling a scraper, ensure it's installed first
const installedScrapers = await localScraperService.getInstalledScrapers();
const isInstalled = installedScrapers.some(scraper => scraper.id === scraperId);
if (!isInstalled) {
// Need to install the scraper first
setIsRefreshing(true);
await localScraperService.refreshRepository();
setIsRefreshing(false);
}
}
await localScraperService.setScraperEnabled(scraperId, enabled); await localScraperService.setScraperEnabled(scraperId, enabled);
await loadScrapers(); await loadScrapers();
} catch (error) { } catch (error) {
logger.error('[ScraperSettings] Failed to toggle scraper:', error); logger.error('[ScraperSettings] Failed to toggle scraper:', error);
Alert.alert('Error', 'Failed to update scraper status'); Alert.alert('Error', 'Failed to update scraper status');
setIsRefreshing(false);
} }
}; };
@ -502,7 +528,7 @@ const ScraperSettingsScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={styles.headerTitle}>Local Scrapers</Text> <Text style={styles.headerTitle}>Plugins</Text>
<ScrollView <ScrollView
style={styles.scrollView} style={styles.scrollView}
@ -606,10 +632,10 @@ const ScraperSettingsScreen: React.FC = () => {
</View> </View>
</View> </View>
{/* Installed Scrapers */} {/* Available Scrapers */}
<View style={[styles.section, !settings.enableLocalScrapers && styles.disabledSection]}> <View style={[styles.section, !settings.enableLocalScrapers && styles.disabledSection]}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, !settings.enableLocalScrapers && styles.disabledText]}>Installed Scrapers</Text> <Text style={[styles.sectionTitle, !settings.enableLocalScrapers && styles.disabledText]}>Available Scrapers</Text>
{installedScrapers.length > 0 && settings.enableLocalScrapers && ( {installedScrapers.length > 0 && settings.enableLocalScrapers && (
<TouchableOpacity <TouchableOpacity
style={styles.clearButton} style={styles.clearButton}
@ -619,56 +645,78 @@ const ScraperSettingsScreen: React.FC = () => {
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
<Text style={[styles.sectionDescription, !settings.enableLocalScrapers && styles.disabledText]}>
Scrapers available in the repository. Only enabled scrapers that are also installed will be used for streaming.
</Text>
{installedScrapers.length === 0 ? ( {installedScrapers.length === 0 ? (
<View style={[styles.emptyContainer, !settings.enableLocalScrapers && styles.disabledContainer]}> <View style={[styles.emptyContainer, !settings.enableLocalScrapers && styles.disabledContainer]}>
<Ionicons name="download-outline" size={48} color={!settings.enableLocalScrapers ? colors.elevation3 : colors.mediumGray} /> <Ionicons name="download-outline" size={48} color={!settings.enableLocalScrapers ? colors.elevation3 : colors.mediumGray} />
<Text style={[styles.emptyStateTitle, !settings.enableLocalScrapers && styles.disabledText]}>No Scrapers Installed</Text> <Text style={[styles.emptyStateTitle, !settings.enableLocalScrapers && styles.disabledText]}>No Scrapers Available</Text>
<Text style={[styles.emptyStateDescription, !settings.enableLocalScrapers && styles.disabledText]}> <Text style={[styles.emptyStateDescription, !settings.enableLocalScrapers && styles.disabledText]}>
Configure a repository above to install scrapers. Configure a repository above to view available scrapers.
</Text> </Text>
</View> </View>
) : ( ) : (
<View style={styles.scrapersContainer}> <View style={styles.scrapersContainer}>
{installedScrapers.map((scraper) => ( {installedScrapers.map((scraper) => {
<View key={scraper.id} style={[styles.scraperItem, !settings.enableLocalScrapers && styles.disabledContainer]}> // Check if scraper is actually installed (has cached code)
{scraper.logo ? ( const isInstalled = localScraperService.getInstalledScrapers().then(installed =>
<Image installed.some(s => s.id === scraper.id)
source={{ uri: scraper.logo }} );
style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledImage]}
resizeMode="contain" return (
/> <View key={scraper.id} style={[styles.scraperItem, !settings.enableLocalScrapers && styles.disabledContainer]}>
) : ( {scraper.logo ? (
<View style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledContainer]} /> <Image
)} source={{ uri: scraper.logo }}
<View style={styles.scraperInfo}> style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledImage]}
<Text style={[styles.scraperName, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.name}</Text> resizeMode="contain"
<Text style={[styles.scraperDescription, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.description}</Text> />
<View style={styles.scraperMeta}> ) : (
<Text style={[styles.scraperVersion, !settings.enableLocalScrapers && styles.disabledText]}>v{scraper.version}</Text> <View style={[styles.scraperLogo, !settings.enableLocalScrapers && styles.disabledContainer]} />
<Text style={[styles.scraperDot, !settings.enableLocalScrapers && styles.disabledText]}></Text> )}
<Text style={[styles.scraperTypes, !settings.enableLocalScrapers && styles.disabledText]}> <View style={styles.scraperInfo}>
{scraper.supportedTypes && Array.isArray(scraper.supportedTypes) ? scraper.supportedTypes.join(', ') : 'Unknown'} <View style={{ flexDirection: 'row', alignItems: 'center' }}>
</Text> <Text style={[styles.scraperName, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.name}</Text>
{scraper.contentLanguage && Array.isArray(scraper.contentLanguage) && scraper.contentLanguage.length > 0 && ( {scraper.manifestEnabled === false ? (
<> <View style={[styles.availableIndicator, { backgroundColor: colors.mediumGray }]}>
<Text style={[styles.scraperDot, !settings.enableLocalScrapers && styles.disabledText]}></Text> <Text style={styles.availableIndicatorText}>Disabled</Text>
<Text style={[styles.scraperLanguage, !settings.enableLocalScrapers && styles.disabledText]}> </View>
{scraper.contentLanguage.map(lang => lang.toUpperCase()).join(', ')} ) : !scraper.enabled && (
</Text> <View style={styles.availableIndicator}>
</> <Text style={styles.availableIndicatorText}>Available</Text>
)} </View>
)}
</View>
<Text style={[styles.scraperDescription, !settings.enableLocalScrapers && styles.disabledText]}>{scraper.description}</Text>
<View style={styles.scraperMeta}>
<Text style={[styles.scraperVersion, !settings.enableLocalScrapers && styles.disabledText]}>v{scraper.version}</Text>
<Text style={[styles.scraperDot, !settings.enableLocalScrapers && styles.disabledText]}></Text>
<Text style={[styles.scraperTypes, !settings.enableLocalScrapers && styles.disabledText]}>
{scraper.supportedTypes && Array.isArray(scraper.supportedTypes) ? scraper.supportedTypes.join(', ') : 'Unknown'}
</Text>
{scraper.contentLanguage && Array.isArray(scraper.contentLanguage) && scraper.contentLanguage.length > 0 && (
<>
<Text style={[styles.scraperDot, !settings.enableLocalScrapers && styles.disabledText]}></Text>
<Text style={[styles.scraperLanguage, !settings.enableLocalScrapers && styles.disabledText]}>
{scraper.contentLanguage.map(lang => lang.toUpperCase()).join(', ')}
</Text>
</>
)}
</View>
</View> </View>
<Switch
value={scraper.enabled && settings.enableLocalScrapers}
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 }}
/>
</View> </View>
<Switch );
value={scraper.enabled && settings.enableLocalScrapers} })}
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
disabled={!settings.enableLocalScrapers}
/>
</View>
))}
</View> </View>
)} )}
</View> </View>
@ -945,4 +993,4 @@ const styles = StyleSheet.create({
}, },
}); });
export default ScraperSettingsScreen; export default PluginsScreen;

View file

@ -323,6 +323,13 @@ const SettingsScreen: React.FC = () => {
renderControl={ChevronRight} renderControl={ChevronRight}
onPress={() => navigation.navigate('Addons')} onPress={() => navigation.navigate('Addons')}
/> />
<SettingItem
title="Plugins"
description="Manage plugins and repositories"
icon="code"
renderControl={ChevronRight}
onPress={() => navigation.navigate('ScraperSettings')}
/>
<SettingItem <SettingItem
title="Catalogs" title="Catalogs"
description={`${catalogCount} active`} description={`${catalogCount} active`}
@ -401,13 +408,6 @@ const SettingsScreen: React.FC = () => {
renderControl={ChevronRight} renderControl={ChevronRight}
onPress={() => navigation.navigate('PlayerSettings')} onPress={() => navigation.navigate('PlayerSettings')}
/> />
<SettingItem
title="Local Scrapers"
description="Manage local scraper repositories"
icon="code"
renderControl={ChevronRight}
onPress={() => navigation.navigate('ScraperSettings')}
/>
<SettingItem <SettingItem
title="Notifications" title="Notifications"
description="Episode reminders" description="Episode reminders"

View file

@ -23,6 +23,7 @@ export interface ScraperInfo {
enabled: boolean; enabled: boolean;
logo?: string; logo?: string;
contentLanguage?: string[]; contentLanguage?: string[];
manifestEnabled?: boolean; // Whether the scraper is enabled in the manifest
} }
export interface LocalScraperResult { export interface LocalScraperResult {
@ -189,17 +190,48 @@ class LocalScraperService {
try { try {
logger.log('[LocalScraperService] Fetching repository manifest from:', this.repositoryUrl); logger.log('[LocalScraperService] Fetching repository manifest from:', this.repositoryUrl);
// Fetch manifest // Fetch manifest with cache busting
const manifestUrl = this.repositoryUrl.endsWith('/') const baseManifestUrl = this.repositoryUrl.endsWith('/')
? `${this.repositoryUrl}manifest.json` ? `${this.repositoryUrl}manifest.json`
: `${this.repositoryUrl}/manifest.json`; : `${this.repositoryUrl}/manifest.json`;
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}`;
const response = await axios.get(manifestUrl, { timeout: 10000 }); const response = await axios.get(manifestUrl, {
const manifest: ScraperManifest = response.data; timeout: 10000,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Expires': '0'
}
});
const manifest: ScraperManifest = response.data;
logger.log('[LocalScraperService] getAvailableScrapers - Raw manifest data:', JSON.stringify(manifest, null, 2));
logger.log('[LocalScraperService] getAvailableScrapers - Manifest scrapers count:', manifest.scrapers?.length || 0);
// Log each scraper's enabled status from manifest
manifest.scrapers?.forEach(scraper => {
logger.log(`[LocalScraperService] getAvailableScrapers - Scraper ${scraper.name}: enabled=${scraper.enabled}`);
});
logger.log('[LocalScraperService] Found', manifest.scrapers.length, 'scrapers in repository'); logger.log('[LocalScraperService] Found', manifest.scrapers.length, 'scrapers in repository');
// Download and install each scraper // Get current manifest scraper IDs
const manifestScraperIds = new Set(manifest.scrapers.map(s => s.id));
// Remove scrapers that are no longer in the manifest
const currentScraperIds = Array.from(this.installedScrapers.keys());
for (const scraperId of currentScraperIds) {
if (!manifestScraperIds.has(scraperId)) {
logger.log('[LocalScraperService] Removing scraper no longer in manifest:', this.installedScrapers.get(scraperId)?.name || scraperId);
this.installedScrapers.delete(scraperId);
this.scraperCode.delete(scraperId);
// Remove from AsyncStorage cache
await AsyncStorage.removeItem(`scraper-code-${scraperId}`);
}
}
// Download and install each scraper from manifest
for (const scraperInfo of manifest.scrapers) { for (const scraperInfo of manifest.scrapers) {
await this.downloadScraper(scraperInfo); await this.downloadScraper(scraperInfo);
} }
@ -296,6 +328,65 @@ class LocalScraperService {
return Array.from(this.installedScrapers.values()); return Array.from(this.installedScrapers.values());
} }
// Get available scrapers from manifest.json (for display in settings)
async getAvailableScrapers(): Promise<ScraperInfo[]> {
if (!this.repositoryUrl) {
logger.log('[LocalScraperService] No repository URL configured, returning installed scrapers');
return this.getInstalledScrapers();
}
try {
logger.log('[LocalScraperService] Fetching available scrapers from manifest');
// Fetch manifest with cache busting
const baseManifestUrl = this.repositoryUrl.endsWith('/')
? `${this.repositoryUrl}manifest.json`
: `${this.repositoryUrl}/manifest.json`;
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}`;
const response = await axios.get(manifestUrl, {
timeout: 10000,
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Expires': '0'
}
});
const manifest: ScraperManifest = response.data;
// 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;
});
logger.log('[LocalScraperService] Found', availableScrapers.length, 'available scrapers in repository');
// Log final scraper states being returned to UI
availableScrapers.forEach(scraper => {
logger.log(`[LocalScraperService] Final scraper ${scraper.name}: manifestEnabled=${scraper.manifestEnabled}, enabled=${scraper.enabled}`);
});
return availableScrapers;
} catch (error) {
logger.error('[LocalScraperService] Failed to fetch available scrapers from manifest:', error);
// Fallback to installed scrapers if manifest fetch fails
return this.getInstalledScrapers();
}
}
// Enable/disable scraper // Enable/disable scraper
async setScraperEnabled(scraperId: string, enabled: boolean): Promise<void> { async setScraperEnabled(scraperId: string, enabled: boolean): Promise<void> {
await this.ensureInitialized(); await this.ensureInitialized();
@ -313,8 +404,14 @@ class LocalScraperService {
async getStreams(type: string, tmdbId: string, season?: number, episode?: number, callback?: ScraperCallback): Promise<void> { async getStreams(type: string, tmdbId: string, season?: number, episode?: number, callback?: ScraperCallback): Promise<void> {
await this.ensureInitialized(); await this.ensureInitialized();
const enabledScrapers = Array.from(this.installedScrapers.values()) // Get available scrapers from manifest (respects manifestEnabled)
.filter(scraper => scraper.enabled && scraper.supportedTypes.includes(type as 'movie' | 'tv')); const availableScrapers = await this.getAvailableScrapers();
const enabledScrapers = availableScrapers
.filter(scraper =>
scraper.enabled &&
scraper.manifestEnabled !== false &&
scraper.supportedTypes.includes(type as 'movie' | 'tv')
);
if (enabledScrapers.length === 0) { if (enabledScrapers.length === 0) {
logger.log('[LocalScraperService] No enabled scrapers found for type:', type); logger.log('[LocalScraperService] No enabled scrapers found for type:', type);