converted from sectionlist to flatflist (streamscreen)

This commit is contained in:
tapframe 2025-12-28 03:41:33 +05:30
parent 43cd14a025
commit 7fdd4c4383
2 changed files with 135 additions and 138 deletions

View file

@ -4,10 +4,10 @@ import {
Text, Text,
StyleSheet, StyleSheet,
ActivityIndicator, ActivityIndicator,
SectionList,
Platform, Platform,
TouchableOpacity, TouchableOpacity,
} from 'react-native'; } from 'react-native';
import { LegendList } from '@legendapp/list';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
@ -309,42 +309,56 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
} }
} }
// Convert sections to SectionList format // Flatten sections into a single list with header items
const sectionListData = sections type ListItem = { type: 'header'; title: string; addonId: string } | { type: 'stream'; stream: Stream; index: number };
const flatListData: ListItem[] = [];
sections
.filter(Boolean) .filter(Boolean)
.filter(section => section!.data && section!.data.length > 0) .filter(section => section!.data && section!.data.length > 0)
.map(section => ({ .forEach(section => {
title: section!.title, flatListData.push({ type: 'header', title: section!.title, addonId: section!.addonId });
addonId: section!.addonId, section!.data.forEach((stream, index) => {
data: section!.data, flatListData.push({ type: 'stream', stream, index });
})); });
});
const renderItem = ({ item, index }: { item: Stream; index: number }) => ( const renderItem = ({ item }: { item: ListItem }) => {
<StreamCard if (item.type === 'header') {
stream={item} return renderSectionHeader({ section: { title: item.title, addonId: item.addonId } });
onPress={() => handleStreamPress(item)} }
index={index}
isLoading={false} const stream = item.stream;
statusMessage={undefined} return (
theme={currentTheme} <StreamCard
showLogos={settings.showScraperLogos} stream={stream}
scraperLogo={(item.addonId && scraperLogos[item.addonId]) || (item as any).addon ? scraperLogos[(item.addonId || (item as any).addon) as string] || null : null} onPress={() => handleStreamPress(stream)}
showAlert={(t: string, m: string) => openAlert(t, m)} index={item.index}
parentTitle={metadata?.name} isLoading={false}
parentType={type as 'movie' | 'series'} statusMessage={undefined}
parentSeason={(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined} theme={currentTheme}
parentEpisode={(type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined} showLogos={settings.showScraperLogos}
parentEpisodeTitle={(type === 'series' || type === 'other') ? currentEpisode?.name : undefined} scraperLogo={(stream.addonId && scraperLogos[stream.addonId]) || (stream as any).addon ? scraperLogos[(stream.addonId || (stream as any).addon) as string] || null : null}
parentPosterUrl={episodeImage || metadata?.poster || undefined} showAlert={(t: string, m: string) => openAlert(t, m)}
providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))} parentTitle={metadata?.name}
parentId={id} parentType={type as 'movie' | 'series'}
parentImdbId={imdbId || undefined} parentSeason={(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined}
/> parentEpisode={(type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined}
); parentEpisodeTitle={(type === 'series' || type === 'other') ? currentEpisode?.name : undefined}
parentPosterUrl={episodeImage || metadata?.poster || undefined}
providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(stream))}
parentId={id}
parentImdbId={imdbId || undefined}
/>
);
};
const keyExtractor = (item: Stream, index: number) => { const keyExtractor = (item: ListItem, index: number) => {
if (item && item.url) { if (item.type === 'header') {
return `${item.url}-${index}`; return `header-${item.addonId}-${index}`;
}
if (item.stream && item.stream.url) {
return `stream-${item.stream.url}-${index}`;
} }
return `empty-${index}`; return `empty-${index}`;
}; };
@ -359,32 +373,20 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
); );
}; };
const getItemLayout = (data: any, index: number) => ({
length: 78,
offset: 78 * index,
index,
});
return ( return (
<SectionList <LegendList
sections={sectionListData} data={flatListData}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
renderItem={renderItem} renderItem={renderItem}
renderSectionHeader={({ section }) => renderSectionHeader({ section })}
ListFooterComponent={ListFooterComponent} ListFooterComponent={ListFooterComponent}
stickySectionHeadersEnabled={false}
contentContainerStyle={[ contentContainerStyle={[
styles.streamsContainer, styles.streamsContainer,
{ paddingBottom: insets.bottom + 100 } { paddingBottom: insets.bottom + 100 }
]} ]}
style={styles.streamsContent} style={styles.streamsContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
initialNumToRender={5} recycleItems={true}
maxToRenderPerBatch={3} estimatedItemSize={78}
updateCellsBatchingPeriod={100}
windowSize={3}
removeClippedSubviews={true}
getItemLayout={getItemLayout}
/> />
); );
}; };

View file

@ -3,10 +3,10 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
SectionList,
ActivityIndicator, ActivityIndicator,
Platform, Platform,
} from 'react-native'; } from 'react-native';
import { LegendList } from '@legendapp/list';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import StreamCard from '../../../components/StreamCard'; import StreamCard from '../../../components/StreamCard';
@ -62,84 +62,91 @@ const StreamsList = memo(
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const styles = React.useMemo(() => createStyles(colors), [colors]); const styles = React.useMemo(() => createStyles(colors), [colors]);
const renderSectionHeader = useCallback( // Flatten sections into a single list with header items
({ section }: { section: StreamSection }) => { type ListItem = { type: 'header'; title: string; addonId: string } | { type: 'stream'; stream: Stream; index: number };
const isProviderLoading = loadingProviders[section.addonId];
const flatListData = useMemo(() => {
return ( const items: ListItem[] = [];
<View style={styles.sectionHeaderContainer}> sections
<View style={styles.sectionHeaderContent}>
<Text style={styles.streamGroupTitle}>{section.title}</Text>
{isProviderLoading && (
<View style={styles.sectionLoadingIndicator}>
<ActivityIndicator size="small" color={colors.primary} />
<Text style={[styles.sectionLoadingText, { color: colors.primary }]}>
Loading...
</Text>
</View>
)}
</View>
</View>
);
},
[loadingProviders, styles, colors.primary]
);
// Convert sections to SectionList format
const sectionListData = useMemo(() => {
return sections
.filter(Boolean) .filter(Boolean)
.filter(section => section!.data && section!.data.length > 0) .filter(section => section!.data && section!.data.length > 0)
.map(section => ({ .forEach(section => {
title: section!.title, items.push({ type: 'header', title: section!.title, addonId: section!.addonId });
addonId: section!.addonId, section!.data.forEach((stream, index) => {
data: section!.data, items.push({ type: 'stream', stream, index });
})); });
});
return items;
}, [sections]); }, [sections]);
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: { item: Stream; index: number }) => ( ({ item }: { item: ListItem }) => {
<StreamCard if (item.type === 'header') {
stream={item} const isProviderLoading = loadingProviders[item.addonId];
onPress={() => handleStreamPress(item)} return (
index={index} <View style={styles.sectionHeaderContainer}>
isLoading={false} <View style={styles.sectionHeaderContent}>
statusMessage={undefined} <Text style={styles.streamGroupTitle}>{item.title}</Text>
theme={currentTheme} {isProviderLoading && (
showLogos={settings.showScraperLogos} <View style={styles.sectionLoadingIndicator}>
scraperLogo={ <ActivityIndicator size="small" color={colors.primary} />
(item.addonId && scraperLogos[item.addonId]) || <Text style={[styles.sectionLoadingText, { color: colors.primary }]}>
((item as any).addon ? scraperLogos[(item.addonId || (item as any).addon) as string] || null : null) Loading...
} </Text>
showAlert={(t: string, m: string) => openAlert(t, m)} </View>
parentTitle={metadata?.name} )}
parentType={type as 'movie' | 'series'} </View>
parentSeason={ </View>
(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined );
} }
parentEpisode={
(type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined const stream = item.stream;
} return (
parentEpisodeTitle={ <StreamCard
(type === 'series' || type === 'other') ? currentEpisode?.name : undefined stream={stream}
} onPress={() => handleStreamPress(stream)}
parentPosterUrl={episodeImage || metadata?.poster || undefined} index={item.index}
providerName={ isLoading={false}
streams && statusMessage={undefined}
Object.keys(streams).find(pid => theme={currentTheme}
(streams as any)[pid]?.streams?.includes?.(item) showLogos={settings.showScraperLogos}
) scraperLogo={
} (stream.addonId && scraperLogos[stream.addonId]) ||
parentId={id} ((stream as any).addon ? scraperLogos[(stream.addonId || (stream as any).addon) as string] || null : null)
parentImdbId={imdbId} }
/> showAlert={(t: string, m: string) => openAlert(t, m)}
), parentTitle={metadata?.name}
[handleStreamPress, currentTheme, settings.showScraperLogos, scraperLogos, openAlert, metadata, type, currentEpisode, episodeImage, streams, id, imdbId] parentType={type as 'movie' | 'series'}
parentSeason={
(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined
}
parentEpisode={
(type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined
}
parentEpisodeTitle={
(type === 'series' || type === 'other') ? currentEpisode?.name : undefined
}
parentPosterUrl={episodeImage || metadata?.poster || undefined}
providerName={
streams &&
Object.keys(streams).find(pid =>
(streams as any)[pid]?.streams?.includes?.(item.stream)
)
}
parentId={id}
parentImdbId={imdbId}
/>
);
},
[handleStreamPress, currentTheme, settings.showScraperLogos, scraperLogos, openAlert, metadata, type, currentEpisode, episodeImage, streams, id, imdbId, loadingProviders, styles, colors.primary]
); );
const keyExtractor = useCallback((item: Stream, index: number) => { const keyExtractor = useCallback((item: ListItem, index: number) => {
if (item && item.url) { if (item.type === 'header') {
return `${item.url}-${index}`; return `header-${item.addonId}-${index}`;
}
if (item.stream && item.stream.url) {
return `stream-${item.stream.url}-${index}`;
} }
return `empty-${index}`; return `empty-${index}`;
}, []); }, []);
@ -166,34 +173,22 @@ const StreamsList = memo(
); );
}, [loadingStreams, loadingEpisodeStreams, hasStremioStreamProviders, styles, colors.primary]); }, [loadingStreams, loadingEpisodeStreams, hasStremioStreamProviders, styles, colors.primary]);
const getItemLayout = useCallback((data: any, index: number) => ({
length: 78,
offset: 78 * index,
index,
}), []);
return ( return (
<View collapsable={false} style={{ flex: 1 }}> <View collapsable={false} style={{ flex: 1 }}>
<SectionList {ListHeaderComponent}
sections={sectionListData} <LegendList
data={flatListData}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
renderItem={renderItem} renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={ListFooterComponent} ListFooterComponent={ListFooterComponent}
stickySectionHeadersEnabled={false}
contentContainerStyle={[ contentContainerStyle={[
styles.streamsContainer, styles.streamsContainer,
{ paddingBottom: insets.bottom + 100 }, { paddingBottom: insets.bottom + 100 },
]} ]}
style={styles.streamsContent} style={styles.streamsContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
initialNumToRender={5} recycleItems={true}
maxToRenderPerBatch={3} estimatedItemSize={78}
updateCellsBatchingPeriod={100}
windowSize={3}
removeClippedSubviews={true}
getItemLayout={getItemLayout}
/> />
</View> </View>
); );