mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
header refactor
This commit is contained in:
parent
52065a1462
commit
9e7543df02
4 changed files with 592 additions and 477 deletions
240
src/components/common/ScreenHeader.tsx
Normal file
240
src/components/common/ScreenHeader.tsx
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
StatusBar,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { Feather, MaterialIcons } from '@expo/vector-icons';
|
||||||
|
|
||||||
|
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
|
|
||||||
|
interface ScreenHeaderProps {
|
||||||
|
/**
|
||||||
|
* The main title displayed in the header
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* Optional right action button (icon name from Feather icons)
|
||||||
|
*/
|
||||||
|
rightActionIcon?: string;
|
||||||
|
/**
|
||||||
|
* Optional callback for right action button press
|
||||||
|
*/
|
||||||
|
onRightActionPress?: () => void;
|
||||||
|
/**
|
||||||
|
* Optional custom right action component (overrides rightActionIcon)
|
||||||
|
*/
|
||||||
|
rightActionComponent?: React.ReactNode;
|
||||||
|
/**
|
||||||
|
* Optional back button (shows arrow back icon)
|
||||||
|
*/
|
||||||
|
showBackButton?: boolean;
|
||||||
|
/**
|
||||||
|
* Optional callback for back button press
|
||||||
|
*/
|
||||||
|
onBackPress?: () => void;
|
||||||
|
/**
|
||||||
|
* Whether this screen is displayed on a tablet layout
|
||||||
|
*/
|
||||||
|
isTablet?: boolean;
|
||||||
|
/**
|
||||||
|
* Optional extra top padding for tablet navigation offset
|
||||||
|
*/
|
||||||
|
tabletNavOffset?: number;
|
||||||
|
/**
|
||||||
|
* Optional custom title component (overrides title text)
|
||||||
|
*/
|
||||||
|
titleComponent?: React.ReactNode;
|
||||||
|
/**
|
||||||
|
* Optional children to render below the title row (e.g., filters, search bar)
|
||||||
|
*/
|
||||||
|
children?: React.ReactNode;
|
||||||
|
/**
|
||||||
|
* Whether to hide the header title row (useful when showing only children)
|
||||||
|
*/
|
||||||
|
hideTitleRow?: boolean;
|
||||||
|
/**
|
||||||
|
* Use MaterialIcons instead of Feather for icons
|
||||||
|
*/
|
||||||
|
useMaterialIcons?: boolean;
|
||||||
|
/**
|
||||||
|
* Optional custom style for title
|
||||||
|
*/
|
||||||
|
titleStyle?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScreenHeader: React.FC<ScreenHeaderProps> = ({
|
||||||
|
title,
|
||||||
|
rightActionIcon,
|
||||||
|
onRightActionPress,
|
||||||
|
rightActionComponent,
|
||||||
|
showBackButton = false,
|
||||||
|
onBackPress,
|
||||||
|
isTablet = false,
|
||||||
|
tabletNavOffset = 64,
|
||||||
|
titleComponent,
|
||||||
|
children,
|
||||||
|
hideTitleRow = false,
|
||||||
|
useMaterialIcons = false,
|
||||||
|
titleStyle,
|
||||||
|
}) => {
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
// Calculate header spacing
|
||||||
|
const topSpacing =
|
||||||
|
(Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT : insets.top) +
|
||||||
|
(isTablet ? tabletNavOffset : 0);
|
||||||
|
|
||||||
|
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
|
||||||
|
const titleRowHeight = headerBaseHeight + topSpacing;
|
||||||
|
|
||||||
|
const IconComponent = useMaterialIcons ? MaterialIcons : Feather;
|
||||||
|
const backIconName = useMaterialIcons ? 'arrow-back' : 'arrow-left';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Fixed position header background to prevent shifts */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.headerBackground,
|
||||||
|
{
|
||||||
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Header Section */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.header,
|
||||||
|
{
|
||||||
|
paddingTop: topSpacing,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Title Row */}
|
||||||
|
{!hideTitleRow && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.titleRow,
|
||||||
|
{
|
||||||
|
height: headerBaseHeight,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.headerContent}>
|
||||||
|
{showBackButton ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={onBackPress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
name={backIconName as any}
|
||||||
|
size={24}
|
||||||
|
color={currentTheme.colors.text}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{titleComponent ? (
|
||||||
|
titleComponent
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.headerTitle,
|
||||||
|
{ color: currentTheme.colors.text },
|
||||||
|
showBackButton && styles.headerTitleWithBack,
|
||||||
|
titleStyle,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Right Action */}
|
||||||
|
{rightActionComponent ? (
|
||||||
|
<View style={styles.rightActionContainer}>{rightActionComponent}</View>
|
||||||
|
) : rightActionIcon && onRightActionPress ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.rightActionButton}
|
||||||
|
onPress={onRightActionPress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
name={rightActionIcon as any}
|
||||||
|
size={24}
|
||||||
|
color={currentTheme.colors.text}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : (
|
||||||
|
<View style={styles.rightActionPlaceholder} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Children (filters, search bar, etc.) */}
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
headerBackground: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
zIndex: 11,
|
||||||
|
},
|
||||||
|
titleRow: {
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
paddingBottom: 8,
|
||||||
|
},
|
||||||
|
headerContent: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginLeft: -8,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: '800',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
headerTitleWithBack: {
|
||||||
|
fontSize: 24,
|
||||||
|
flex: 0,
|
||||||
|
},
|
||||||
|
rightActionContainer: {
|
||||||
|
minWidth: 40,
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
rightActionButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginRight: -8,
|
||||||
|
},
|
||||||
|
rightActionPlaceholder: {
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ScreenHeader;
|
||||||
|
|
@ -34,6 +34,7 @@ import { VideoPlayerService } from '../services/videoPlayerService';
|
||||||
import type { DownloadItem } from '../contexts/DownloadsContext';
|
import type { DownloadItem } from '../contexts/DownloadsContext';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
import ScreenHeader from '../components/common/ScreenHeader';
|
||||||
|
|
||||||
const { height, width } = Dimensions.get('window');
|
const { height, width } = Dimensions.get('window');
|
||||||
const isTablet = width >= 768;
|
const isTablet = width >= 768;
|
||||||
|
|
@ -346,7 +347,6 @@ const DownloadsScreen: React.FC = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const { top: safeAreaTop } = useSafeAreaInsets();
|
|
||||||
const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads();
|
const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads();
|
||||||
const { showSuccess, showInfo } = useToast();
|
const { showSuccess, showInfo } = useToast();
|
||||||
|
|
||||||
|
|
@ -356,9 +356,6 @@ const DownloadsScreen: React.FC = () => {
|
||||||
const [showRemoveAlert, setShowRemoveAlert] = useState(false);
|
const [showRemoveAlert, setShowRemoveAlert] = useState(false);
|
||||||
const [pendingRemoveItem, setPendingRemoveItem] = useState<DownloadItem | null>(null);
|
const [pendingRemoveItem, setPendingRemoveItem] = useState<DownloadItem | null>(null);
|
||||||
|
|
||||||
// Animation values
|
|
||||||
const headerOpacity = useSharedValue(1);
|
|
||||||
|
|
||||||
// Filter downloads based on selected filter
|
// Filter downloads based on selected filter
|
||||||
const filteredDownloads = useMemo(() => {
|
const filteredDownloads = useMemo(() => {
|
||||||
if (selectedFilter === 'all') return downloads;
|
if (selectedFilter === 'all') return downloads;
|
||||||
|
|
@ -571,11 +568,6 @@ const DownloadsScreen: React.FC = () => {
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|
||||||
// Animated styles
|
|
||||||
const headerStyle = useAnimatedStyle(() => ({
|
|
||||||
opacity: headerOpacity.value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => (
|
const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={filter}
|
key={filter}
|
||||||
|
|
@ -632,22 +624,10 @@ const DownloadsScreen: React.FC = () => {
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Header */}
|
{/* ScreenHeader Component */}
|
||||||
<Animated.View style={[
|
<ScreenHeader
|
||||||
styles.header,
|
title="Downloads"
|
||||||
{
|
rightActionComponent={
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
|
||||||
paddingTop: (Platform.OS === 'android'
|
|
||||||
? (StatusBar.currentHeight || 0) + 26
|
|
||||||
: safeAreaTop + 15) + (isTablet ? 64 : 0),
|
|
||||||
borderBottomColor: currentTheme.colors.border,
|
|
||||||
},
|
|
||||||
headerStyle,
|
|
||||||
]}>
|
|
||||||
<View style={styles.headerTitleRow}>
|
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
|
||||||
Downloads
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.helpButton}
|
style={styles.helpButton}
|
||||||
onPress={showDownloadHelp}
|
onPress={showDownloadHelp}
|
||||||
|
|
@ -659,8 +639,9 @@ const DownloadsScreen: React.FC = () => {
|
||||||
color={currentTheme.colors.mediumEmphasis}
|
color={currentTheme.colors.mediumEmphasis}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
}
|
||||||
|
isTablet={isTablet}
|
||||||
|
>
|
||||||
{downloads.length > 0 && (
|
{downloads.length > 0 && (
|
||||||
<View style={styles.filterContainer}>
|
<View style={styles.filterContainer}>
|
||||||
{renderFilterButton('all', 'All', stats.total)}
|
{renderFilterButton('all', 'All', stats.total)}
|
||||||
|
|
@ -669,7 +650,7 @@ const DownloadsScreen: React.FC = () => {
|
||||||
{renderFilterButton('paused', 'Paused', stats.paused)}
|
{renderFilterButton('paused', 'Paused', stats.paused)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</ScreenHeader>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{downloads.length === 0 ? (
|
{downloads.length === 0 ? (
|
||||||
|
|
@ -742,23 +723,6 @@ const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
header: {
|
|
||||||
paddingHorizontal: isTablet ? 24 : Math.max(1, width * 0.05),
|
|
||||||
paddingBottom: isTablet ? 20 : 16,
|
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
||||||
},
|
|
||||||
headerTitleRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginBottom: isTablet ? 20 : 16,
|
|
||||||
paddingBottom: 8,
|
|
||||||
},
|
|
||||||
headerTitle: {
|
|
||||||
fontSize: isTablet ? 36 : Math.min(32, width * 0.08),
|
|
||||||
fontWeight: '800',
|
|
||||||
letterSpacing: 0.3,
|
|
||||||
},
|
|
||||||
helpButton: {
|
helpButton: {
|
||||||
padding: 8,
|
padding: 8,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Share } from 'react-native';
|
||||||
import { mmkvStorage } from '../services/mmkvStorage';
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
import DropUpMenu from '../components/home/DropUpMenu';
|
import DropUpMenu from '../components/home/DropUpMenu';
|
||||||
|
import ScreenHeader from '../components/common/ScreenHeader';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -107,7 +108,7 @@ const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item:
|
||||||
navigation.navigate('Metadata', { id: item.imdbId, type: item.type });
|
navigation.navigate('Metadata', { id: item.imdbId, type: item.type });
|
||||||
}
|
}
|
||||||
}, [navigation, item.imdbId, item.type]);
|
}, [navigation, item.imdbId, item.type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.itemContainer, { width }]}
|
style={[styles.itemContainer, { width }]}
|
||||||
|
|
@ -168,17 +169,17 @@ const SkeletonLoader = () => {
|
||||||
|
|
||||||
const renderSkeletonItem = () => (
|
const renderSkeletonItem = () => (
|
||||||
<View style={[styles.itemContainer, { width: itemWidth }]}>
|
<View style={[styles.itemContainer, { width: itemWidth }]}>
|
||||||
<RNAnimated.View
|
<RNAnimated.View
|
||||||
style={[
|
style={[
|
||||||
styles.posterContainer,
|
styles.posterContainer,
|
||||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<RNAnimated.View
|
<RNAnimated.View
|
||||||
style={[
|
style={[
|
||||||
styles.skeletonTitle,
|
styles.skeletonTitle,
|
||||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -212,7 +213,7 @@ const LibraryScreen = () => {
|
||||||
const [selectedItem, setSelectedItem] = useState<LibraryItem | null>(null);
|
const [selectedItem, setSelectedItem] = useState<LibraryItem | null>(null);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
// Trakt integration
|
// Trakt integration
|
||||||
const {
|
const {
|
||||||
isAuthenticated: traktAuthenticated,
|
isAuthenticated: traktAuthenticated,
|
||||||
|
|
@ -272,14 +273,14 @@ const LibraryScreen = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const items = await catalogService.getLibraryItems();
|
const items = await catalogService.getLibraryItems();
|
||||||
|
|
||||||
// Sort by date added (most recent first)
|
// Sort by date added (most recent first)
|
||||||
const sortedItems = items.sort((a, b) => {
|
const sortedItems = items.sort((a, b) => {
|
||||||
const timeA = (a as any).addedToLibraryAt || 0;
|
const timeA = (a as any).addedToLibraryAt || 0;
|
||||||
const timeB = (b as any).addedToLibraryAt || 0;
|
const timeB = (b as any).addedToLibraryAt || 0;
|
||||||
return timeB - timeA; // Descending order (newest first)
|
return timeB - timeA; // Descending order (newest first)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load watched status for each item from AsyncStorage
|
// Load watched status for each item from AsyncStorage
|
||||||
const updatedItems = await Promise.all(sortedItems.map(async (item) => {
|
const updatedItems = await Promise.all(sortedItems.map(async (item) => {
|
||||||
// Map StreamingContent to LibraryItem shape
|
// Map StreamingContent to LibraryItem shape
|
||||||
|
|
@ -313,7 +314,7 @@ const LibraryScreen = () => {
|
||||||
const timeB = (b as any).addedToLibraryAt || 0;
|
const timeB = (b as any).addedToLibraryAt || 0;
|
||||||
return timeB - timeA; // Descending order (newest first)
|
return timeB - timeA; // Descending order (newest first)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync watched status on update
|
// Sync watched status on update
|
||||||
const updatedItems = await Promise.all(sortedItems.map(async (item) => {
|
const updatedItems = await Promise.all(sortedItems.map(async (item) => {
|
||||||
// Map StreamingContent to LibraryItem shape
|
// Map StreamingContent to LibraryItem shape
|
||||||
|
|
@ -403,8 +404,8 @@ const LibraryScreen = () => {
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||||
style={styles.poster}
|
style={styles.poster}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
|
@ -425,7 +426,7 @@ const LibraryScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
<Text style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -444,11 +445,11 @@ const LibraryScreen = () => {
|
||||||
>
|
>
|
||||||
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
|
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
<View style={styles.folderGradient}>
|
<View style={styles.folderGradient}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={folder.icon}
|
name={folder.icon}
|
||||||
size={48}
|
size={48}
|
||||||
color={currentTheme.colors.white}
|
color={currentTheme.colors.white}
|
||||||
style={{ marginBottom: 8 }}
|
style={{ marginBottom: 8 }}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.folderTitle, { color: currentTheme.colors.white }]}>
|
<Text style={[styles.folderTitle, { color: currentTheme.colors.white }]}>
|
||||||
{folder.name}
|
{folder.name}
|
||||||
|
|
@ -724,8 +725,8 @@ const LibraryScreen = () => {
|
||||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||||
Your Trakt collections will appear here once you start using Trakt
|
Your Trakt collections will appear here once you start using Trakt
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.exploreButton, {
|
style={[styles.exploreButton, {
|
||||||
backgroundColor: currentTheme.colors.primary,
|
backgroundColor: currentTheme.colors.primary,
|
||||||
shadowColor: currentTheme.colors.black
|
shadowColor: currentTheme.colors.black
|
||||||
}]}
|
}]}
|
||||||
|
|
@ -742,22 +743,22 @@ const LibraryScreen = () => {
|
||||||
|
|
||||||
// Show collection folders
|
// Show collection folders
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
data={traktFolders}
|
data={traktFolders}
|
||||||
renderItem={({ item }) => renderTraktCollectionFolder({ folder: item })}
|
renderItem={({ item }) => renderTraktCollectionFolder({ folder: item })}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
numColumns={numColumns}
|
numColumns={numColumns}
|
||||||
contentContainerStyle={styles.listContainer}
|
contentContainerStyle={styles.listContainer}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onEndReachedThreshold={0.7}
|
onEndReachedThreshold={0.7}
|
||||||
onEndReached={() => {}}
|
onEndReached={() => { }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show content for specific folder
|
// Show content for specific folder
|
||||||
const folderItems = getTraktFolderItems(selectedTraktFolder);
|
const folderItems = getTraktFolderItems(selectedTraktFolder);
|
||||||
|
|
||||||
if (folderItems.length === 0) {
|
if (folderItems.length === 0) {
|
||||||
const folderName = traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection';
|
const folderName = traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection';
|
||||||
return (
|
return (
|
||||||
|
|
@ -767,8 +768,8 @@ const LibraryScreen = () => {
|
||||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||||
This collection is empty
|
This collection is empty
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.exploreButton, {
|
style={[styles.exploreButton, {
|
||||||
backgroundColor: currentTheme.colors.primary,
|
backgroundColor: currentTheme.colors.primary,
|
||||||
shadowColor: currentTheme.colors.black
|
shadowColor: currentTheme.colors.black
|
||||||
}]}
|
}]}
|
||||||
|
|
@ -793,14 +794,14 @@ const LibraryScreen = () => {
|
||||||
contentContainerStyle={{ paddingBottom: insets.bottom + 80 }}
|
contentContainerStyle={{ paddingBottom: insets.bottom + 80 }}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onEndReachedThreshold={0.7}
|
onEndReachedThreshold={0.7}
|
||||||
onEndReached={() => {}}
|
onEndReached={() => { }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFilter = (filterType: 'trakt' | 'movies' | 'series', label: string, iconName: keyof typeof MaterialIcons.glyphMap) => {
|
const renderFilter = (filterType: 'trakt' | 'movies' | 'series', label: string, iconName: keyof typeof MaterialIcons.glyphMap) => {
|
||||||
const isActive = filter === filterType;
|
const isActive = filter === filterType;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -858,9 +859,9 @@ const LibraryScreen = () => {
|
||||||
const emptySubtitle = 'Add some content to your library to see it here';
|
const emptySubtitle = 'Add some content to your library to see it here';
|
||||||
return (
|
return (
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="video-library"
|
name="video-library"
|
||||||
size={64}
|
size={64}
|
||||||
color={currentTheme.colors.lightGray}
|
color={currentTheme.colors.lightGray}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||||
|
|
@ -869,8 +870,8 @@ const LibraryScreen = () => {
|
||||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||||
{emptySubtitle}
|
{emptySubtitle}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.exploreButton, {
|
style={[styles.exploreButton, {
|
||||||
backgroundColor: currentTheme.colors.primary,
|
backgroundColor: currentTheme.colors.primary,
|
||||||
shadowColor: currentTheme.colors.black
|
shadowColor: currentTheme.colors.black
|
||||||
}]}
|
}]}
|
||||||
|
|
@ -892,92 +893,53 @@ const LibraryScreen = () => {
|
||||||
contentContainerStyle={styles.listContainer}
|
contentContainerStyle={styles.listContainer}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onEndReachedThreshold={0.7}
|
onEndReachedThreshold={0.7}
|
||||||
onEndReached={() => {}}
|
onEndReached={() => { }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
|
|
||||||
// Tablet detection aligned with navigation tablet logic
|
// Tablet detection aligned with navigation tablet logic
|
||||||
const isTablet = useMemo(() => {
|
const isTablet = useMemo(() => {
|
||||||
const smallestDimension = Math.min(width, height);
|
const smallestDimension = Math.min(width, height);
|
||||||
return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768);
|
return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768);
|
||||||
}, [width, height]);
|
}, [width, height]);
|
||||||
// Keep header below floating top navigator on tablets
|
|
||||||
const tabletNavOffset = isTablet ? 64 : 0;
|
|
||||||
const topSpacing = (Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top) + tabletNavOffset;
|
|
||||||
const headerHeight = headerBaseHeight + topSpacing;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
{/* Fixed position header background to prevent shifts */}
|
{/* ScreenHeader Component */}
|
||||||
<View style={[styles.headerBackground, { height: headerHeight, backgroundColor: currentTheme.colors.darkBackground }]} />
|
<ScreenHeader
|
||||||
|
title={showTraktContent
|
||||||
<View style={{ flex: 1 }}>
|
? (selectedTraktFolder
|
||||||
{/* Header Section with proper top spacing */}
|
? traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection'
|
||||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
: 'Trakt Collection')
|
||||||
<View style={[styles.headerContent, showTraktContent && { justifyContent: 'flex-start' }]}>
|
: 'Library'
|
||||||
{showTraktContent ? (
|
}
|
||||||
<>
|
showBackButton={showTraktContent}
|
||||||
<TouchableOpacity
|
onBackPress={showTraktContent ? () => {
|
||||||
style={styles.backButton}
|
if (selectedTraktFolder) {
|
||||||
onPress={() => {
|
setSelectedTraktFolder(null);
|
||||||
if (selectedTraktFolder) {
|
} else {
|
||||||
setSelectedTraktFolder(null);
|
setShowTraktContent(false);
|
||||||
} else {
|
}
|
||||||
setShowTraktContent(false);
|
} : undefined}
|
||||||
}
|
useMaterialIcons={showTraktContent}
|
||||||
}}
|
rightActionIcon={!showTraktContent ? 'calendar' : undefined}
|
||||||
activeOpacity={0.7}
|
onRightActionPress={!showTraktContent ? () => navigation.navigate('Calendar') : undefined}
|
||||||
>
|
isTablet={isTablet}
|
||||||
<MaterialIcons
|
/>
|
||||||
name="arrow-back"
|
|
||||||
size={28}
|
|
||||||
color={currentTheme.colors.white}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white, fontSize: 24, marginLeft: 16 }]}>
|
|
||||||
{selectedTraktFolder
|
|
||||||
? traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection'
|
|
||||||
: 'Trakt Collection'
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Library</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.calendarButton}
|
|
||||||
onPress={() => navigation.navigate('Calendar')}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Feather
|
|
||||||
name="calendar"
|
|
||||||
size={24}
|
|
||||||
color={currentTheme.colors.white}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
{!showTraktContent && (
|
{!showTraktContent && (
|
||||||
// Replaced ScrollView with View and used the modified style
|
<View style={styles.filtersContainer}>
|
||||||
<View
|
{renderFilter('trakt', 'Trakt', 'pan-tool')}
|
||||||
style={styles.filtersContainer}
|
{renderFilter('movies', 'Movies', 'movie')}
|
||||||
>
|
{renderFilter('series', 'TV Shows', 'live-tv')}
|
||||||
{renderFilter('trakt', 'Trakt', 'pan-tool')}
|
|
||||||
{renderFilter('movies', 'Movies', 'movie')}
|
|
||||||
{renderFilter('series', 'TV Shows', 'live-tv')}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showTraktContent ? renderTraktContent() : renderContent()}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)}
|
||||||
|
|
||||||
|
{showTraktContent ? renderTraktContent() : renderContent()}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* DropUpMenu integration */}
|
{/* DropUpMenu integration */}
|
||||||
{selectedItem && (
|
{selectedItem && (
|
||||||
|
|
@ -991,45 +953,45 @@ const LibraryScreen = () => {
|
||||||
if (!selectedItem) return;
|
if (!selectedItem) return;
|
||||||
switch (option) {
|
switch (option) {
|
||||||
case 'library': {
|
case 'library': {
|
||||||
try {
|
try {
|
||||||
await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id);
|
await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id);
|
||||||
showInfo('Removed from Library', 'Item removed from your library');
|
showInfo('Removed from Library', 'Item removed from your library');
|
||||||
setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type)));
|
setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type)));
|
||||||
setMenuVisible(false);
|
setMenuVisible(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError('Failed to update Library', 'Unable to remove item from library');
|
showError('Failed to update Library', 'Unable to remove item from library');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'watched': {
|
case 'watched': {
|
||||||
try {
|
try {
|
||||||
// Use AsyncStorage to store watched status by key
|
// Use AsyncStorage to store watched status by key
|
||||||
const key = `watched:${selectedItem.type}:${selectedItem.id}`;
|
const key = `watched:${selectedItem.type}:${selectedItem.id}`;
|
||||||
const newWatched = !selectedItem.watched;
|
const newWatched = !selectedItem.watched;
|
||||||
await mmkvStorage.setItem(key, newWatched ? 'true' : 'false');
|
await mmkvStorage.setItem(key, newWatched ? 'true' : 'false');
|
||||||
showInfo(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', newWatched ? 'Item marked as watched' : 'Item marked as unwatched');
|
showInfo(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', newWatched ? 'Item marked as watched' : 'Item marked as unwatched');
|
||||||
// Instantly update local state
|
// Instantly update local state
|
||||||
setLibraryItems(prev => prev.map(item =>
|
setLibraryItems(prev => prev.map(item =>
|
||||||
item.id === selectedItem.id && item.type === selectedItem.type
|
item.id === selectedItem.id && item.type === selectedItem.type
|
||||||
? { ...item, watched: newWatched }
|
? { ...item, watched: newWatched }
|
||||||
: item
|
: item
|
||||||
));
|
));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError('Failed to update watched status', 'Unable to update watched status');
|
showError('Failed to update watched status', 'Unable to update watched status');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'share': {
|
case 'share': {
|
||||||
let url = '';
|
let url = '';
|
||||||
if (selectedItem.id) {
|
if (selectedItem.id) {
|
||||||
url = `https://www.imdb.com/title/${selectedItem.id}/`;
|
url = `https://www.imdb.com/title/${selectedItem.id}/`;
|
||||||
}
|
}
|
||||||
const message = `${selectedItem.name}\n${url}`;
|
const message = `${selectedItem.name}\n${url}`;
|
||||||
Share.share({ message, url, title: selectedItem.name });
|
Share.share({ message, url, title: selectedItem.name });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1042,13 +1004,6 @@ const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
headerBackground: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
watchedIndicator: {
|
watchedIndicator: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
|
|
@ -1060,23 +1015,6 @@ const styles = StyleSheet.create({
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
header: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
paddingBottom: 8,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
zIndex: 2,
|
|
||||||
},
|
|
||||||
headerContent: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
headerTitle: {
|
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: '800',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
|
||||||
filtersContainer: {
|
filtersContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
@ -1130,7 +1068,7 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||||
aspectRatio: 2/3,
|
aspectRatio: 2 / 3,
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
shadowOpacity: 0.2,
|
shadowOpacity: 0.2,
|
||||||
|
|
@ -1253,7 +1191,7 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||||
aspectRatio: 2/3,
|
aspectRatio: 2 / 3,
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
shadowOpacity: 0.2,
|
shadowOpacity: 0.2,
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,11 @@ import debounce from 'lodash/debounce';
|
||||||
import { DropUpMenu } from '../components/home/DropUpMenu';
|
import { DropUpMenu } from '../components/home/DropUpMenu';
|
||||||
import { DeviceEventEmitter, Share } from 'react-native';
|
import { DeviceEventEmitter, Share } from 'react-native';
|
||||||
import { mmkvStorage } from '../services/mmkvStorage';
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
FadeOut,
|
FadeOut,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
withTiming,
|
||||||
interpolate,
|
interpolate,
|
||||||
withSpring,
|
withSpring,
|
||||||
|
|
@ -43,6 +43,7 @@ import { BlurView } from 'expo-blur';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import LoadingSpinner from '../components/common/LoadingSpinner';
|
import LoadingSpinner from '../components/common/LoadingSpinner';
|
||||||
|
import ScreenHeader from '../components/common/ScreenHeader';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -110,21 +111,21 @@ const SkeletonLoader = () => {
|
||||||
const renderSkeletonItem = () => (
|
const renderSkeletonItem = () => (
|
||||||
<View style={styles.skeletonVerticalItem}>
|
<View style={styles.skeletonVerticalItem}>
|
||||||
<RNAnimated.View style={[
|
<RNAnimated.View style={[
|
||||||
styles.skeletonPoster,
|
styles.skeletonPoster,
|
||||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]} />
|
]} />
|
||||||
<View style={styles.skeletonItemDetails}>
|
<View style={styles.skeletonItemDetails}>
|
||||||
<RNAnimated.View style={[
|
<RNAnimated.View style={[
|
||||||
styles.skeletonTitle,
|
styles.skeletonTitle,
|
||||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]} />
|
]} />
|
||||||
<View style={styles.skeletonMetaRow}>
|
<View style={styles.skeletonMetaRow}>
|
||||||
<RNAnimated.View style={[
|
<RNAnimated.View style={[
|
||||||
styles.skeletonMeta,
|
styles.skeletonMeta,
|
||||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]} />
|
]} />
|
||||||
<RNAnimated.View style={[
|
<RNAnimated.View style={[
|
||||||
styles.skeletonMeta,
|
styles.skeletonMeta,
|
||||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]} />
|
]} />
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -138,7 +139,7 @@ const SkeletonLoader = () => {
|
||||||
<View key={index}>
|
<View key={index}>
|
||||||
{index === 0 && (
|
{index === 0 && (
|
||||||
<RNAnimated.View style={[
|
<RNAnimated.View style={[
|
||||||
styles.skeletonSectionHeader,
|
styles.skeletonSectionHeader,
|
||||||
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
{ opacity, backgroundColor: currentTheme.colors.darkBackground }
|
||||||
]} />
|
]} />
|
||||||
)}
|
)}
|
||||||
|
|
@ -157,7 +158,7 @@ const SimpleSearchAnimation = () => {
|
||||||
const spinAnim = React.useRef(new RNAnimated.Value(0)).current;
|
const spinAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||||
const fadeAnim = React.useRef(new RNAnimated.Value(0)).current;
|
const fadeAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Rotation animation
|
// Rotation animation
|
||||||
const spin = RNAnimated.loop(
|
const spin = RNAnimated.loop(
|
||||||
|
|
@ -168,32 +169,32 @@ const SimpleSearchAnimation = () => {
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fade animation
|
// Fade animation
|
||||||
const fade = RNAnimated.timing(fadeAnim, {
|
const fade = RNAnimated.timing(fadeAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start animations
|
// Start animations
|
||||||
spin.start();
|
spin.start();
|
||||||
fade.start();
|
fade.start();
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
return () => {
|
return () => {
|
||||||
spin.stop();
|
spin.stop();
|
||||||
};
|
};
|
||||||
}, [spinAnim, fadeAnim]);
|
}, [spinAnim, fadeAnim]);
|
||||||
|
|
||||||
// Simple rotation interpolation
|
// Simple rotation interpolation
|
||||||
const spin = spinAnim.interpolate({
|
const spin = spinAnim.interpolate({
|
||||||
inputRange: [0, 1],
|
inputRange: [0, 1],
|
||||||
outputRange: ['0deg', '360deg'],
|
outputRange: ['0deg', '360deg'],
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RNAnimated.View
|
<RNAnimated.View
|
||||||
style={[
|
style={[
|
||||||
styles.simpleAnimationContainer,
|
styles.simpleAnimationContainer,
|
||||||
{ opacity: fadeAnim }
|
{ opacity: fadeAnim }
|
||||||
|
|
@ -204,10 +205,10 @@ const SimpleSearchAnimation = () => {
|
||||||
styles.spinnerContainer,
|
styles.spinnerContainer,
|
||||||
{ transform: [{ rotate: spin }], backgroundColor: currentTheme.colors.primary }
|
{ transform: [{ rotate: spin }], backgroundColor: currentTheme.colors.primary }
|
||||||
]}>
|
]}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="search"
|
name="search"
|
||||||
size={32}
|
size={32}
|
||||||
color={currentTheme.colors.white}
|
color={currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
</RNAnimated.View>
|
</RNAnimated.View>
|
||||||
<Text style={[styles.simpleAnimationText, { color: currentTheme.colors.white }]}>Searching</Text>
|
<Text style={[styles.simpleAnimationText, { color: currentTheme.colors.white }]}>Searching</Text>
|
||||||
|
|
@ -268,9 +269,9 @@ const SearchScreen = () => {
|
||||||
StatusBar.setBackgroundColor('transparent');
|
StatusBar.setBackgroundColor('transparent');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
applyStatusBarConfig();
|
applyStatusBarConfig();
|
||||||
|
|
||||||
// Re-apply on focus
|
// Re-apply on focus
|
||||||
const unsubscribe = navigation.addListener('focus', applyStatusBarConfig);
|
const unsubscribe = navigation.addListener('focus', applyStatusBarConfig);
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
|
|
@ -284,7 +285,7 @@ const SearchScreen = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRecentSearches();
|
loadRecentSearches();
|
||||||
|
|
||||||
// Cleanup function to cancel pending searches on unmount
|
// Cleanup function to cancel pending searches on unmount
|
||||||
return () => {
|
return () => {
|
||||||
debouncedSearch.cancel();
|
debouncedSearch.cancel();
|
||||||
|
|
@ -302,12 +303,12 @@ const SearchScreen = () => {
|
||||||
return {
|
return {
|
||||||
opacity: backButtonOpacity.value,
|
opacity: backButtonOpacity.value,
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
translateX: interpolate(
|
translateX: interpolate(
|
||||||
backButtonOpacity.value,
|
backButtonOpacity.value,
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[-20, 0]
|
[-20, 0]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
@ -361,14 +362,14 @@ const SearchScreen = () => {
|
||||||
const saveRecentSearch = async (searchQuery: string) => {
|
const saveRecentSearch = async (searchQuery: string) => {
|
||||||
try {
|
try {
|
||||||
setRecentSearches(prevSearches => {
|
setRecentSearches(prevSearches => {
|
||||||
const newRecentSearches = [
|
const newRecentSearches = [
|
||||||
searchQuery,
|
searchQuery,
|
||||||
...prevSearches.filter(s => s !== searchQuery)
|
...prevSearches.filter(s => s !== searchQuery)
|
||||||
].slice(0, MAX_RECENT_SEARCHES);
|
].slice(0, MAX_RECENT_SEARCHES);
|
||||||
|
|
||||||
// Save to AsyncStorage
|
// Save to AsyncStorage
|
||||||
mmkvStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
|
mmkvStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
|
||||||
|
|
||||||
return newRecentSearches;
|
return newRecentSearches;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -400,7 +401,7 @@ const SearchScreen = () => {
|
||||||
const rank: Record<string, number> = {};
|
const rank: Record<string, number> = {};
|
||||||
addons.forEach((a, idx) => { rank[a.id] = idx; });
|
addons.forEach((a, idx) => { rank[a.id] = idx; });
|
||||||
addonOrderRankRef.current = rank;
|
addonOrderRankRef.current = rank;
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
const handle = catalogService.startLiveSearch(searchQuery, async (section: AddonSearchResults) => {
|
const handle = catalogService.startLiveSearch(searchQuery, async (section: AddonSearchResults) => {
|
||||||
// Append/update this addon section immediately with minimal changes
|
// Append/update this addon section immediately with minimal changes
|
||||||
|
|
@ -444,7 +445,7 @@ const SearchScreen = () => {
|
||||||
// Save to recents after first result batch
|
// Save to recents after first result batch
|
||||||
try {
|
try {
|
||||||
await saveRecentSearch(searchQuery);
|
await saveRecentSearch(searchQuery);
|
||||||
} catch {}
|
} catch { }
|
||||||
});
|
});
|
||||||
liveSearchHandle.current = handle;
|
liveSearchHandle.current = handle;
|
||||||
}, 800);
|
}, 800);
|
||||||
|
|
@ -502,7 +503,7 @@ const SearchScreen = () => {
|
||||||
if (!showRecent || recentSearches.length === 0) return null;
|
if (!showRecent || recentSearches.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={styles.recentSearchesContainer}
|
style={styles.recentSearchesContainer}
|
||||||
entering={FadeIn.duration(300)}
|
entering={FadeIn.duration(300)}
|
||||||
>
|
>
|
||||||
|
|
@ -586,10 +587,10 @@ const SearchScreen = () => {
|
||||||
entering={FadeIn.duration(300).delay(index * 50)}
|
entering={FadeIn.duration(300).delay(index * 50)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={[styles.horizontalItemPosterContainer, {
|
<View style={[styles.horizontalItemPosterContainer, {
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
borderColor: 'rgba(255,255,255,0.05)'
|
borderColor: 'rgba(255,255,255,0.05)'
|
||||||
}]}>
|
}]}>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: item.poster || PLACEHOLDER_POSTER }}
|
source={{ uri: item.poster || PLACEHOLDER_POSTER }}
|
||||||
style={styles.horizontalItemPoster}
|
style={styles.horizontalItemPoster}
|
||||||
|
|
@ -597,28 +598,28 @@ const SearchScreen = () => {
|
||||||
/>
|
/>
|
||||||
{/* Bookmark and watched icons top right, bookmark to the left of watched */}
|
{/* Bookmark and watched icons top right, bookmark to the left of watched */}
|
||||||
{inLibrary && (
|
{inLibrary && (
|
||||||
<View style={[styles.libraryBadge, { position: 'absolute', top: 8, right: 36, backgroundColor: 'transparent', zIndex: 2 }] }>
|
<View style={[styles.libraryBadge, { position: 'absolute', top: 8, right: 36, backgroundColor: 'transparent', zIndex: 2 }]}>
|
||||||
<Feather name="bookmark" size={16} color={currentTheme.colors.white} />
|
<Feather name="bookmark" size={16} color={currentTheme.colors.white} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{watched && (
|
{watched && (
|
||||||
<View style={[styles.watchedIndicator, { position: 'absolute', top: 8, right: 8, backgroundColor: 'transparent', zIndex: 2 }] }>
|
<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'} />
|
<MaterialIcons name="check-circle" size={20} color={currentTheme.colors.success || '#4CAF50'} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{item.imdbRating && (
|
{item.imdbRating && (
|
||||||
<View style={styles.ratingContainer}>
|
<View style={styles.ratingContainer}>
|
||||||
<MaterialIcons name="star" size={12} color="#FFC107" />
|
<MaterialIcons name="star" size={12} color="#FFC107" />
|
||||||
<Text style={[styles.ratingText, { color: currentTheme.colors.white }]}>
|
<Text style={[styles.ratingText, { color: currentTheme.colors.white }]}>
|
||||||
{item.imdbRating}
|
{item.imdbRating}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.horizontalItemTitle,
|
styles.horizontalItemTitle,
|
||||||
{
|
{
|
||||||
color: currentTheme.colors.white,
|
color: currentTheme.colors.white,
|
||||||
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 14,
|
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 14,
|
||||||
lineHeight: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 18,
|
lineHeight: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 18,
|
||||||
|
|
@ -629,36 +630,36 @@ const SearchScreen = () => {
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
{item.year && (
|
{item.year && (
|
||||||
<Text style={[styles.yearText, { color: currentTheme.colors.mediumGray, fontSize: isTV ? 12 : isLargeTablet ? 11 : isTablet ? 10 : 12 }]}>
|
<Text style={[styles.yearText, { color: currentTheme.colors.mediumGray, fontSize: isTV ? 12 : isLargeTablet ? 11 : isTablet ? 10 : 12 }]}>
|
||||||
{item.year}
|
{item.year}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</AnimatedTouchable>
|
</AnimatedTouchable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasResultsToShow = useMemo(() => {
|
const hasResultsToShow = useMemo(() => {
|
||||||
return results.byAddon.length > 0;
|
return results.byAddon.length > 0;
|
||||||
}, [results]);
|
}, [results]);
|
||||||
|
|
||||||
// Memoized addon section to prevent re-rendering unchanged sections
|
// Memoized addon section to prevent re-rendering unchanged sections
|
||||||
const AddonSection = React.memo(({
|
const AddonSection = React.memo(({
|
||||||
addonGroup,
|
addonGroup,
|
||||||
addonIndex
|
addonIndex
|
||||||
}: {
|
}: {
|
||||||
addonGroup: AddonSearchResults;
|
addonGroup: AddonSearchResults;
|
||||||
addonIndex: number;
|
addonIndex: number;
|
||||||
}) => {
|
}) => {
|
||||||
const movieResults = useMemo(() =>
|
const movieResults = useMemo(() =>
|
||||||
addonGroup.results.filter(item => item.type === 'movie'),
|
addonGroup.results.filter(item => item.type === 'movie'),
|
||||||
[addonGroup.results]
|
[addonGroup.results]
|
||||||
);
|
);
|
||||||
const seriesResults = useMemo(() =>
|
const seriesResults = useMemo(() =>
|
||||||
addonGroup.results.filter(item => item.type === 'series'),
|
addonGroup.results.filter(item => item.type === 'series'),
|
||||||
[addonGroup.results]
|
[addonGroup.results]
|
||||||
);
|
);
|
||||||
const otherResults = useMemo(() =>
|
const otherResults = useMemo(() =>
|
||||||
addonGroup.results.filter(item => item.type !== 'movie' && item.type !== 'series'),
|
addonGroup.results.filter(item => item.type !== 'movie' && item.type !== 'series'),
|
||||||
[addonGroup.results]
|
[addonGroup.results]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -679,15 +680,15 @@ const SearchScreen = () => {
|
||||||
{/* Movies */}
|
{/* Movies */}
|
||||||
{movieResults.length > 0 && (
|
{movieResults.length > 0 && (
|
||||||
<Animated.View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]} entering={FadeIn.duration(300)}>
|
<Animated.View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]} entering={FadeIn.duration(300)}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.carouselSubtitle,
|
styles.carouselSubtitle,
|
||||||
{
|
{
|
||||||
color: currentTheme.colors.lightGray,
|
color: currentTheme.colors.lightGray,
|
||||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
Movies ({movieResults.length})
|
Movies ({movieResults.length})
|
||||||
</Text>
|
</Text>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
|
@ -713,15 +714,15 @@ const SearchScreen = () => {
|
||||||
{/* TV Shows */}
|
{/* TV Shows */}
|
||||||
{seriesResults.length > 0 && (
|
{seriesResults.length > 0 && (
|
||||||
<Animated.View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]} entering={FadeIn.duration(300)}>
|
<Animated.View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]} entering={FadeIn.duration(300)}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.carouselSubtitle,
|
styles.carouselSubtitle,
|
||||||
{
|
{
|
||||||
color: currentTheme.colors.lightGray,
|
color: currentTheme.colors.lightGray,
|
||||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
TV Shows ({seriesResults.length})
|
TV Shows ({seriesResults.length})
|
||||||
</Text>
|
</Text>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
|
@ -747,15 +748,15 @@ const SearchScreen = () => {
|
||||||
{/* Other types */}
|
{/* Other types */}
|
||||||
{otherResults.length > 0 && (
|
{otherResults.length > 0 && (
|
||||||
<Animated.View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]} entering={FadeIn.duration(300)}>
|
<Animated.View style={[styles.carouselContainer, { marginBottom: isTV ? 40 : isLargeTablet ? 36 : isTablet ? 32 : 24 }]} entering={FadeIn.duration(300)}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.carouselSubtitle,
|
styles.carouselSubtitle,
|
||||||
{
|
{
|
||||||
color: currentTheme.colors.lightGray,
|
color: currentTheme.colors.lightGray,
|
||||||
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14,
|
||||||
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
marginBottom: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 8,
|
||||||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
{otherResults[0].type.charAt(0).toUpperCase() + otherResults[0].type.slice(1)} ({otherResults.length})
|
{otherResults[0].type.charAt(0).toUpperCase() + otherResults[0].type.slice(1)} ({otherResults.length})
|
||||||
</Text>
|
</Text>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
|
@ -784,12 +785,6 @@ const SearchScreen = () => {
|
||||||
return prev.addonGroup === next.addonGroup && prev.addonIndex === next.addonIndex;
|
return prev.addonGroup === next.addonGroup && prev.addonIndex === next.addonIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
|
|
||||||
// Keep header below floating top navigator on tablets by adding extra offset
|
|
||||||
const tabletNavOffset = (isTV || isLargeTablet || isTablet) ? 64 : 0;
|
|
||||||
const topSpacing = (Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top) + tabletNavOffset;
|
|
||||||
const headerHeight = headerBaseHeight + topSpacing + 60;
|
|
||||||
|
|
||||||
// Set up listeners for watched status and library updates
|
// Set up listeners for watched status and library updates
|
||||||
// These will trigger re-renders in individual SearchResultItem components
|
// These will trigger re-renders in individual SearchResultItem components
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -809,11 +804,11 @@ const SearchScreen = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
|
style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
|
||||||
entering={Platform.OS === 'android' ? undefined : FadeIn.duration(350)}
|
entering={Platform.OS === 'android' ? undefined : FadeIn.duration(350)}
|
||||||
exiting={Platform.OS === 'android' ?
|
exiting={Platform.OS === 'android' ?
|
||||||
FadeOut.duration(200).withInitialValues({ opacity: 1 }) :
|
FadeOut.duration(200).withInitialValues({ opacity: 1 }) :
|
||||||
FadeOut.duration(250)
|
FadeOut.duration(250)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -822,172 +817,170 @@ const SearchScreen = () => {
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
translucent
|
translucent
|
||||||
/>
|
/>
|
||||||
{/* Fixed position header background to prevent shifts */}
|
|
||||||
<View style={[styles.headerBackground, {
|
{/* ScreenHeader Component */}
|
||||||
height: headerHeight,
|
<ScreenHeader
|
||||||
backgroundColor: currentTheme.colors.darkBackground
|
title="Search"
|
||||||
}]} />
|
isTablet={isTV || isLargeTablet || isTablet}
|
||||||
<View style={{ flex: 1 }}>
|
>
|
||||||
{/* Header Section with proper top spacing */}
|
{/* Search Bar */}
|
||||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
<View style={styles.searchBarContainer}>
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Search</Text>
|
<View style={[
|
||||||
<View style={styles.searchBarContainer}>
|
styles.searchBarWrapper,
|
||||||
|
{ width: '100%' }
|
||||||
|
]}>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.searchBarWrapper,
|
styles.searchBar,
|
||||||
{ width: '100%' }
|
{
|
||||||
|
backgroundColor: currentTheme.colors.elevation2,
|
||||||
|
borderColor: 'rgba(255,255,255,0.1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
}
|
||||||
]}>
|
]}>
|
||||||
<View style={[
|
<MaterialIcons
|
||||||
styles.searchBar,
|
name="search"
|
||||||
{
|
size={24}
|
||||||
backgroundColor: currentTheme.colors.elevation2,
|
color={currentTheme.colors.lightGray}
|
||||||
borderColor: 'rgba(255,255,255,0.1)',
|
style={styles.searchIcon}
|
||||||
borderWidth: 1,
|
/>
|
||||||
}
|
<TextInput
|
||||||
]}>
|
style={[
|
||||||
<MaterialIcons
|
styles.searchInput,
|
||||||
name="search"
|
{ color: currentTheme.colors.white }
|
||||||
size={24}
|
]}
|
||||||
color={currentTheme.colors.lightGray}
|
placeholder="Search movies, shows..."
|
||||||
style={styles.searchIcon}
|
placeholderTextColor={currentTheme.colors.lightGray}
|
||||||
/>
|
value={query}
|
||||||
<TextInput
|
onChangeText={setQuery}
|
||||||
style={[
|
returnKeyType="search"
|
||||||
styles.searchInput,
|
keyboardAppearance="dark"
|
||||||
{ color: currentTheme.colors.white }
|
ref={inputRef}
|
||||||
]}
|
/>
|
||||||
placeholder="Search movies, shows..."
|
{query.length > 0 && (
|
||||||
placeholderTextColor={currentTheme.colors.lightGray}
|
<TouchableOpacity
|
||||||
value={query}
|
onPress={handleClearSearch}
|
||||||
onChangeText={setQuery}
|
style={styles.clearButton}
|
||||||
returnKeyType="search"
|
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
|
||||||
keyboardAppearance="dark"
|
>
|
||||||
ref={inputRef}
|
<MaterialIcons
|
||||||
/>
|
name="close"
|
||||||
{query.length > 0 && (
|
size={20}
|
||||||
<TouchableOpacity
|
color={currentTheme.colors.lightGray}
|
||||||
onPress={handleClearSearch}
|
/>
|
||||||
style={styles.clearButton}
|
</TouchableOpacity>
|
||||||
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
|
)}
|
||||||
>
|
|
||||||
<MaterialIcons
|
|
||||||
name="close"
|
|
||||||
size={20}
|
|
||||||
color={currentTheme.colors.lightGray}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{/* Content Container */}
|
</ScreenHeader>
|
||||||
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
|
||||||
{searching ? (
|
{/* Content Container */}
|
||||||
<View style={styles.loadingOverlay} pointerEvents="none">
|
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
<LoadingSpinner
|
{searching ? (
|
||||||
size="large"
|
<View style={styles.loadingOverlay} pointerEvents="none">
|
||||||
offsetY={-60}
|
<LoadingSpinner
|
||||||
|
size="large"
|
||||||
|
offsetY={-60}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : query.trim().length === 1 ? (
|
||||||
|
<Animated.View
|
||||||
|
style={styles.emptyContainer}
|
||||||
|
entering={FadeIn.duration(300)}
|
||||||
|
>
|
||||||
|
<MaterialIcons
|
||||||
|
name="search"
|
||||||
|
size={64}
|
||||||
|
color={currentTheme.colors.lightGray}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||||
|
Keep typing...
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||||
|
Type at least 2 characters to search
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
|
) : searched && !hasResultsToShow ? (
|
||||||
|
<Animated.View
|
||||||
|
style={styles.emptyContainer}
|
||||||
|
entering={FadeIn.duration(300)}
|
||||||
|
>
|
||||||
|
<MaterialIcons
|
||||||
|
name="search-off"
|
||||||
|
size={64}
|
||||||
|
color={currentTheme.colors.lightGray}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||||
|
No results found
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||||
|
Try different keywords or check your spelling
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
|
) : (
|
||||||
|
<Animated.ScrollView
|
||||||
|
style={styles.scrollView}
|
||||||
|
contentContainerStyle={styles.scrollViewContent}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
onScrollBeginDrag={Keyboard.dismiss}
|
||||||
|
entering={FadeIn.duration(300)}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{!query.trim() && renderRecentSearches()}
|
||||||
|
{/* Render results grouped by addon using memoized component */}
|
||||||
|
{results.byAddon.map((addonGroup, addonIndex) => (
|
||||||
|
<AddonSection
|
||||||
|
key={addonGroup.addonId}
|
||||||
|
addonGroup={addonGroup}
|
||||||
|
addonIndex={addonIndex}
|
||||||
/>
|
/>
|
||||||
</View>
|
))}
|
||||||
) : query.trim().length === 1 ? (
|
</Animated.ScrollView>
|
||||||
<Animated.View
|
|
||||||
style={styles.emptyContainer}
|
|
||||||
entering={FadeIn.duration(300)}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
|
||||||
name="search"
|
|
||||||
size={64}
|
|
||||||
color={currentTheme.colors.lightGray}
|
|
||||||
/>
|
|
||||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
|
||||||
Keep typing...
|
|
||||||
</Text>
|
|
||||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
|
||||||
Type at least 2 characters to search
|
|
||||||
</Text>
|
|
||||||
</Animated.View>
|
|
||||||
) : searched && !hasResultsToShow ? (
|
|
||||||
<Animated.View
|
|
||||||
style={styles.emptyContainer}
|
|
||||||
entering={FadeIn.duration(300)}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
|
||||||
name="search-off"
|
|
||||||
size={64}
|
|
||||||
color={currentTheme.colors.lightGray}
|
|
||||||
/>
|
|
||||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
|
||||||
No results found
|
|
||||||
</Text>
|
|
||||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
|
||||||
Try different keywords or check your spelling
|
|
||||||
</Text>
|
|
||||||
</Animated.View>
|
|
||||||
) : (
|
|
||||||
<Animated.ScrollView
|
|
||||||
style={styles.scrollView}
|
|
||||||
contentContainerStyle={styles.scrollViewContent}
|
|
||||||
keyboardShouldPersistTaps="handled"
|
|
||||||
onScrollBeginDrag={Keyboard.dismiss}
|
|
||||||
entering={FadeIn.duration(300)}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
>
|
|
||||||
{!query.trim() && renderRecentSearches()}
|
|
||||||
{/* Render results grouped by addon using memoized component */}
|
|
||||||
{results.byAddon.map((addonGroup, addonIndex) => (
|
|
||||||
<AddonSection
|
|
||||||
key={addonGroup.addonId}
|
|
||||||
addonGroup={addonGroup}
|
|
||||||
addonIndex={addonIndex}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Animated.ScrollView>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
{/* DropUpMenu integration for search results */}
|
|
||||||
{selectedItem && (
|
|
||||||
<DropUpMenu
|
|
||||||
visible={menuVisible}
|
|
||||||
onClose={() => setMenuVisible(false)}
|
|
||||||
item={selectedItem}
|
|
||||||
isSaved={isSaved}
|
|
||||||
isWatched={isWatched}
|
|
||||||
onOptionSelect={async (option: string) => {
|
|
||||||
if (!selectedItem) return;
|
|
||||||
switch (option) {
|
|
||||||
case 'share': {
|
|
||||||
let url = '';
|
|
||||||
if (selectedItem.id) {
|
|
||||||
url = `https://www.imdb.com/title/${selectedItem.id}/`;
|
|
||||||
}
|
|
||||||
const message = `${selectedItem.name}\n${url}`;
|
|
||||||
Share.share({ message, url, title: selectedItem.name });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'library': {
|
|
||||||
if (isSaved) {
|
|
||||||
await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id);
|
|
||||||
setIsSaved(false);
|
|
||||||
} else {
|
|
||||||
await catalogService.addToLibrary(selectedItem);
|
|
||||||
setIsSaved(true);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'watched': {
|
|
||||||
const key = `watched:${selectedItem.type}:${selectedItem.id}`;
|
|
||||||
const newWatched = !isWatched;
|
|
||||||
await mmkvStorage.setItem(key, newWatched ? 'true' : 'false');
|
|
||||||
setIsWatched(newWatched);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
{/* DropUpMenu integration for search results */}
|
||||||
|
{selectedItem && (
|
||||||
|
<DropUpMenu
|
||||||
|
visible={menuVisible}
|
||||||
|
onClose={() => setMenuVisible(false)}
|
||||||
|
item={selectedItem}
|
||||||
|
isSaved={isSaved}
|
||||||
|
isWatched={isWatched}
|
||||||
|
onOptionSelect={async (option: string) => {
|
||||||
|
if (!selectedItem) return;
|
||||||
|
switch (option) {
|
||||||
|
case 'share': {
|
||||||
|
let url = '';
|
||||||
|
if (selectedItem.id) {
|
||||||
|
url = `https://www.imdb.com/title/${selectedItem.id}/`;
|
||||||
|
}
|
||||||
|
const message = `${selectedItem.name}\n${url}`;
|
||||||
|
Share.share({ message, url, title: selectedItem.name });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'library': {
|
||||||
|
if (isSaved) {
|
||||||
|
await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id);
|
||||||
|
setIsSaved(false);
|
||||||
|
} else {
|
||||||
|
await catalogService.addToLibrary(selectedItem);
|
||||||
|
setIsSaved(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'watched': {
|
||||||
|
const key = `watched:${selectedItem.type}:${selectedItem.id}`;
|
||||||
|
const newWatched = !isWatched;
|
||||||
|
await mmkvStorage.setItem(key, newWatched ? 'true' : 'false');
|
||||||
|
setIsWatched(newWatched);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -996,30 +989,10 @@ const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
headerBackground: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
},
|
},
|
||||||
header: {
|
|
||||||
paddingHorizontal: 15,
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
paddingBottom: 0,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
zIndex: 2,
|
|
||||||
},
|
|
||||||
headerTitle: {
|
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: '800',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
searchBarContainer: {
|
searchBarContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue