mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-22 01:01:56 +00:00
ui changes
This commit is contained in:
parent
581e912d4c
commit
b5b61d05f8
1 changed files with 164 additions and 142 deletions
|
|
@ -381,27 +381,41 @@ const SearchScreen = () => {
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const handle = catalogService.startLiveSearch(searchQuery, async (section: AddonSearchResults) => {
|
const handle = catalogService.startLiveSearch(searchQuery, async (section: AddonSearchResults) => {
|
||||||
// Append/update this addon section immediately
|
// Append/update this addon section immediately with minimal changes
|
||||||
setResults(prev => {
|
setResults(prev => {
|
||||||
const existingIndex = prev.byAddon.findIndex(s => s.addonId === section.addonId);
|
|
||||||
let nextByAddon: AddonSearchResults[];
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
nextByAddon = prev.byAddon.slice();
|
|
||||||
nextByAddon[existingIndex] = section;
|
|
||||||
} else {
|
|
||||||
nextByAddon = [...prev.byAddon, section];
|
|
||||||
}
|
|
||||||
// Sort by installation order
|
|
||||||
const rank = addonOrderRankRef.current;
|
const rank = addonOrderRankRef.current;
|
||||||
nextByAddon = nextByAddon.slice().sort((a, b) => {
|
const getRank = (id: string) => rank[id] ?? Number.MAX_SAFE_INTEGER;
|
||||||
const ra = rank[a.addonId] ?? Number.MAX_SAFE_INTEGER;
|
|
||||||
const rb = rank[b.addonId] ?? Number.MAX_SAFE_INTEGER;
|
const existingIndex = prev.byAddon.findIndex(s => s.addonId === section.addonId);
|
||||||
return ra - rb;
|
|
||||||
});
|
if (existingIndex >= 0) {
|
||||||
|
// Update existing section in-place (preserve order and other sections)
|
||||||
|
const copy = prev.byAddon.slice();
|
||||||
|
copy[existingIndex] = section;
|
||||||
|
return { byAddon: copy, allResults: prev.allResults };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new section at correct position based on rank
|
||||||
|
const insertRank = getRank(section.addonId);
|
||||||
|
let insertAt = prev.byAddon.length;
|
||||||
|
for (let i = 0; i < prev.byAddon.length; i++) {
|
||||||
|
if (getRank(prev.byAddon[i].addonId) > insertRank) {
|
||||||
|
insertAt = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextByAddon = [
|
||||||
|
...prev.byAddon.slice(0, insertAt),
|
||||||
|
section,
|
||||||
|
...prev.byAddon.slice(insertAt)
|
||||||
|
];
|
||||||
|
|
||||||
// Hide loading overlay once first section arrives
|
// Hide loading overlay once first section arrives
|
||||||
if (nextByAddon.length === 1) {
|
if (prev.byAddon.length === 0) {
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { byAddon: nextByAddon, allResults: prev.allResults };
|
return { byAddon: nextByAddon, allResults: prev.allResults };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -601,6 +615,131 @@ const SearchScreen = () => {
|
||||||
return results.byAddon.length > 0;
|
return results.byAddon.length > 0;
|
||||||
}, [results]);
|
}, [results]);
|
||||||
|
|
||||||
|
// Memoized addon section to prevent re-rendering unchanged sections
|
||||||
|
const AddonSection = React.memo(({
|
||||||
|
addonGroup,
|
||||||
|
addonIndex
|
||||||
|
}: {
|
||||||
|
addonGroup: AddonSearchResults;
|
||||||
|
addonIndex: number;
|
||||||
|
}) => {
|
||||||
|
const movieResults = useMemo(() =>
|
||||||
|
addonGroup.results.filter(item => item.type === 'movie'),
|
||||||
|
[addonGroup.results]
|
||||||
|
);
|
||||||
|
const seriesResults = useMemo(() =>
|
||||||
|
addonGroup.results.filter(item => item.type === 'series'),
|
||||||
|
[addonGroup.results]
|
||||||
|
);
|
||||||
|
const otherResults = useMemo(() =>
|
||||||
|
addonGroup.results.filter(item => item.type !== 'movie' && item.type !== 'series'),
|
||||||
|
[addonGroup.results]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View entering={FadeIn.duration(300).delay(addonIndex * 50)}>
|
||||||
|
{/* Addon Header */}
|
||||||
|
<View style={styles.addonHeaderContainer}>
|
||||||
|
<Text style={[styles.addonHeaderText, { color: currentTheme.colors.white }]}>
|
||||||
|
{addonGroup.addonName}
|
||||||
|
</Text>
|
||||||
|
<View style={[styles.addonHeaderBadge, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||||
|
<Text style={[styles.addonHeaderBadgeText, { color: currentTheme.colors.lightGray }]}>
|
||||||
|
{addonGroup.results.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Movies */}
|
||||||
|
{movieResults.length > 0 && (
|
||||||
|
<Animated.View style={styles.carouselContainer} entering={FadeIn.duration(300)}>
|
||||||
|
<Text style={[styles.carouselSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||||
|
Movies ({movieResults.length})
|
||||||
|
</Text>
|
||||||
|
<FlatList
|
||||||
|
data={movieResults}
|
||||||
|
renderItem={({ item, index }) => (
|
||||||
|
<SearchResultItem
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
navigation={navigation}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
setMenuVisible={setMenuVisible}
|
||||||
|
currentTheme={currentTheme}
|
||||||
|
refreshFlag={refreshFlag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
keyExtractor={item => `${addonGroup.addonId}-movie-${item.id}`}
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.horizontalListContent}
|
||||||
|
extraData={refreshFlag}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TV Shows */}
|
||||||
|
{seriesResults.length > 0 && (
|
||||||
|
<Animated.View style={styles.carouselContainer} entering={FadeIn.duration(300)}>
|
||||||
|
<Text style={[styles.carouselSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||||
|
TV Shows ({seriesResults.length})
|
||||||
|
</Text>
|
||||||
|
<FlatList
|
||||||
|
data={seriesResults}
|
||||||
|
renderItem={({ item, index }) => (
|
||||||
|
<SearchResultItem
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
navigation={navigation}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
setMenuVisible={setMenuVisible}
|
||||||
|
currentTheme={currentTheme}
|
||||||
|
refreshFlag={refreshFlag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
keyExtractor={item => `${addonGroup.addonId}-series-${item.id}`}
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.horizontalListContent}
|
||||||
|
extraData={refreshFlag}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Other types */}
|
||||||
|
{otherResults.length > 0 && (
|
||||||
|
<Animated.View style={styles.carouselContainer} entering={FadeIn.duration(300)}>
|
||||||
|
<Text style={[styles.carouselSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||||
|
{otherResults[0].type.charAt(0).toUpperCase() + otherResults[0].type.slice(1)} ({otherResults.length})
|
||||||
|
</Text>
|
||||||
|
<FlatList
|
||||||
|
data={otherResults}
|
||||||
|
renderItem={({ item, index }) => (
|
||||||
|
<SearchResultItem
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
navigation={navigation}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
setMenuVisible={setMenuVisible}
|
||||||
|
currentTheme={currentTheme}
|
||||||
|
refreshFlag={refreshFlag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
keyExtractor={item => `${addonGroup.addonId}-${item.type}-${item.id}`}
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.horizontalListContent}
|
||||||
|
extraData={refreshFlag}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}, (prev, next) => {
|
||||||
|
// Only re-render if this section's reference changed
|
||||||
|
return prev.addonGroup === next.addonGroup && prev.addonIndex === next.addonIndex;
|
||||||
|
});
|
||||||
|
|
||||||
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
|
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
|
||||||
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
|
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
|
||||||
const headerHeight = headerBaseHeight + topSpacing + 60;
|
const headerHeight = headerBaseHeight + topSpacing + 60;
|
||||||
|
|
@ -741,131 +880,14 @@ const SearchScreen = () => {
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
{!query.trim() && renderRecentSearches()}
|
{!query.trim() && renderRecentSearches()}
|
||||||
{/* Render results grouped by addon */}
|
{/* Render results grouped by addon using memoized component */}
|
||||||
{results.byAddon.map((addonGroup, addonIndex) => {
|
{results.byAddon.map((addonGroup, addonIndex) => (
|
||||||
// Group by type within each addon
|
<AddonSection
|
||||||
const movieResults = addonGroup.results.filter(item => item.type === 'movie');
|
key={addonGroup.addonId}
|
||||||
const seriesResults = addonGroup.results.filter(item => item.type === 'series');
|
addonGroup={addonGroup}
|
||||||
const otherResults = addonGroup.results.filter(item => item.type !== 'movie' && item.type !== 'series');
|
addonIndex={addonIndex}
|
||||||
|
/>
|
||||||
return (
|
))}
|
||||||
<Animated.View
|
|
||||||
key={addonGroup.addonId}
|
|
||||||
entering={FadeIn.duration(300).delay(addonIndex * 100)}
|
|
||||||
>
|
|
||||||
{/* Addon Header */}
|
|
||||||
<View style={styles.addonHeaderContainer}>
|
|
||||||
<MaterialIcons
|
|
||||||
name="extension"
|
|
||||||
size={20}
|
|
||||||
color={currentTheme.colors.primary}
|
|
||||||
style={styles.addonHeaderIcon}
|
|
||||||
/>
|
|
||||||
<Text style={[styles.addonHeaderText, { color: currentTheme.colors.white }]}>
|
|
||||||
{addonGroup.addonName}
|
|
||||||
</Text>
|
|
||||||
<View style={[styles.addonHeaderBadge, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
|
||||||
<Text style={[styles.addonHeaderBadgeText, { color: currentTheme.colors.lightGray }]}>
|
|
||||||
{addonGroup.results.length}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Movies from this addon */}
|
|
||||||
{movieResults.length > 0 && (
|
|
||||||
<Animated.View
|
|
||||||
style={styles.carouselContainer}
|
|
||||||
entering={FadeIn.duration(300)}
|
|
||||||
>
|
|
||||||
<Text style={[styles.carouselSubtitle, { color: currentTheme.colors.lightGray }]}>
|
|
||||||
Movies ({movieResults.length})
|
|
||||||
</Text>
|
|
||||||
<FlatList
|
|
||||||
data={movieResults}
|
|
||||||
renderItem={({ item, index }) => (
|
|
||||||
<SearchResultItem
|
|
||||||
item={item}
|
|
||||||
index={index}
|
|
||||||
navigation={navigation}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
setMenuVisible={setMenuVisible}
|
|
||||||
currentTheme={currentTheme}
|
|
||||||
refreshFlag={refreshFlag}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
keyExtractor={item => `${addonGroup.addonId}-movie-${item.id}`}
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.horizontalListContent}
|
|
||||||
extraData={refreshFlag}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* TV Shows from this addon */}
|
|
||||||
{seriesResults.length > 0 && (
|
|
||||||
<Animated.View
|
|
||||||
style={styles.carouselContainer}
|
|
||||||
entering={FadeIn.duration(300)}
|
|
||||||
>
|
|
||||||
<Text style={[styles.carouselSubtitle, { color: currentTheme.colors.lightGray }]}>
|
|
||||||
TV Shows ({seriesResults.length})
|
|
||||||
</Text>
|
|
||||||
<FlatList
|
|
||||||
data={seriesResults}
|
|
||||||
renderItem={({ item, index }) => (
|
|
||||||
<SearchResultItem
|
|
||||||
item={item}
|
|
||||||
index={index}
|
|
||||||
navigation={navigation}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
setMenuVisible={setMenuVisible}
|
|
||||||
currentTheme={currentTheme}
|
|
||||||
refreshFlag={refreshFlag}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
keyExtractor={item => `${addonGroup.addonId}-series-${item.id}`}
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.horizontalListContent}
|
|
||||||
extraData={refreshFlag}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Other content types (anime, etc.) */}
|
|
||||||
{otherResults.length > 0 && (
|
|
||||||
<Animated.View
|
|
||||||
style={styles.carouselContainer}
|
|
||||||
entering={FadeIn.duration(300)}
|
|
||||||
>
|
|
||||||
<Text style={[styles.carouselSubtitle, { color: currentTheme.colors.lightGray }]}>
|
|
||||||
{otherResults[0].type.charAt(0).toUpperCase() + otherResults[0].type.slice(1)} ({otherResults.length})
|
|
||||||
</Text>
|
|
||||||
<FlatList
|
|
||||||
data={otherResults}
|
|
||||||
renderItem={({ item, index }) => (
|
|
||||||
<SearchResultItem
|
|
||||||
item={item}
|
|
||||||
index={index}
|
|
||||||
navigation={navigation}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
setMenuVisible={setMenuVisible}
|
|
||||||
currentTheme={currentTheme}
|
|
||||||
refreshFlag={refreshFlag}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
keyExtractor={item => `${addonGroup.addonId}-${item.type}-${item.id}`}
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.horizontalListContent}
|
|
||||||
extraData={refreshFlag}
|
|
||||||
/>
|
|
||||||
</Animated.View>
|
|
||||||
)}
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1015,7 +1037,7 @@ const styles = StyleSheet.create({
|
||||||
borderBottomColor: 'rgba(255,255,255,0.1)',
|
borderBottomColor: 'rgba(255,255,255,0.1)',
|
||||||
},
|
},
|
||||||
addonHeaderIcon: {
|
addonHeaderIcon: {
|
||||||
marginRight: 8,
|
// removed icon
|
||||||
},
|
},
|
||||||
addonHeaderText: {
|
addonHeaderText: {
|
||||||
fontSize: isTablet ? 18 : 16,
|
fontSize: isTablet ? 18 : 16,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue