mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-13 13:10:56 +00:00
refactor search screen
This commit is contained in:
parent
b10aab6057
commit
9924d26ff6
8 changed files with 1530 additions and 1548 deletions
155
src/components/search/AddonSection.tsx
Normal file
155
src/components/search/AddonSection.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { View, Text, FlatList } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AddonSearchResults, StreamingContent } from '../../services/catalogService';
|
||||
import { SearchResultItem } from './SearchResultItem';
|
||||
import { isTablet, isLargeTablet, isTV } from './searchUtils';
|
||||
import { searchStyles as styles } from './searchStyles';
|
||||
|
||||
interface AddonSectionProps {
|
||||
addonGroup: AddonSearchResults;
|
||||
addonIndex: number;
|
||||
onItemPress: (item: StreamingContent) => void;
|
||||
onItemLongPress: (item: StreamingContent) => void;
|
||||
currentTheme: any;
|
||||
}
|
||||
|
||||
export const AddonSection = React.memo(({
|
||||
addonGroup,
|
||||
addonIndex,
|
||||
onItemPress,
|
||||
onItemLongPress,
|
||||
currentTheme,
|
||||
}: AddonSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
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 (
|
||||
<View>
|
||||
{/* 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 && (
|
||||
<View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]}>
|
||||
<Text style={[
|
||||
styles.carouselSubtitle,
|
||||
{
|
||||
color: currentTheme.colors.lightGray,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
{t('search.movies')} ({movieResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={movieResults}
|
||||
renderItem={({ item, index }) => (
|
||||
<SearchResultItem
|
||||
item={item}
|
||||
index={index}
|
||||
onPress={onItemPress}
|
||||
onLongPress={onItemLongPress}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => `${addonGroup.addonId}-movie-${item.id}`}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalListContent}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* TV Shows */}
|
||||
{seriesResults.length > 0 && (
|
||||
<View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]}>
|
||||
<Text style={[
|
||||
styles.carouselSubtitle,
|
||||
{
|
||||
color: currentTheme.colors.lightGray,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
{t('search.tv_shows')} ({seriesResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={seriesResults}
|
||||
renderItem={({ item, index }) => (
|
||||
<SearchResultItem
|
||||
item={item}
|
||||
index={index}
|
||||
onPress={onItemPress}
|
||||
onLongPress={onItemLongPress}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => `${addonGroup.addonId}-series-${item.id}`}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalListContent}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Other types */}
|
||||
{otherResults.length > 0 && (
|
||||
<View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]}>
|
||||
<Text style={[
|
||||
styles.carouselSubtitle,
|
||||
{
|
||||
color: currentTheme.colors.lightGray,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
{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}
|
||||
onPress={onItemPress}
|
||||
onLongPress={onItemLongPress}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => `${addonGroup.addonId}-${item.type}-${item.id}`}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalListContent}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}, (prev, next) => {
|
||||
// Only re-render if this section's reference changed
|
||||
return prev.addonGroup === next.addonGroup && prev.addonIndex === next.addonIndex;
|
||||
});
|
||||
|
||||
AddonSection.displayName = 'AddonSection';
|
||||
266
src/components/search/DiscoverBottomSheets.tsx
Normal file
266
src/components/search/DiscoverBottomSheets.tsx
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
import React, { useMemo, useCallback, forwardRef, RefObject } from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
|
||||
import { DiscoverCatalog } from './searchUtils';
|
||||
import { searchStyles as styles } from './searchStyles';
|
||||
|
||||
interface DiscoverBottomSheetsProps {
|
||||
typeSheetRef: RefObject<BottomSheetModal>;
|
||||
catalogSheetRef: RefObject<BottomSheetModal>;
|
||||
genreSheetRef: RefObject<BottomSheetModal>;
|
||||
selectedDiscoverType: 'movie' | 'series';
|
||||
selectedCatalog: DiscoverCatalog | null;
|
||||
selectedDiscoverGenre: string | null;
|
||||
filteredCatalogs: DiscoverCatalog[];
|
||||
availableGenres: string[];
|
||||
onTypeSelect: (type: 'movie' | 'series') => void;
|
||||
onCatalogSelect: (catalog: DiscoverCatalog) => void;
|
||||
onGenreSelect: (genre: string | null) => void;
|
||||
currentTheme: any;
|
||||
}
|
||||
|
||||
export const DiscoverBottomSheets = ({
|
||||
typeSheetRef,
|
||||
catalogSheetRef,
|
||||
genreSheetRef,
|
||||
selectedDiscoverType,
|
||||
selectedCatalog,
|
||||
selectedDiscoverGenre,
|
||||
filteredCatalogs,
|
||||
availableGenres,
|
||||
onTypeSelect,
|
||||
onCatalogSelect,
|
||||
onGenreSelect,
|
||||
currentTheme,
|
||||
}: DiscoverBottomSheetsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const typeSnapPoints = useMemo(() => ['25%'], []);
|
||||
const catalogSnapPoints = useMemo(() => ['50%'], []);
|
||||
const genreSnapPoints = useMemo(() => ['50%'], []);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: any) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
opacity={0.5}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Catalog Selection Bottom Sheet */}
|
||||
<BottomSheetModal
|
||||
ref={catalogSheetRef}
|
||||
index={0}
|
||||
snapPoints={catalogSnapPoints}
|
||||
enableDynamicSizing={false}
|
||||
enablePanDownToClose={true}
|
||||
backdropComponent={renderBackdrop}
|
||||
backgroundStyle={{
|
||||
backgroundColor: currentTheme.colors.darkGray || '#0A0C0C',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
}}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: currentTheme.colors.mediumGray,
|
||||
}}
|
||||
>
|
||||
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
|
||||
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
|
||||
{t('search.select_catalog')}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => catalogSheetRef.current?.dismiss()}>
|
||||
<MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<BottomSheetScrollView
|
||||
style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }}
|
||||
contentContainerStyle={styles.bottomSheetContent}
|
||||
>
|
||||
{filteredCatalogs.map((catalog, index) => (
|
||||
<TouchableOpacity
|
||||
key={`${catalog.addonId}-${catalog.catalogId}-${index}`}
|
||||
style={[
|
||||
styles.bottomSheetItem,
|
||||
selectedCatalog?.catalogId === catalog.catalogId &&
|
||||
selectedCatalog?.addonId === catalog.addonId &&
|
||||
styles.bottomSheetItemSelected
|
||||
]}
|
||||
onPress={() => onCatalogSelect(catalog)}
|
||||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
{catalog.catalogName}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
{catalog.addonName}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedCatalog?.catalogId === catalog.catalogId &&
|
||||
selectedCatalog?.addonId === catalog.addonId && (
|
||||
<MaterialIcons name="check" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
|
||||
{/* Genre Selection Bottom Sheet */}
|
||||
<BottomSheetModal
|
||||
ref={genreSheetRef}
|
||||
index={0}
|
||||
snapPoints={genreSnapPoints}
|
||||
enableDynamicSizing={false}
|
||||
enablePanDownToClose={true}
|
||||
backdropComponent={renderBackdrop}
|
||||
android_keyboardInputMode="adjustResize"
|
||||
animateOnMount={true}
|
||||
backgroundStyle={{
|
||||
backgroundColor: currentTheme.colors.darkGray || '#0A0C0C',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
}}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: currentTheme.colors.mediumGray,
|
||||
}}
|
||||
>
|
||||
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
|
||||
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
|
||||
{t('search.select_genre')}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => genreSheetRef.current?.dismiss()}>
|
||||
<MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<BottomSheetScrollView
|
||||
style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }}
|
||||
contentContainerStyle={styles.bottomSheetContent}
|
||||
>
|
||||
{/* All Genres option */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.bottomSheetItem,
|
||||
!selectedDiscoverGenre && styles.bottomSheetItemSelected
|
||||
]}
|
||||
onPress={() => onGenreSelect(null)}
|
||||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
{t('search.all_genres')}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
{t('search.show_all_content')}
|
||||
</Text>
|
||||
</View>
|
||||
{!selectedDiscoverGenre && (
|
||||
<MaterialIcons name="check" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Genre options */}
|
||||
{availableGenres.map((genre, index) => (
|
||||
<TouchableOpacity
|
||||
key={`${genre}-${index}`}
|
||||
style={[
|
||||
styles.bottomSheetItem,
|
||||
selectedDiscoverGenre === genre && styles.bottomSheetItemSelected
|
||||
]}
|
||||
onPress={() => onGenreSelect(genre)}
|
||||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
{genre}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedDiscoverGenre === genre && (
|
||||
<MaterialIcons name="check" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
|
||||
{/* Type Selection Bottom Sheet */}
|
||||
<BottomSheetModal
|
||||
ref={typeSheetRef}
|
||||
index={0}
|
||||
snapPoints={typeSnapPoints}
|
||||
enableDynamicSizing={false}
|
||||
enablePanDownToClose={true}
|
||||
backdropComponent={renderBackdrop}
|
||||
backgroundStyle={{
|
||||
backgroundColor: currentTheme.colors.darkGray || '#0A0C0C',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
}}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: currentTheme.colors.mediumGray,
|
||||
}}
|
||||
>
|
||||
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
|
||||
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
|
||||
{t('search.select_type')}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => typeSheetRef.current?.dismiss()}>
|
||||
<MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<BottomSheetScrollView
|
||||
style={{ backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }}
|
||||
contentContainerStyle={styles.bottomSheetContent}
|
||||
>
|
||||
{/* Movies option */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.bottomSheetItem,
|
||||
selectedDiscoverType === 'movie' && styles.bottomSheetItemSelected
|
||||
]}
|
||||
onPress={() => onTypeSelect('movie')}
|
||||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
{t('search.movies')}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
{t('search.browse_movies')}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedDiscoverType === 'movie' && (
|
||||
<MaterialIcons name="check" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* TV Shows option */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.bottomSheetItem,
|
||||
selectedDiscoverType === 'series' && styles.bottomSheetItemSelected
|
||||
]}
|
||||
onPress={() => onTypeSelect('series')}
|
||||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
{t('search.tv_shows')}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
{t('search.browse_tv')}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedDiscoverType === 'series' && (
|
||||
<MaterialIcons name="check" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DiscoverBottomSheets.displayName = 'DiscoverBottomSheets';
|
||||
159
src/components/search/DiscoverResultItem.tsx
Normal file
159
src/components/search/DiscoverResultItem.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import React, { useMemo, useEffect, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, Dimensions, DeviceEventEmitter } from 'react-native';
|
||||
import { MaterialIcons, Feather } from '@expo/vector-icons';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { StreamingContent, catalogService } from '../../services/catalogService';
|
||||
import { mmkvStorage } from '../../services/mmkvStorage';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import {
|
||||
isTablet,
|
||||
isLargeTablet,
|
||||
isTV,
|
||||
HORIZONTAL_ITEM_WIDTH,
|
||||
HORIZONTAL_POSTER_HEIGHT,
|
||||
PLACEHOLDER_POSTER,
|
||||
} from './searchUtils';
|
||||
import { searchStyles as styles } from './searchStyles';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
interface DiscoverResultItemProps {
|
||||
item: StreamingContent;
|
||||
index: number;
|
||||
navigation: any;
|
||||
setSelectedItem: (item: StreamingContent) => void;
|
||||
setMenuVisible: (visible: boolean) => void;
|
||||
currentTheme: any;
|
||||
isGrid?: boolean;
|
||||
}
|
||||
|
||||
export const DiscoverResultItem = React.memo(({
|
||||
item,
|
||||
index,
|
||||
navigation,
|
||||
setSelectedItem,
|
||||
setMenuVisible,
|
||||
currentTheme,
|
||||
isGrid = false
|
||||
}: DiscoverResultItemProps) => {
|
||||
const { settings } = useSettings();
|
||||
const [inLibrary, setInLibrary] = useState(!!item.inLibrary);
|
||||
const [watched, setWatched] = useState(false);
|
||||
|
||||
// Calculate dimensions based on poster shape
|
||||
const { itemWidth, aspectRatio } = useMemo(() => {
|
||||
const shape = item.posterShape || 'poster';
|
||||
const baseHeight = HORIZONTAL_POSTER_HEIGHT;
|
||||
|
||||
let w = HORIZONTAL_ITEM_WIDTH;
|
||||
let r = 2 / 3;
|
||||
|
||||
if (isGrid) {
|
||||
// Grid Calculation: (Window Width - Padding) / Columns
|
||||
const columns = isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3;
|
||||
const totalPadding = 32;
|
||||
const totalGap = 12 * (Math.max(3, columns) - 1);
|
||||
const availableWidth = width - totalPadding - totalGap;
|
||||
w = availableWidth / Math.max(3, columns);
|
||||
} else {
|
||||
if (shape === 'landscape') {
|
||||
r = 16 / 9;
|
||||
w = baseHeight * r;
|
||||
} else if (shape === 'square') {
|
||||
r = 1;
|
||||
w = baseHeight;
|
||||
}
|
||||
}
|
||||
return { itemWidth: w, aspectRatio: r };
|
||||
}, [item.posterShape, isGrid]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateWatched = () => {
|
||||
mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setWatched(val === 'true'));
|
||||
};
|
||||
updateWatched();
|
||||
const sub = DeviceEventEmitter.addListener('watchedStatusChanged', updateWatched);
|
||||
return () => sub.remove();
|
||||
}, [item.id, item.type]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => {
|
||||
const found = items.find((libItem) => libItem.id === item.id && libItem.type === item.type);
|
||||
setInLibrary(!!found);
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, [item.id, item.type]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.horizontalItem,
|
||||
{ width: itemWidth },
|
||||
isGrid && styles.discoverGridItem
|
||||
]}
|
||||
onPress={() => {
|
||||
navigation.navigate('Metadata', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
addonId: item.addonId
|
||||
});
|
||||
}}
|
||||
onLongPress={() => {
|
||||
setSelectedItem(item);
|
||||
setMenuVisible(true);
|
||||
}}
|
||||
delayLongPress={300}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.horizontalItemPosterContainer, {
|
||||
width: itemWidth,
|
||||
height: undefined,
|
||||
aspectRatio: aspectRatio,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
borderRadius: settings.posterBorderRadius ?? 12,
|
||||
}]}>
|
||||
<FastImage
|
||||
source={{
|
||||
uri: item.poster || PLACEHOLDER_POSTER,
|
||||
priority: FastImage.priority.low,
|
||||
cache: FastImage.cacheControl.immutable,
|
||||
}}
|
||||
style={[styles.horizontalItemPoster, { borderRadius: settings.posterBorderRadius ?? 12 }]}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
{/* Bookmark icon */}
|
||||
{inLibrary && (
|
||||
<View style={[styles.libraryBadge, { position: 'absolute', top: 8, right: 36, backgroundColor: 'transparent', zIndex: 2 }]}>
|
||||
<Feather name="bookmark" size={16} color={currentTheme.colors.white} />
|
||||
</View>
|
||||
)}
|
||||
{/* Watched icon */}
|
||||
{watched && (
|
||||
<View style={[styles.watchedIndicator, { position: 'absolute', top: 8, right: 8, backgroundColor: 'transparent', zIndex: 2 }]}>
|
||||
<MaterialIcons name="check-circle" size={20} color={currentTheme.colors.success || '#4CAF50'} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.horizontalItemTitle,
|
||||
{
|
||||
color: currentTheme.colors.white,
|
||||
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 14,
|
||||
lineHeight: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 18,
|
||||
}
|
||||
]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.year && (
|
||||
<Text style={[styles.yearText, { color: currentTheme.colors.mediumGray, fontSize: isTV ? 12 : isLargeTablet ? 11 : isTablet ? 10 : 12 }]}>
|
||||
{item.year}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
DiscoverResultItem.displayName = 'DiscoverResultItem';
|
||||
198
src/components/search/DiscoverSection.tsx
Normal file
198
src/components/search/DiscoverSection.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { StreamingContent } from '../../services/catalogService';
|
||||
import { DiscoverCatalog, isTablet, isLargeTablet, isTV } from './searchUtils';
|
||||
import { DiscoverResultItem } from './DiscoverResultItem';
|
||||
import { searchStyles as styles } from './searchStyles';
|
||||
import { BottomSheetModal } from '@gorhom/bottom-sheet';
|
||||
|
||||
interface DiscoverSectionProps {
|
||||
discoverLoading: boolean;
|
||||
discoverInitialized: boolean;
|
||||
discoverResults: StreamingContent[];
|
||||
pendingDiscoverResults: StreamingContent[];
|
||||
loadingMore: boolean;
|
||||
selectedCatalog: DiscoverCatalog | null;
|
||||
selectedDiscoverType: 'movie' | 'series';
|
||||
selectedDiscoverGenre: string | null;
|
||||
availableGenres: string[];
|
||||
typeSheetRef: React.RefObject<BottomSheetModal>;
|
||||
catalogSheetRef: React.RefObject<BottomSheetModal>;
|
||||
genreSheetRef: React.RefObject<BottomSheetModal>;
|
||||
handleShowMore: () => void;
|
||||
navigation: any;
|
||||
setSelectedItem: (item: StreamingContent) => void;
|
||||
setMenuVisible: (visible: boolean) => void;
|
||||
currentTheme: any;
|
||||
}
|
||||
|
||||
export const DiscoverSection = ({
|
||||
discoverLoading,
|
||||
discoverInitialized,
|
||||
discoverResults,
|
||||
pendingDiscoverResults,
|
||||
loadingMore,
|
||||
selectedCatalog,
|
||||
selectedDiscoverType,
|
||||
selectedDiscoverGenre,
|
||||
availableGenres,
|
||||
typeSheetRef,
|
||||
catalogSheetRef,
|
||||
genreSheetRef,
|
||||
handleShowMore,
|
||||
navigation,
|
||||
setSelectedItem,
|
||||
setMenuVisible,
|
||||
currentTheme,
|
||||
}: DiscoverSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style={styles.discoverContainer}>
|
||||
{/* Section Header */}
|
||||
<View style={styles.discoverHeader}>
|
||||
<Text style={[styles.discoverTitle, { color: currentTheme.colors.white }]}>
|
||||
{t('search.discover')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Filter Chips Row */}
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.discoverChipsScroll}
|
||||
contentContainerStyle={styles.discoverChipsContent}
|
||||
>
|
||||
{/* Type Selector Chip (Movie/TV Show) */}
|
||||
<TouchableOpacity
|
||||
style={[styles.discoverSelectorChip, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||
onPress={() => typeSheetRef.current?.present()}
|
||||
>
|
||||
<Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}>
|
||||
{selectedDiscoverType === 'movie' ? t('search.movies') : t('search.tv_shows')}
|
||||
</Text>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Catalog Selector Chip */}
|
||||
<TouchableOpacity
|
||||
style={[styles.discoverSelectorChip, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||
onPress={() => catalogSheetRef.current?.present()}
|
||||
>
|
||||
<Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}>
|
||||
{selectedCatalog ? selectedCatalog.catalogName : t('search.select_catalog')}
|
||||
</Text>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Genre Selector Chip - only show if catalog has genres */}
|
||||
{availableGenres.length > 0 && (
|
||||
<TouchableOpacity
|
||||
style={[styles.discoverSelectorChip, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||
onPress={() => genreSheetRef.current?.present()}
|
||||
>
|
||||
<Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}>
|
||||
{selectedDiscoverGenre || t('search.all_genres')}
|
||||
</Text>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{/* Selected filters summary */}
|
||||
{selectedCatalog && (
|
||||
<View style={styles.discoverFilterSummary}>
|
||||
<Text style={[styles.discoverFilterSummaryText, { color: currentTheme.colors.lightGray }]}>
|
||||
{selectedCatalog.addonName} • {selectedCatalog.type === 'movie' ? t('search.movies') : t('search.tv_shows')}
|
||||
{selectedDiscoverGenre ? ` • ${selectedDiscoverGenre}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Discover Results */}
|
||||
{discoverLoading ? (
|
||||
<View style={styles.discoverLoadingContainer}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.discoverLoadingText, { color: currentTheme.colors.lightGray }]}>
|
||||
{t('search.discovering')}
|
||||
</Text>
|
||||
</View>
|
||||
) : discoverResults.length > 0 ? (
|
||||
<FlatList
|
||||
data={discoverResults}
|
||||
keyExtractor={(item, index) => `discover-${item.id}-${index}`}
|
||||
numColumns={isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3}
|
||||
key={isTV ? 'tv-6' : isLargeTablet ? 'ltab-5' : isTablet ? 'tab-4' : 'phone-3'}
|
||||
columnWrapperStyle={styles.discoverGridRow}
|
||||
contentContainerStyle={styles.discoverGridContent}
|
||||
renderItem={({ item, index }) => (
|
||||
<DiscoverResultItem
|
||||
key={`discover-${item.id}-${index}`}
|
||||
item={item}
|
||||
index={index}
|
||||
navigation={navigation}
|
||||
setSelectedItem={setSelectedItem}
|
||||
setMenuVisible={setMenuVisible}
|
||||
currentTheme={currentTheme}
|
||||
isGrid={true}
|
||||
/>
|
||||
)}
|
||||
initialNumToRender={9}
|
||||
maxToRenderPerBatch={6}
|
||||
windowSize={5}
|
||||
removeClippedSubviews={true}
|
||||
scrollEnabled={false}
|
||||
ListFooterComponent={
|
||||
pendingDiscoverResults.length > 0 ? (
|
||||
<TouchableOpacity
|
||||
style={styles.showMoreButton}
|
||||
onPress={handleShowMore}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.showMoreButtonText, { color: currentTheme.colors.white }]}>
|
||||
{t('search.show_more', { count: pendingDiscoverResults.length })}
|
||||
</Text>
|
||||
<MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} />
|
||||
</TouchableOpacity>
|
||||
) : loadingMore ? (
|
||||
<View style={styles.loadingMoreContainer}>
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
) : discoverInitialized && !discoverLoading && selectedCatalog ? (
|
||||
<View style={styles.discoverEmptyContainer}>
|
||||
<MaterialIcons name="movie-filter" size={48} color={currentTheme.colors.lightGray} />
|
||||
<Text style={[styles.discoverEmptyText, { color: currentTheme.colors.lightGray }]}>
|
||||
{t('search.no_content_found')}
|
||||
</Text>
|
||||
<Text style={[styles.discoverEmptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
{t('search.try_different')}
|
||||
</Text>
|
||||
</View>
|
||||
) : !selectedCatalog && discoverInitialized ? (
|
||||
<View style={styles.discoverEmptyContainer}>
|
||||
<MaterialIcons name="touch-app" size={48} color={currentTheme.colors.lightGray} />
|
||||
<Text style={[styles.discoverEmptyText, { color: currentTheme.colors.lightGray }]}>
|
||||
{t('search.select_catalog_desc')}
|
||||
</Text>
|
||||
<Text style={[styles.discoverEmptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
{t('search.tap_catalog_desc')}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
DiscoverSection.displayName = 'DiscoverSection';
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
// Search components barrel export
|
||||
export * from './searchUtils';
|
||||
export { searchStyles } from './searchStyles';
|
||||
export { SearchSkeletonLoader } from './SearchSkeletonLoader';
|
||||
export { SearchAnimation } from './SearchAnimation';
|
||||
export { SearchResultItem } from './SearchResultItem';
|
||||
export { RecentSearches } from './RecentSearches';
|
||||
export { DiscoverResultItem } from './DiscoverResultItem';
|
||||
export { AddonSection } from './AddonSection';
|
||||
export { DiscoverSection } from './DiscoverSection';
|
||||
export { DiscoverBottomSheets } from './DiscoverBottomSheets';
|
||||
|
|
|
|||
531
src/components/search/searchStyles.ts
Normal file
531
src/components/search/searchStyles.ts
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
import { StyleSheet, Platform, Dimensions } from 'react-native';
|
||||
import { isTablet, isTV, isLargeTablet, HORIZONTAL_ITEM_WIDTH, HORIZONTAL_POSTER_HEIGHT, POSTER_WIDTH, POSTER_HEIGHT } from './searchUtils';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
export const searchStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
paddingTop: 0,
|
||||
},
|
||||
searchBarContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
height: 48,
|
||||
},
|
||||
searchBarWrapper: {
|
||||
flex: 1,
|
||||
height: 48,
|
||||
},
|
||||
searchBar: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
height: '100%',
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: 12,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
height: '100%',
|
||||
},
|
||||
clearButton: {
|
||||
padding: 4,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollViewContent: {
|
||||
paddingBottom: isTablet ? 120 : 100,
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
carouselContainer: {
|
||||
marginBottom: isTablet ? 32 : 24,
|
||||
},
|
||||
carouselTitle: {
|
||||
fontSize: isTablet ? 20 : 18,
|
||||
fontWeight: '700',
|
||||
marginBottom: isTablet ? 16 : 12,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
carouselSubtitle: {
|
||||
fontSize: isTablet ? 16 : 14,
|
||||
fontWeight: '600',
|
||||
marginBottom: isTablet ? 12 : 8,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
addonHeaderContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: isTablet ? 16 : 12,
|
||||
marginTop: isTablet ? 24 : 16,
|
||||
marginBottom: isTablet ? 8 : 4,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(255,255,255,0.1)',
|
||||
},
|
||||
addonHeaderIcon: {
|
||||
// removed icon
|
||||
},
|
||||
addonHeaderText: {
|
||||
fontSize: isTablet ? 18 : 16,
|
||||
fontWeight: '700',
|
||||
flex: 1,
|
||||
},
|
||||
addonHeaderBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
},
|
||||
addonHeaderBadgeText: {
|
||||
fontSize: isTablet ? 12 : 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
horizontalListContent: {
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
horizontalItem: {
|
||||
width: HORIZONTAL_ITEM_WIDTH,
|
||||
marginRight: 16,
|
||||
},
|
||||
horizontalItemPosterContainer: {
|
||||
width: HORIZONTAL_ITEM_WIDTH,
|
||||
height: HORIZONTAL_POSTER_HEIGHT,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 8,
|
||||
borderWidth: 1.5,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
elevation: Platform.OS === 'android' ? 1 : 0,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 1,
|
||||
},
|
||||
horizontalItemPoster: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
horizontalItemTitle: {
|
||||
fontSize: isTablet ? 12 : 14,
|
||||
fontWeight: '600',
|
||||
lineHeight: isTablet ? 16 : 18,
|
||||
textAlign: 'left',
|
||||
},
|
||||
yearText: {
|
||||
fontSize: isTablet ? 10 : 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
recentSearchesContainer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: isTablet ? 24 : 16,
|
||||
paddingTop: isTablet ? 12 : 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(255,255,255,0.05)',
|
||||
marginBottom: isTablet ? 16 : 8,
|
||||
},
|
||||
recentSearchItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: isTablet ? 12 : 10,
|
||||
paddingHorizontal: 16,
|
||||
marginVertical: 1,
|
||||
},
|
||||
recentSearchIcon: {
|
||||
marginRight: 12,
|
||||
},
|
||||
recentSearchText: {
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
},
|
||||
recentSearchDeleteButton: {
|
||||
padding: 4,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 5,
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: isTablet ? 64 : 32,
|
||||
paddingBottom: isTablet ? 120 : 100,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptySubtext: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
skeletonContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
skeletonVerticalItem: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 16,
|
||||
},
|
||||
skeletonPoster: {
|
||||
width: POSTER_WIDTH,
|
||||
height: POSTER_HEIGHT,
|
||||
borderRadius: 12,
|
||||
},
|
||||
skeletonItemDetails: {
|
||||
flex: 1,
|
||||
marginLeft: 16,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
skeletonMetaRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
},
|
||||
skeletonTitle: {
|
||||
height: 20,
|
||||
width: '80%',
|
||||
marginBottom: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
skeletonMeta: {
|
||||
height: 14,
|
||||
width: '30%',
|
||||
borderRadius: 4,
|
||||
},
|
||||
skeletonSectionHeader: {
|
||||
height: 24,
|
||||
width: '40%',
|
||||
marginBottom: 16,
|
||||
borderRadius: 4,
|
||||
},
|
||||
ratingContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 4,
|
||||
},
|
||||
ratingText: {
|
||||
fontSize: isTablet ? 9 : 10,
|
||||
fontWeight: '700',
|
||||
marginLeft: 2,
|
||||
},
|
||||
simpleAnimationContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
simpleAnimationContent: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
spinnerContainer: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
simpleAnimationText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
watchedIndicator: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
borderRadius: 12,
|
||||
padding: 2,
|
||||
zIndex: 2,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
libraryBadge: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
zIndex: 2,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
// Discover section styles
|
||||
discoverContainer: {
|
||||
paddingTop: isTablet ? 16 : 12,
|
||||
paddingBottom: isTablet ? 24 : 16,
|
||||
},
|
||||
discoverHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: isTablet ? 16 : 12,
|
||||
gap: 8,
|
||||
},
|
||||
discoverTitle: {
|
||||
fontSize: isTablet ? 22 : 20,
|
||||
fontWeight: '700',
|
||||
},
|
||||
discoverTypeContainer: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: isTablet ? 16 : 12,
|
||||
gap: 12,
|
||||
},
|
||||
discoverTypeButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
gap: 6,
|
||||
},
|
||||
discoverTypeText: {
|
||||
fontSize: isTablet ? 15 : 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
discoverGenreScroll: {
|
||||
marginBottom: isTablet ? 20 : 16,
|
||||
},
|
||||
discoverGenreContent: {
|
||||
paddingHorizontal: 16,
|
||||
gap: 8,
|
||||
},
|
||||
discoverGenreChip: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
marginRight: 8,
|
||||
},
|
||||
discoverGenreChipActive: {
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
},
|
||||
discoverGenreText: {
|
||||
fontSize: isTablet ? 14 : 13,
|
||||
fontWeight: '500',
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
},
|
||||
discoverGenreTextActive: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '600',
|
||||
},
|
||||
discoverLoadingContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 60,
|
||||
},
|
||||
discoverLoadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 14,
|
||||
},
|
||||
discoverAddonSection: {
|
||||
marginBottom: isTablet ? 28 : 20,
|
||||
},
|
||||
discoverAddonHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: isTablet ? 12 : 8,
|
||||
},
|
||||
discoverAddonName: {
|
||||
fontSize: isTablet ? 16 : 15,
|
||||
fontWeight: '600',
|
||||
flex: 1,
|
||||
},
|
||||
discoverAddonBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 10,
|
||||
},
|
||||
discoverAddonBadgeText: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
discoverEmptyContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 60,
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
discoverEmptyText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginTop: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
discoverEmptySubtext: {
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
textAlign: 'center',
|
||||
},
|
||||
discoverGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
paddingHorizontal: 16,
|
||||
gap: 12,
|
||||
},
|
||||
discoverGridRow: {
|
||||
justifyContent: 'flex-start',
|
||||
gap: 12,
|
||||
},
|
||||
discoverGridContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
discoverGridItem: {
|
||||
marginRight: 0,
|
||||
marginBottom: 12,
|
||||
},
|
||||
loadingMoreContainer: {
|
||||
width: '100%',
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
// New chip-based discover styles
|
||||
discoverChipsScroll: {
|
||||
marginBottom: isTablet ? 12 : 10,
|
||||
flexGrow: 0,
|
||||
},
|
||||
discoverChipsContent: {
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
discoverSelectorChip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
gap: 6,
|
||||
},
|
||||
discoverSelectorText: {
|
||||
fontSize: isTablet ? 14 : 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
discoverFilterSummary: {
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: isTablet ? 16 : 12,
|
||||
},
|
||||
discoverFilterSummaryText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
// Bottom sheet styles
|
||||
bottomSheetHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(255,255,255,0.1)',
|
||||
},
|
||||
bottomSheetTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
bottomSheetContent: {
|
||||
paddingHorizontal: 12,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
bottomSheetItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
marginVertical: 2,
|
||||
},
|
||||
bottomSheetItemSelected: {
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
},
|
||||
bottomSheetItemIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
bottomSheetItemContent: {
|
||||
flex: 1,
|
||||
},
|
||||
bottomSheetItemTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
bottomSheetItemSubtitle: {
|
||||
fontSize: 13,
|
||||
marginTop: 2,
|
||||
},
|
||||
showMoreButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 24,
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
borderRadius: 8,
|
||||
marginVertical: 20,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
showMoreButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginRight: 8,
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -132,6 +132,7 @@ export interface StreamingContent {
|
|||
backdrop_path?: string;
|
||||
};
|
||||
addedToLibraryAt?: number; // Timestamp when added to library
|
||||
addonId?: string; // ID of the addon that provided this content
|
||||
}
|
||||
|
||||
export interface CatalogContent {
|
||||
|
|
@ -1203,29 +1204,29 @@ class CatalogService {
|
|||
* @param catalogId - The catalog ID
|
||||
* @param type - Content type (movie/series)
|
||||
* @param genre - Optional genre filter
|
||||
* @param limit - Maximum items to return
|
||||
* @param page - Page number for pagination (default 1)
|
||||
*/
|
||||
async discoverContentFromCatalog(
|
||||
addonId: string,
|
||||
catalogId: string,
|
||||
type: string,
|
||||
genre?: string,
|
||||
limit: number = 20
|
||||
page: number = 1
|
||||
): Promise<StreamingContent[]> {
|
||||
try {
|
||||
const manifests = await stremioService.getInstalledAddonsAsync();
|
||||
const manifest = manifests.find(m => m.id === addonId);
|
||||
|
||||
|
||||
if (!manifest) {
|
||||
logger.error(`Addon ${addonId} not found`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const filters = genre ? [{ title: 'genre', value: genre }] : [];
|
||||
const metas = await stremioService.getCatalog(manifest, type, catalogId, 1, filters);
|
||||
const metas = await stremioService.getCatalog(manifest, type, catalogId, page, filters);
|
||||
|
||||
if (metas && metas.length > 0) {
|
||||
return metas.slice(0, limit).map(meta => this.convertMetaToStreamingContent(meta));
|
||||
return metas.map(meta => this.convertMetaToStreamingContent(meta));
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue