diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index 5a4738c66..c3d3b0056 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -41,6 +41,7 @@ export interface AppSettings {
enableLocalScrapers: boolean; // Enable/disable local scraper functionality
scraperTimeout: number; // Timeout for scraper execution in seconds
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 = {
@@ -64,6 +65,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
enableLocalScrapers: true,
scraperTimeout: 60, // 60 seconds timeout
enableScraperUrlValidation: true, // Enable URL validation by default
+ streamDisplayMode: 'separate', // Default to separate display by provider
};
const SETTINGS_STORAGE_KEY = 'app_settings';
diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx
index 9c3ef0d58..53dca9028 100644
--- a/src/screens/PluginsScreen.tsx
+++ b/src/screens/PluginsScreen.tsx
@@ -739,6 +739,22 @@ const PluginsScreen: React.FC = () => {
disabled={!settings.enableLocalScrapers}
/>
+
+
+
+ Stream Display Mode
+
+ {settings.streamDisplayMode === 'separate' ? 'Show each provider separately' : 'Group all providers under one name'}
+
+
+ updateSetting('streamDisplayMode', value ? 'grouped' : 'separate')}
+ trackColor={{ false: colors.elevation3, true: colors.primary }}
+ thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
+ disabled={!settings.enableLocalScrapers}
+ />
+
diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx
index 393992569..e412a2137 100644
--- a/src/screens/StreamsScreen.tsx
+++ b/src/screens/StreamsScreen.tsx
@@ -101,15 +101,8 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
};
}, [stream.name, stream.title, stream.behaviorHints, stream.size]);
- // Animation delay based on index - stagger effect
- const enterDelay = 100 + (index * 30);
-
return (
-
-
-
);
});
@@ -232,11 +224,7 @@ const ProviderFilter = memo(({
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
-
-
-
), [selectedProvider, onSelect, styles]);
return (
-
+
-
+
);
});
@@ -385,7 +370,12 @@ export const StreamsScreen = () => {
if (providersWithStreams.length > 0) {
logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`);
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
@@ -407,11 +397,28 @@ export const StreamsScreen = () => {
}, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type]);
// 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(() => {
- 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');
}
- }, [selectedProvider, availableProviders]);
+ }, [selectedProvider, availableProviders, episodeStreams, groupedStreams, type]);
// Update useEffect to check for sources
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 [
{ id: 'all', name: 'All Providers' },
...Array.from(allProviders)
@@ -889,19 +933,26 @@ export const StreamsScreen = () => {
return { id: provider, name: displayName };
})
];
- }, [availableProviders, type, episodeStreams, groupedStreams]);
+ }, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
const sections = useMemo(() => {
const streams = type === 'series' ? episodeStreams : groupedStreams;
const installedAddons = stremioService.getInstalledAddons();
- // Filter streams by selected provider - only if not "all"
+ // Filter streams by selected provider
const filteredEntries = Object.entries(streams)
.filter(([addonId]) => {
// If "all" is selected, show all providers
if (selectedProvider === 'all') {
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
return addonId === selectedProvider;
})
@@ -914,17 +965,59 @@ export const StreamsScreen = () => {
if (indexA !== -1) return -1;
if (indexB !== -1) return 1;
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 {
title: addonName,
addonId,
data: providerStreams
};
});
-
- return filteredEntries;
- }, [selectedProvider, type, episodeStreams, groupedStreams]);
+ }
+ }, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
const episodeImage = useMemo(() => {
if (episodeThumbnail) {
@@ -993,11 +1086,7 @@ export const StreamsScreen = () => {
const isProviderLoading = loadingProviders[section.addonId];
return (
-
+
{section.title}
{isProviderLoading && (
@@ -1009,7 +1098,7 @@ export const StreamsScreen = () => {
)}
-
+
);
}, [styles.streamGroupTitle, styles.sectionHeaderContainer, styles.sectionHeaderContent, styles.sectionLoadingIndicator, styles.sectionLoadingText, loadingProviders, colors.primary]);