mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-28 11:53:00 +00:00
fixes
This commit is contained in:
parent
494b35b1c0
commit
ebcbced142
3 changed files with 142 additions and 35 deletions
|
|
@ -41,6 +41,7 @@ export interface AppSettings {
|
||||||
enableLocalScrapers: boolean; // Enable/disable local scraper functionality
|
enableLocalScrapers: boolean; // Enable/disable local scraper functionality
|
||||||
scraperTimeout: number; // Timeout for scraper execution in seconds
|
scraperTimeout: number; // Timeout for scraper execution in seconds
|
||||||
enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers
|
enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers
|
||||||
|
streamDisplayMode: 'separate' | 'grouped'; // How to display streaming links - separately by provider or grouped under one name
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: AppSettings = {
|
export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
|
|
@ -64,6 +65,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
enableLocalScrapers: true,
|
enableLocalScrapers: true,
|
||||||
scraperTimeout: 60, // 60 seconds timeout
|
scraperTimeout: 60, // 60 seconds timeout
|
||||||
enableScraperUrlValidation: true, // Enable URL validation by default
|
enableScraperUrlValidation: true, // Enable URL validation by default
|
||||||
|
streamDisplayMode: 'separate', // Default to separate display by provider
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_STORAGE_KEY = 'app_settings';
|
const SETTINGS_STORAGE_KEY = 'app_settings';
|
||||||
|
|
|
||||||
|
|
@ -739,6 +739,22 @@ const PluginsScreen: React.FC = () => {
|
||||||
disabled={!settings.enableLocalScrapers}
|
disabled={!settings.enableLocalScrapers}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.settingRow}>
|
||||||
|
<View style={styles.settingInfo}>
|
||||||
|
<Text style={[styles.settingTitle, !settings.enableLocalScrapers && styles.disabledText]}>Stream Display Mode</Text>
|
||||||
|
<Text style={[styles.settingDescription, !settings.enableLocalScrapers && styles.disabledText]}>
|
||||||
|
{settings.streamDisplayMode === 'separate' ? 'Show each provider separately' : 'Group all providers under one name'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={settings.streamDisplayMode === 'grouped'}
|
||||||
|
onValueChange={(value) => updateSetting('streamDisplayMode', value ? 'grouped' : 'separate')}
|
||||||
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
|
thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
|
||||||
|
disabled={!settings.enableLocalScrapers}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,15 +101,8 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
|
||||||
};
|
};
|
||||||
}, [stream.name, stream.title, stream.behaviorHints, stream.size]);
|
}, [stream.name, stream.title, stream.behaviorHints, stream.size]);
|
||||||
|
|
||||||
// Animation delay based on index - stagger effect
|
|
||||||
const enterDelay = 100 + (index * 30);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<TouchableOpacity
|
||||||
entering={FadeInDown.duration(200).delay(enterDelay)}
|
|
||||||
exiting={FadeOut.duration(150)}
|
|
||||||
>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
style={[
|
||||||
styles.streamCard,
|
styles.streamCard,
|
||||||
isLoading && styles.streamCardLoading
|
isLoading && styles.streamCardLoading
|
||||||
|
|
@ -169,7 +162,6 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -232,11 +224,7 @@ const ProviderFilter = memo(({
|
||||||
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
||||||
|
|
||||||
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
|
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
|
||||||
<Animated.View
|
<TouchableOpacity
|
||||||
entering={FadeIn.duration(300).delay(100 + index * 40)}
|
|
||||||
exiting={FadeOut.duration(150)}
|
|
||||||
>
|
|
||||||
<TouchableOpacity
|
|
||||||
key={item.id}
|
key={item.id}
|
||||||
style={[
|
style={[
|
||||||
styles.filterChip,
|
styles.filterChip,
|
||||||
|
|
@ -251,13 +239,10 @@ const ProviderFilter = memo(({
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
|
||||||
), [selectedProvider, onSelect, styles]);
|
), [selectedProvider, onSelect, styles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<View>
|
||||||
entering={FadeIn.duration(300)}
|
|
||||||
>
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={providers}
|
data={providers}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
|
|
@ -277,7 +262,7 @@ const ProviderFilter = memo(({
|
||||||
index,
|
index,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -385,7 +370,12 @@ export const StreamsScreen = () => {
|
||||||
if (providersWithStreams.length > 0) {
|
if (providersWithStreams.length > 0) {
|
||||||
logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`);
|
logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`);
|
||||||
const providersWithStreamsSet = new Set(providersWithStreams);
|
const providersWithStreamsSet = new Set(providersWithStreams);
|
||||||
setAvailableProviders(providersWithStreamsSet);
|
|
||||||
|
// Only update if we have new providers, don't remove existing ones during loading
|
||||||
|
setAvailableProviders(prevProviders => {
|
||||||
|
const newProviders = new Set([...prevProviders, ...providersWithStreamsSet]);
|
||||||
|
return newProviders;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update loading states for individual providers
|
// Update loading states for individual providers
|
||||||
|
|
@ -407,11 +397,28 @@ export const StreamsScreen = () => {
|
||||||
}, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type]);
|
}, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type]);
|
||||||
|
|
||||||
// Reset the selected provider to 'all' if the current selection is no longer available
|
// Reset the selected provider to 'all' if the current selection is no longer available
|
||||||
|
// But preserve special filter values like 'grouped-plugins' and 'all'
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedProvider !== 'all' && !availableProviders.has(selectedProvider)) {
|
// Don't reset if it's a special filter value
|
||||||
|
const isSpecialFilter = selectedProvider === 'all' || selectedProvider === 'grouped-plugins';
|
||||||
|
|
||||||
|
if (isSpecialFilter) {
|
||||||
|
return; // Always preserve special filters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if provider exists in current streams data
|
||||||
|
const currentStreamsData = type === 'series' ? episodeStreams : groupedStreams;
|
||||||
|
const hasStreamsForProvider = currentStreamsData[selectedProvider] &&
|
||||||
|
currentStreamsData[selectedProvider].streams &&
|
||||||
|
currentStreamsData[selectedProvider].streams.length > 0;
|
||||||
|
|
||||||
|
// Only reset if the provider doesn't exist in available providers AND doesn't have streams
|
||||||
|
const isAvailableProvider = availableProviders.has(selectedProvider);
|
||||||
|
|
||||||
|
if (!isAvailableProvider && !hasStreamsForProvider) {
|
||||||
setSelectedProvider('all');
|
setSelectedProvider('all');
|
||||||
}
|
}
|
||||||
}, [selectedProvider, availableProviders]);
|
}, [selectedProvider, availableProviders, episodeStreams, groupedStreams, type]);
|
||||||
|
|
||||||
// Update useEffect to check for sources
|
// Update useEffect to check for sources
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -863,6 +870,43 @@ export const StreamsScreen = () => {
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// In grouped mode, separate addons and plugins
|
||||||
|
if (settings.streamDisplayMode === 'grouped') {
|
||||||
|
const addonProviders: string[] = [];
|
||||||
|
const pluginProviders: string[] = [];
|
||||||
|
|
||||||
|
Array.from(allProviders).forEach(provider => {
|
||||||
|
const isInstalledAddon = installedAddons.some(addon => addon.id === provider);
|
||||||
|
if (isInstalledAddon) {
|
||||||
|
addonProviders.push(provider);
|
||||||
|
} else {
|
||||||
|
pluginProviders.push(provider);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterChips = [{ id: 'all', name: 'All Providers' }];
|
||||||
|
|
||||||
|
// Add individual addon chips
|
||||||
|
addonProviders
|
||||||
|
.sort((a, b) => {
|
||||||
|
const indexA = installedAddons.findIndex(addon => addon.id === a);
|
||||||
|
const indexB = installedAddons.findIndex(addon => addon.id === b);
|
||||||
|
return indexA - indexB;
|
||||||
|
})
|
||||||
|
.forEach(provider => {
|
||||||
|
const installedAddon = installedAddons.find(addon => addon.id === provider);
|
||||||
|
filterChips.push({ id: provider, name: installedAddon?.name || provider });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add single grouped plugins chip if there are any plugins
|
||||||
|
if (pluginProviders.length > 0) {
|
||||||
|
filterChips.push({ id: 'grouped-plugins', name: 'Plugins' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterChips;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal mode - individual chips for all providers
|
||||||
return [
|
return [
|
||||||
{ id: 'all', name: 'All Providers' },
|
{ id: 'all', name: 'All Providers' },
|
||||||
...Array.from(allProviders)
|
...Array.from(allProviders)
|
||||||
|
|
@ -889,19 +933,26 @@ export const StreamsScreen = () => {
|
||||||
return { id: provider, name: displayName };
|
return { id: provider, name: displayName };
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}, [availableProviders, type, episodeStreams, groupedStreams]);
|
}, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
|
||||||
|
|
||||||
const sections = useMemo(() => {
|
const sections = useMemo(() => {
|
||||||
const streams = type === 'series' ? episodeStreams : groupedStreams;
|
const streams = type === 'series' ? episodeStreams : groupedStreams;
|
||||||
const installedAddons = stremioService.getInstalledAddons();
|
const installedAddons = stremioService.getInstalledAddons();
|
||||||
|
|
||||||
// Filter streams by selected provider - only if not "all"
|
// Filter streams by selected provider
|
||||||
const filteredEntries = Object.entries(streams)
|
const filteredEntries = Object.entries(streams)
|
||||||
.filter(([addonId]) => {
|
.filter(([addonId]) => {
|
||||||
// If "all" is selected, show all providers
|
// If "all" is selected, show all providers
|
||||||
if (selectedProvider === 'all') {
|
if (selectedProvider === 'all') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In grouped mode, handle special 'grouped-plugins' filter
|
||||||
|
if (settings.streamDisplayMode === 'grouped' && selectedProvider === 'grouped-plugins') {
|
||||||
|
const isInstalledAddon = installedAddons.some(addon => addon.id === addonId);
|
||||||
|
return !isInstalledAddon; // Show only plugins (non-installed addons)
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise only show the selected provider
|
// Otherwise only show the selected provider
|
||||||
return addonId === selectedProvider;
|
return addonId === selectedProvider;
|
||||||
})
|
})
|
||||||
|
|
@ -914,17 +965,59 @@ export const StreamsScreen = () => {
|
||||||
if (indexA !== -1) return -1;
|
if (indexA !== -1) return -1;
|
||||||
if (indexB !== -1) return 1;
|
if (indexB !== -1) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
})
|
});
|
||||||
.map(([addonId, { addonName, streams: providerStreams }]) => {
|
|
||||||
|
// Check if we should group all streams under one section
|
||||||
|
if (settings.streamDisplayMode === 'grouped') {
|
||||||
|
// Separate streams by type: installed addons vs plugins
|
||||||
|
const addonStreams: Stream[] = [];
|
||||||
|
const pluginStreams: Stream[] = [];
|
||||||
|
const addonNames: string[] = [];
|
||||||
|
const pluginNames: string[] = [];
|
||||||
|
|
||||||
|
filteredEntries.forEach(([addonId, { addonName, streams: providerStreams }]) => {
|
||||||
|
const isInstalledAddon = installedAddons.some(addon => addon.id === addonId);
|
||||||
|
if (isInstalledAddon) {
|
||||||
|
addonStreams.push(...providerStreams);
|
||||||
|
if (!addonNames.includes(addonName)) {
|
||||||
|
addonNames.push(addonName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pluginStreams.push(...providerStreams);
|
||||||
|
if (!pluginNames.includes(addonName)) {
|
||||||
|
pluginNames.push(addonName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sections = [];
|
||||||
|
if (addonStreams.length > 0) {
|
||||||
|
sections.push({
|
||||||
|
title: addonNames.join(', '),
|
||||||
|
addonId: 'grouped-addons',
|
||||||
|
data: addonStreams
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (pluginStreams.length > 0) {
|
||||||
|
sections.push({
|
||||||
|
title: pluginNames.join(', '),
|
||||||
|
addonId: 'grouped-plugins',
|
||||||
|
data: pluginStreams
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections;
|
||||||
|
} else {
|
||||||
|
// Use separate sections for each provider (current behavior)
|
||||||
|
return filteredEntries.map(([addonId, { addonName, streams: providerStreams }]) => {
|
||||||
return {
|
return {
|
||||||
title: addonName,
|
title: addonName,
|
||||||
addonId,
|
addonId,
|
||||||
data: providerStreams
|
data: providerStreams
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return filteredEntries;
|
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
|
||||||
}, [selectedProvider, type, episodeStreams, groupedStreams]);
|
|
||||||
|
|
||||||
const episodeImage = useMemo(() => {
|
const episodeImage = useMemo(() => {
|
||||||
if (episodeThumbnail) {
|
if (episodeThumbnail) {
|
||||||
|
|
@ -993,11 +1086,7 @@ export const StreamsScreen = () => {
|
||||||
const isProviderLoading = loadingProviders[section.addonId];
|
const isProviderLoading = loadingProviders[section.addonId];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<View style={styles.sectionHeaderContainer}>
|
||||||
entering={FadeIn.duration(400)}
|
|
||||||
exiting={FadeOut.duration(150)}
|
|
||||||
style={styles.sectionHeaderContainer}
|
|
||||||
>
|
|
||||||
<View style={styles.sectionHeaderContent}>
|
<View style={styles.sectionHeaderContent}>
|
||||||
<Text style={styles.streamGroupTitle}>{section.title}</Text>
|
<Text style={styles.streamGroupTitle}>{section.title}</Text>
|
||||||
{isProviderLoading && (
|
{isProviderLoading && (
|
||||||
|
|
@ -1009,7 +1098,7 @@ export const StreamsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</View>
|
||||||
);
|
);
|
||||||
}, [styles.streamGroupTitle, styles.sectionHeaderContainer, styles.sectionHeaderContent, styles.sectionLoadingIndicator, styles.sectionLoadingText, loadingProviders, colors.primary]);
|
}, [styles.streamGroupTitle, styles.sectionHeaderContainer, styles.sectionHeaderContent, styles.sectionLoadingIndicator, styles.sectionLoadingText, loadingProviders, colors.primary]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue