mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
streamsceen scrollview changed to sectionlist
This commit is contained in:
parent
f6dea03c05
commit
de7fcb4d4d
3 changed files with 185 additions and 170 deletions
|
|
@ -1,12 +1,11 @@
|
||||||
import React, { memo, useEffect, useState } from 'react';
|
import React, { memo, useEffect, useState, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
SectionList,
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
|
@ -310,78 +309,83 @@ const TabletStreamsLayout: React.FC<TabletStreamsLayoutProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 }) => (
|
||||||
|
<StreamCard
|
||||||
|
stream={item}
|
||||||
|
onPress={() => 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 (
|
||||||
|
<View style={styles.footerLoading}>
|
||||||
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
|
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemLayout = (data: any, index: number) => ({
|
||||||
|
length: 78,
|
||||||
|
offset: 78 * index,
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<SectionList
|
||||||
style={styles.streamsContent}
|
sections={sectionListData}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
renderItem={renderItem}
|
||||||
|
renderSectionHeader={({ section }) => renderSectionHeader({ section })}
|
||||||
|
ListFooterComponent={ListFooterComponent}
|
||||||
|
stickySectionHeadersEnabled={false}
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
styles.streamsContainer,
|
styles.streamsContainer,
|
||||||
{ paddingBottom: insets.bottom + 100 }
|
{ paddingBottom: insets.bottom + 100 }
|
||||||
]}
|
]}
|
||||||
|
style={styles.streamsContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
bounces={true}
|
initialNumToRender={5}
|
||||||
overScrollMode="never"
|
maxToRenderPerBatch={3}
|
||||||
scrollEventThrottle={16}
|
updateCellsBatchingPeriod={100}
|
||||||
>
|
windowSize={3}
|
||||||
{sections.filter(Boolean).map((section, sectionIndex) => (
|
removeClippedSubviews={true}
|
||||||
<View key={section!.addonId || sectionIndex}>
|
getItemLayout={getItemLayout}
|
||||||
{renderSectionHeader({ section: section! })}
|
/>
|
||||||
|
|
||||||
{section!.data && section!.data.length > 0 ? (
|
|
||||||
<FlatList
|
|
||||||
data={section!.data}
|
|
||||||
keyExtractor={(item, index) => {
|
|
||||||
if (item && item.url) {
|
|
||||||
return `${item.url}-${sectionIndex}-${index}`;
|
|
||||||
}
|
|
||||||
return `empty-${sectionIndex}-${index}`;
|
|
||||||
}}
|
|
||||||
renderItem={({ item, index }) => (
|
|
||||||
<View>
|
|
||||||
<StreamCard
|
|
||||||
stream={item}
|
|
||||||
onPress={() => 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}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
scrollEnabled={false}
|
|
||||||
initialNumToRender={6}
|
|
||||||
maxToRenderPerBatch={2}
|
|
||||||
windowSize={3}
|
|
||||||
removeClippedSubviews={true}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
getItemLayout={(data, index) => ({
|
|
||||||
length: 78,
|
|
||||||
offset: 78 * index,
|
|
||||||
index,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && (
|
|
||||||
<View style={styles.footerLoading}>
|
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
|
||||||
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,15 +95,18 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
// Dual video engine state: ExoPlayer primary, MPV fallback
|
// Dual video engine state: ExoPlayer primary, MPV fallback
|
||||||
// If videoPlayerEngine is 'mpv', always use MPV; otherwise use auto behavior
|
// 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);
|
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(() => {
|
useEffect(() => {
|
||||||
if (settings.videoPlayerEngine === 'mpv') {
|
if (!hasAppliedEngineSettingRef.current && settings.videoPlayerEngine === 'mpv') {
|
||||||
|
hasAppliedEngineSettingRef.current = true;
|
||||||
setUseExoPlayer(false);
|
setUseExoPlayer(false);
|
||||||
}
|
}
|
||||||
// Note: We don't reset to true when 'auto' because ExoPlayer might have failed
|
|
||||||
}, [settings.videoPlayerEngine]);
|
}, [settings.videoPlayerEngine]);
|
||||||
|
|
||||||
// Subtitle addon state
|
// Subtitle addon state
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React, { memo, useCallback } from 'react';
|
import React, { memo, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
ScrollView,
|
SectionList,
|
||||||
FlatList,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Platform,
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
@ -86,107 +85,116 @@ const StreamsList = memo(
|
||||||
[loadingProviders, styles, colors.primary]
|
[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 }) => (
|
||||||
|
<StreamCard
|
||||||
|
stream={item}
|
||||||
|
onPress={() => 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 (
|
||||||
|
<View style={styles.autoplayOverlay}>
|
||||||
|
<View style={styles.autoplayIndicator}>
|
||||||
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
|
<Text style={styles.autoplayText}>Starting best stream...</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [isAutoplayWaiting, autoplayTriggered, styles, colors.primary]);
|
||||||
|
|
||||||
|
const ListFooterComponent = useMemo(() => {
|
||||||
|
if (!(loadingStreams || loadingEpisodeStreams) || !hasStremioStreamProviders) return null;
|
||||||
|
return (
|
||||||
|
<View style={styles.footerLoading}>
|
||||||
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
|
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [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 }}>
|
||||||
{/* Autoplay overlay */}
|
<SectionList
|
||||||
{isAutoplayWaiting && !autoplayTriggered && (
|
sections={sectionListData}
|
||||||
<View style={styles.autoplayOverlay}>
|
keyExtractor={keyExtractor}
|
||||||
<View style={styles.autoplayIndicator}>
|
renderItem={renderItem}
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
renderSectionHeader={renderSectionHeader}
|
||||||
<Text style={styles.autoplayText}>Starting best stream...</Text>
|
ListHeaderComponent={ListHeaderComponent}
|
||||||
</View>
|
ListFooterComponent={ListFooterComponent}
|
||||||
</View>
|
stickySectionHeadersEnabled={false}
|
||||||
)}
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
style={styles.streamsContent}
|
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
styles.streamsContainer,
|
styles.streamsContainer,
|
||||||
{ paddingBottom: insets.bottom + 100 },
|
{ paddingBottom: insets.bottom + 100 },
|
||||||
]}
|
]}
|
||||||
|
style={styles.streamsContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
bounces={true}
|
initialNumToRender={5}
|
||||||
overScrollMode="never"
|
maxToRenderPerBatch={3}
|
||||||
{...(Platform.OS === 'ios' && {
|
updateCellsBatchingPeriod={100}
|
||||||
removeClippedSubviews: false,
|
windowSize={3}
|
||||||
scrollEventThrottle: 16,
|
removeClippedSubviews={true}
|
||||||
})}
|
getItemLayout={getItemLayout}
|
||||||
>
|
/>
|
||||||
{sections.filter(Boolean).map((section, sectionIndex) => (
|
|
||||||
<View key={section!.addonId || sectionIndex}>
|
|
||||||
{renderSectionHeader({ section: section! })}
|
|
||||||
|
|
||||||
{section!.data && section!.data.length > 0 ? (
|
|
||||||
<FlatList
|
|
||||||
data={section!.data}
|
|
||||||
keyExtractor={(item, index) => {
|
|
||||||
if (item && item.url) {
|
|
||||||
return `${item.url}-${sectionIndex}-${index}`;
|
|
||||||
}
|
|
||||||
return `empty-${sectionIndex}-${index}`;
|
|
||||||
}}
|
|
||||||
renderItem={({ item, index }) => (
|
|
||||||
<View>
|
|
||||||
<StreamCard
|
|
||||||
stream={item}
|
|
||||||
onPress={() => 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}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
scrollEnabled={false}
|
|
||||||
initialNumToRender={6}
|
|
||||||
maxToRenderPerBatch={2}
|
|
||||||
windowSize={3}
|
|
||||||
removeClippedSubviews={true}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
getItemLayout={(data, index) => ({
|
|
||||||
length: 78,
|
|
||||||
offset: 78 * index,
|
|
||||||
index,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Footer Loading */}
|
|
||||||
{(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && (
|
|
||||||
<View style={styles.footerLoading}>
|
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
|
||||||
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue