From de7fcb4d4dffc5d4ecba54c479492826338f3cc3 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 28 Dec 2025 02:59:26 +0530 Subject: [PATCH] streamsceen scrollview changed to sectionlist --- src/components/TabletStreamsLayout.tsx | 142 ++++++------ src/components/player/AndroidVideoPlayer.tsx | 11 +- .../streams/components/StreamsList.tsx | 202 +++++++++--------- 3 files changed, 185 insertions(+), 170 deletions(-) diff --git a/src/components/TabletStreamsLayout.tsx b/src/components/TabletStreamsLayout.tsx index 16fe57e..d05c58a 100644 --- a/src/components/TabletStreamsLayout.tsx +++ b/src/components/TabletStreamsLayout.tsx @@ -1,12 +1,11 @@ -import React, { memo, useEffect, useState } from 'react'; +import React, { memo, useEffect, useState, useCallback, useMemo } from 'react'; import { View, Text, StyleSheet, ActivityIndicator, - FlatList, + SectionList, Platform, - ScrollView, TouchableOpacity, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; @@ -310,78 +309,83 @@ const TabletStreamsLayout: React.FC = ({ } } + // Convert sections to SectionList format + const sectionListData = sections + .filter(Boolean) + .filter(section => section!.data && section!.data.length > 0) + .map(section => ({ + title: section!.title, + addonId: section!.addonId, + data: section!.data, + })); + + const renderItem = ({ item, index }: { item: Stream; index: number }) => ( + handleStreamPress(item)} + index={index} + isLoading={false} + statusMessage={undefined} + theme={currentTheme} + showLogos={settings.showScraperLogos} + scraperLogo={(item.addonId && scraperLogos[item.addonId]) || (item as any).addon ? scraperLogos[(item.addonId || (item as any).addon) as string] || null : null} + showAlert={(t: string, m: string) => openAlert(t, m)} + parentTitle={metadata?.name} + 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))} + parentId={id} + parentImdbId={imdbId || undefined} + /> + ); + + const keyExtractor = (item: Stream, index: number) => { + if (item && item.url) { + return `${item.url}-${index}`; + } + return `empty-${index}`; + }; + + const ListFooterComponent = () => { + if (!(loadingStreams || loadingEpisodeStreams) || !hasStremioStreamProviders) return null; + return ( + + + Loading more sources... + + ); + }; + + const getItemLayout = (data: any, index: number) => ({ + length: 78, + offset: 78 * index, + index, + }); + return ( - renderSectionHeader({ section })} + ListFooterComponent={ListFooterComponent} + stickySectionHeadersEnabled={false} contentContainerStyle={[ styles.streamsContainer, { paddingBottom: insets.bottom + 100 } ]} + style={styles.streamsContent} showsVerticalScrollIndicator={false} - bounces={true} - overScrollMode="never" - scrollEventThrottle={16} - > - {sections.filter(Boolean).map((section, sectionIndex) => ( - - {renderSectionHeader({ section: section! })} - - {section!.data && section!.data.length > 0 ? ( - { - if (item && item.url) { - return `${item.url}-${sectionIndex}-${index}`; - } - return `empty-${sectionIndex}-${index}`; - }} - renderItem={({ item, index }) => ( - - handleStreamPress(item)} - index={index} - isLoading={false} - statusMessage={undefined} - theme={currentTheme} - showLogos={settings.showScraperLogos} - scraperLogo={(item.addonId && scraperLogos[item.addonId]) || (item as any).addon ? scraperLogos[(item.addonId || (item as any).addon) as string] || null : null} - showAlert={(t: string, m: string) => openAlert(t, m)} - parentTitle={metadata?.name} - 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))} - parentId={id} - parentImdbId={imdbId || undefined} - /> - - )} - scrollEnabled={false} - initialNumToRender={6} - maxToRenderPerBatch={2} - windowSize={3} - removeClippedSubviews={true} - showsVerticalScrollIndicator={false} - getItemLayout={(data, index) => ({ - length: 78, - offset: 78 * index, - index, - })} - /> - ) : null} - - ))} - - {(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && ( - - - Loading more sources... - - )} - + initialNumToRender={5} + maxToRenderPerBatch={3} + updateCellsBatchingPeriod={100} + windowSize={3} + removeClippedSubviews={true} + getItemLayout={getItemLayout} + /> ); }; diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 39f1f59..d2d9ba4 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -95,15 +95,18 @@ const AndroidVideoPlayer: React.FC = () => { // Dual video engine state: ExoPlayer primary, MPV fallback // If videoPlayerEngine is 'mpv', always use MPV; otherwise use auto behavior - const [useExoPlayer, setUseExoPlayer] = useState(settings.videoPlayerEngine !== 'mpv'); + const shouldUseMpvOnly = settings.videoPlayerEngine === 'mpv'; + const [useExoPlayer, setUseExoPlayer] = useState(!shouldUseMpvOnly); const hasExoPlayerFailed = useRef(false); - // Sync useExoPlayer with settings when videoPlayerEngine changes + // Sync useExoPlayer with settings when videoPlayerEngine is set to 'mpv' + // Only run once on mount to avoid re-render loops + const hasAppliedEngineSettingRef = useRef(false); useEffect(() => { - if (settings.videoPlayerEngine === 'mpv') { + if (!hasAppliedEngineSettingRef.current && settings.videoPlayerEngine === 'mpv') { + hasAppliedEngineSettingRef.current = true; setUseExoPlayer(false); } - // Note: We don't reset to true when 'auto' because ExoPlayer might have failed }, [settings.videoPlayerEngine]); // Subtitle addon state diff --git a/src/screens/streams/components/StreamsList.tsx b/src/screens/streams/components/StreamsList.tsx index 1844067..392255e 100644 --- a/src/screens/streams/components/StreamsList.tsx +++ b/src/screens/streams/components/StreamsList.tsx @@ -1,10 +1,9 @@ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import { View, Text, StyleSheet, - ScrollView, - FlatList, + SectionList, ActivityIndicator, Platform, } from 'react-native'; @@ -86,107 +85,116 @@ const StreamsList = memo( [loadingProviders, styles, colors.primary] ); + // Convert sections to SectionList format + const sectionListData = useMemo(() => { + return sections + .filter(Boolean) + .filter(section => section!.data && section!.data.length > 0) + .map(section => ({ + title: section!.title, + addonId: section!.addonId, + data: section!.data, + })); + }, [sections]); + + const renderItem = useCallback( + ({ item, index }: { item: Stream; index: number }) => ( + handleStreamPress(item)} + index={index} + isLoading={false} + statusMessage={undefined} + theme={currentTheme} + showLogos={settings.showScraperLogos} + scraperLogo={ + (item.addonId && scraperLogos[item.addonId]) || + ((item as any).addon ? scraperLogos[(item.addonId || (item as any).addon) as string] || null : null) + } + showAlert={(t: string, m: string) => openAlert(t, m)} + parentTitle={metadata?.name} + 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) + ) + } + parentId={id} + parentImdbId={imdbId} + /> + ), + [handleStreamPress, currentTheme, settings.showScraperLogos, scraperLogos, openAlert, metadata, type, currentEpisode, episodeImage, streams, id, imdbId] + ); + + const keyExtractor = useCallback((item: Stream, index: number) => { + if (item && item.url) { + return `${item.url}-${index}`; + } + return `empty-${index}`; + }, []); + + const ListHeaderComponent = useMemo(() => { + if (!isAutoplayWaiting || autoplayTriggered) return null; + return ( + + + + Starting best stream... + + + ); + }, [isAutoplayWaiting, autoplayTriggered, styles, colors.primary]); + + const ListFooterComponent = useMemo(() => { + if (!(loadingStreams || loadingEpisodeStreams) || !hasStremioStreamProviders) return null; + return ( + + + Loading more sources... + + ); + }, [loadingStreams, loadingEpisodeStreams, hasStremioStreamProviders, styles, colors.primary]); + + const getItemLayout = useCallback((data: any, index: number) => ({ + length: 78, + offset: 78 * index, + index, + }), []); + return ( - {/* Autoplay overlay */} - {isAutoplayWaiting && !autoplayTriggered && ( - - - - Starting best stream... - - - )} - - - {sections.filter(Boolean).map((section, sectionIndex) => ( - - {renderSectionHeader({ section: section! })} - - {section!.data && section!.data.length > 0 ? ( - { - if (item && item.url) { - return `${item.url}-${sectionIndex}-${index}`; - } - return `empty-${sectionIndex}-${index}`; - }} - renderItem={({ item, index }) => ( - - handleStreamPress(item)} - index={index} - isLoading={false} - statusMessage={undefined} - theme={currentTheme} - showLogos={settings.showScraperLogos} - scraperLogo={ - (item.addonId && scraperLogos[item.addonId]) || - ((item as any).addon ? scraperLogos[(item.addonId || (item as any).addon) as string] || null : null) - } - showAlert={(t: string, m: string) => openAlert(t, m)} - parentTitle={metadata?.name} - 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) - ) - } - parentId={id} - parentImdbId={imdbId} - /> - - )} - scrollEnabled={false} - initialNumToRender={6} - maxToRenderPerBatch={2} - windowSize={3} - removeClippedSubviews={true} - showsVerticalScrollIndicator={false} - getItemLayout={(data, index) => ({ - length: 78, - offset: 78 * index, - index, - })} - /> - ) : null} - - ))} - - {/* Footer Loading */} - {(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && ( - - - Loading more sources... - - )} - + initialNumToRender={5} + maxToRenderPerBatch={3} + updateCellsBatchingPeriod={100} + windowSize={3} + removeClippedSubviews={true} + getItemLayout={getItemLayout} + /> ); }