mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-18 07:12:18 +00:00
Merge pull request #156 from CrissZollo/fix-action-menu
Action menu fixes and utilisation
This commit is contained in:
commit
56654e1ced
7 changed files with 370 additions and 49 deletions
15
package-lock.json
generated
15
package-lock.json
generated
|
|
@ -14,7 +14,7 @@
|
|||
"@expo/metro-runtime": "~4.0.1",
|
||||
"@expo/vector-icons": "~14.0.4",
|
||||
"@lottiefiles/dotlottie-react": "^0.6.5",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-async-storage/async-storage": "^1.23.1",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-native-community/slider": "4.5.5",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"@supabase/supabase-js": "^2.54.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/react-native-video": "^5.0.20",
|
||||
"axios": "^1.11.0",
|
||||
"axios": "^1.12.2",
|
||||
"axios-cookiejar-support": "^6.0.4",
|
||||
"cheerio-without-node-native": "^0.20.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
|
|
@ -15007,17 +15007,6 @@
|
|||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
|
||||
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"@expo/metro-runtime": "~4.0.1",
|
||||
"@expo/vector-icons": "~14.0.4",
|
||||
"@lottiefiles/dotlottie-react": "^0.6.5",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-async-storage/async-storage": "^1.23.1",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-native-community/slider": "4.5.5",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"@supabase/supabase-js": "^2.54.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/react-native-video": "^5.0.20",
|
||||
"axios": "^1.11.0",
|
||||
"axios": "^1.12.2",
|
||||
"axios-cookiejar-support": "^6.0.4",
|
||||
"cheerio-without-node-native": "^0.20.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Animated } from 'react-native';
|
||||
import { toast } from '@backpackapp-io/react-native-toast';
|
||||
import { DeviceEventEmitter } from 'react-native';
|
||||
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Animated, Share } from 'react-native';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { catalogService, StreamingContent } from '../../services/catalogService';
|
||||
import { DropUpMenu } from './DropUpMenu';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
interface ContentItemProps {
|
||||
item: StreamingContent;
|
||||
|
|
@ -70,6 +73,17 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
});
|
||||
return () => unsubscribe();
|
||||
}, [item.id, item.type]);
|
||||
|
||||
// Load watched state from AsyncStorage when item changes
|
||||
useEffect(() => {
|
||||
const updateWatched = () => {
|
||||
AsyncStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setIsWatched(val === 'true'));
|
||||
};
|
||||
updateWatched();
|
||||
const sub = DeviceEventEmitter.addListener('watchedStatusChanged', updateWatched);
|
||||
return () => sub.remove();
|
||||
}, [item.id, item.type]);
|
||||
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [isWatched, setIsWatched] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
|
@ -107,17 +121,37 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
case 'library':
|
||||
if (inLibrary) {
|
||||
catalogService.removeFromLibrary(item.type, item.id);
|
||||
toast('Removed from Library', { duration: 1200 });
|
||||
} else {
|
||||
catalogService.addToLibrary(item);
|
||||
toast('Added to Library', { duration: 1200 });
|
||||
}
|
||||
break;
|
||||
case 'watched':
|
||||
setIsWatched(prev => !prev);
|
||||
case 'watched': {
|
||||
setIsWatched(prevWatched => {
|
||||
const newWatched = !prevWatched;
|
||||
AsyncStorage.setItem(`watched:${item.type}:${item.id}`, newWatched ? 'true' : 'false');
|
||||
toast(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', { duration: 1200 });
|
||||
// Fire a custom event so other screens can update
|
||||
setTimeout(() => {
|
||||
DeviceEventEmitter.emit('watchedStatusChanged');
|
||||
}, 100);
|
||||
return newWatched;
|
||||
});
|
||||
setMenuVisible(false);
|
||||
break;
|
||||
}
|
||||
case 'playlist':
|
||||
break;
|
||||
case 'share':
|
||||
case 'share': {
|
||||
let url = '';
|
||||
if (item.id) {
|
||||
url = `https://www.imdb.com/title/${item.id}/`;
|
||||
}
|
||||
const message = `${item.name}\n${url}`;
|
||||
Share.share({ message, url, title: item.name });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [item, inLibrary]);
|
||||
|
||||
|
|
|
|||
|
|
@ -92,9 +92,9 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
// Robustly determine if the item is in the library (saved)
|
||||
const isSaved = typeof isSavedProp === 'boolean' ? isSavedProp : !!item.inLibrary;
|
||||
const isWatched = !!isWatchedProp;
|
||||
const menuOptions = [
|
||||
let menuOptions = [
|
||||
{
|
||||
icon: isSaved ? 'bookmark' : 'bookmark-border',
|
||||
icon: 'bookmark',
|
||||
label: isSaved ? 'Remove from Library' : 'Add to Library',
|
||||
action: 'library'
|
||||
},
|
||||
|
|
@ -103,11 +103,13 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
label: isWatched ? 'Mark as Unwatched' : 'Mark as Watched',
|
||||
action: 'watched'
|
||||
},
|
||||
/*
|
||||
{
|
||||
icon: 'playlist-add',
|
||||
label: 'Add to Playlist',
|
||||
action: 'playlist'
|
||||
},
|
||||
*/
|
||||
{
|
||||
icon: 'share',
|
||||
label: 'Share',
|
||||
|
|
@ -115,6 +117,11 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
}
|
||||
];
|
||||
|
||||
// If used in LibraryScreen, only show 'Remove from Library' if item is in library
|
||||
if (isSavedProp === true) {
|
||||
menuOptions = menuOptions.filter(opt => opt.action !== 'library' || isSaved);
|
||||
}
|
||||
|
||||
const backgroundColor = isDarkMode ? '#1A1A1A' : '#FFFFFF';
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1337,4 +1337,17 @@ const styles = StyleSheet.create<any>({
|
|||
},
|
||||
});
|
||||
|
||||
export default React.memo(HomeScreen);
|
||||
import { DeviceEventEmitter } from 'react-native';
|
||||
|
||||
const HomeScreenWithFocusSync = (props: any) => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('focus', () => {
|
||||
DeviceEventEmitter.emit('watchedStatusChanged');
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, [navigation]);
|
||||
return <HomeScreen {...props} />;
|
||||
};
|
||||
|
||||
export default React.memo(HomeScreenWithFocusSync);
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { DeviceEventEmitter } from 'react-native';
|
||||
import { Share } from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { toast } from '@backpackapp-io/react-native-toast';
|
||||
import DropUpMenu from '../components/home/DropUpMenu';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -40,6 +45,7 @@ interface LibraryItem extends StreamingContent {
|
|||
imdbId?: string;
|
||||
traktId: number;
|
||||
images?: TraktImages;
|
||||
watched?: boolean;
|
||||
}
|
||||
|
||||
interface TraktDisplayItem {
|
||||
|
|
@ -205,6 +211,9 @@ const LibraryScreen = () => {
|
|||
const [filter, setFilter] = useState<'trakt' | 'movies' | 'series'>('movies');
|
||||
const [showTraktContent, setShowTraktContent] = useState(false);
|
||||
const [selectedTraktFolder, setSelectedTraktFolder] = useState<string | null>(null);
|
||||
// DropUpMenu state
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<LibraryItem | null>(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
|
@ -267,7 +276,22 @@ const LibraryScreen = () => {
|
|||
setLoading(true);
|
||||
try {
|
||||
const items = await catalogService.getLibraryItems();
|
||||
setLibraryItems(items as LibraryItem[]);
|
||||
// Load watched status for each item from AsyncStorage
|
||||
const updatedItems = await Promise.all(items.map(async (item) => {
|
||||
// Map StreamingContent to LibraryItem shape
|
||||
const libraryItem: LibraryItem = {
|
||||
...item,
|
||||
gradient: Array.isArray((item as any).gradient) ? (item as any).gradient : ['#222', '#444'],
|
||||
traktId: typeof (item as any).traktId === 'number' ? (item as any).traktId : 0,
|
||||
};
|
||||
const key = `watched:${item.type}:${item.id}`;
|
||||
const watched = await AsyncStorage.getItem(key);
|
||||
return {
|
||||
...libraryItem,
|
||||
watched: watched === 'true'
|
||||
};
|
||||
}));
|
||||
setLibraryItems(updatedItems);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load library:', error);
|
||||
} finally {
|
||||
|
|
@ -278,14 +302,37 @@ const LibraryScreen = () => {
|
|||
loadLibrary();
|
||||
|
||||
// Subscribe to library updates
|
||||
const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => {
|
||||
setLibraryItems(items as LibraryItem[]);
|
||||
const unsubscribe = catalogService.subscribeToLibraryUpdates(async (items) => {
|
||||
// Sync watched status on update
|
||||
const updatedItems = await Promise.all(items.map(async (item) => {
|
||||
// Map StreamingContent to LibraryItem shape
|
||||
const libraryItem: LibraryItem = {
|
||||
...item,
|
||||
gradient: Array.isArray((item as any).gradient) ? (item as any).gradient : ['#222', '#444'],
|
||||
traktId: typeof (item as any).traktId === 'number' ? (item as any).traktId : 0,
|
||||
};
|
||||
const key = `watched:${item.type}:${item.id}`;
|
||||
const watched = await AsyncStorage.getItem(key);
|
||||
return {
|
||||
...libraryItem,
|
||||
watched: watched === 'true'
|
||||
};
|
||||
}));
|
||||
setLibraryItems(updatedItems);
|
||||
});
|
||||
|
||||
// Listen for watched status changes
|
||||
const watchedSub = DeviceEventEmitter.addListener('watchedStatusChanged', loadLibrary);
|
||||
|
||||
// Refresh when screen regains focus
|
||||
const focusSub = navigation.addListener('focus', loadLibrary);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
watchedSub.remove();
|
||||
focusSub();
|
||||
};
|
||||
}, []);
|
||||
}, [navigation]);
|
||||
|
||||
const filteredItems = libraryItems.filter(item => {
|
||||
if (filter === 'movies') return item.type === 'movie';
|
||||
|
|
@ -348,17 +395,25 @@ const LibraryScreen = () => {
|
|||
<TouchableOpacity
|
||||
style={[styles.itemContainer, { width: itemWidth }]}
|
||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||
onLongPress={() => {
|
||||
setSelectedItem(item);
|
||||
setMenuVisible(true);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<Image
|
||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||
style={styles.poster}
|
||||
contentFit="cover"
|
||||
transition={300}
|
||||
/>
|
||||
|
||||
{item.watched && (
|
||||
<View style={styles.watchedIndicator}>
|
||||
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success || '#4CAF50'} />
|
||||
</View>
|
||||
)}
|
||||
{item.progress !== undefined && item.progress < 1 && (
|
||||
<View style={styles.progressBarContainer}>
|
||||
<View
|
||||
|
|
@ -370,7 +425,7 @@ const LibraryScreen = () => {
|
|||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.white }]}>
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.white }]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -932,6 +987,62 @@ const LibraryScreen = () => {
|
|||
{showTraktContent ? renderTraktContent() : renderContent()}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* DropUpMenu integration */}
|
||||
{selectedItem && (
|
||||
<DropUpMenu
|
||||
visible={menuVisible}
|
||||
onClose={() => setMenuVisible(false)}
|
||||
item={selectedItem}
|
||||
isWatched={!!selectedItem.watched}
|
||||
isSaved={true} // Since this is from library, it's always saved
|
||||
onOptionSelect={async (option) => {
|
||||
if (!selectedItem) return;
|
||||
switch (option) {
|
||||
case 'library': {
|
||||
try {
|
||||
await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id);
|
||||
toast('Removed from Library', { duration: 1200 });
|
||||
setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type)));
|
||||
setMenuVisible(false);
|
||||
} catch (error) {
|
||||
toast('Failed to update Library', { duration: 1200 });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'watched': {
|
||||
try {
|
||||
// Use AsyncStorage to store watched status by key
|
||||
const key = `watched:${selectedItem.type}:${selectedItem.id}`;
|
||||
const newWatched = !selectedItem.watched;
|
||||
await AsyncStorage.setItem(key, newWatched ? 'true' : 'false');
|
||||
toast(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', { duration: 1200 });
|
||||
// Instantly update local state
|
||||
setLibraryItems(prev => prev.map(item =>
|
||||
item.id === selectedItem.id && item.type === selectedItem.type
|
||||
? { ...item, watched: newWatched }
|
||||
: item
|
||||
));
|
||||
} catch (error) {
|
||||
toast('Failed to update watched status', { duration: 1200 });
|
||||
}
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -947,6 +1058,14 @@ const styles = StyleSheet.create({
|
|||
right: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
watchedIndicator: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
borderRadius: 12,
|
||||
padding: 2,
|
||||
zIndex: 2,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import { MaterialIcons } from '@expo/vector-icons';
|
|||
import { catalogService, StreamingContent } from '../services/catalogService';
|
||||
import { Image } from 'expo-image';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { DropUpMenu } from '../components/home/DropUpMenu';
|
||||
import { DeviceEventEmitter, Share } from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
|
|
@ -207,7 +209,26 @@ const SearchScreen = () => {
|
|||
const inputRef = useRef<TextInput>(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// DropUpMenu state
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<StreamingContent | null>(null);
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
const [isWatched, setIsWatched] = useState(false);
|
||||
const [refreshFlag, setRefreshFlag] = React.useState(false);
|
||||
|
||||
// Update isSaved and isWatched when selectedItem changes
|
||||
useEffect(() => {
|
||||
if (!selectedItem) return;
|
||||
(async () => {
|
||||
// Check if item is in library
|
||||
const items = await catalogService.getLibraryItems();
|
||||
const found = items.find((libItem: any) => libItem.id === selectedItem.id && libItem.type === selectedItem.type);
|
||||
setIsSaved(!!found);
|
||||
// Check watched status
|
||||
const val = await AsyncStorage.getItem(`watched:${selectedItem.type}:${selectedItem.id}`);
|
||||
setIsWatched(val === 'true');
|
||||
})();
|
||||
}, [selectedItem]);
|
||||
// Animation values
|
||||
const searchBarWidth = useSharedValue(width - 32);
|
||||
const searchBarOpacity = useSharedValue(1);
|
||||
|
|
@ -441,35 +462,85 @@ const SearchScreen = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const renderHorizontalItem = ({ item, index }: { item: StreamingContent, index: number }) => {
|
||||
const SearchResultItem = ({ item, index, navigation, setSelectedItem, setMenuVisible, currentTheme, refreshFlag }: {
|
||||
item: StreamingContent;
|
||||
index: number;
|
||||
navigation: any;
|
||||
setSelectedItem: (item: StreamingContent) => void;
|
||||
setMenuVisible: (visible: boolean) => void;
|
||||
currentTheme: any;
|
||||
refreshFlag: boolean;
|
||||
}) => {
|
||||
const [inLibrary, setInLibrary] = React.useState(!!item.inLibrary);
|
||||
const [watched, setWatched] = React.useState(false);
|
||||
// Re-check status when refreshFlag changes
|
||||
React.useEffect(() => {
|
||||
AsyncStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setWatched(val === 'true'));
|
||||
const items = catalogService.getLibraryItems();
|
||||
const found = items.find((libItem: any) => libItem.id === item.id && libItem.type === item.type);
|
||||
setInLibrary(!!found);
|
||||
}, [refreshFlag, item.id, item.type]);
|
||||
React.useEffect(() => {
|
||||
const updateWatched = () => {
|
||||
AsyncStorage.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]);
|
||||
React.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 (
|
||||
<AnimatedTouchable
|
||||
style={styles.horizontalItem}
|
||||
onPress={() => {
|
||||
navigation.navigate('Metadata', { id: item.id, type: item.type });
|
||||
}}
|
||||
onLongPress={() => {
|
||||
setSelectedItem(item);
|
||||
setMenuVisible(true);
|
||||
// Do NOT toggle refreshFlag here
|
||||
}}
|
||||
delayLongPress={300}
|
||||
entering={FadeIn.duration(300).delay(index * 50)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.horizontalItemPosterContainer, {
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
borderColor: 'rgba(255,255,255,0.05)'
|
||||
}]}>
|
||||
}]}>
|
||||
<Image
|
||||
source={{ uri: item.poster || PLACEHOLDER_POSTER }}
|
||||
style={styles.horizontalItemPoster}
|
||||
contentFit="cover"
|
||||
transition={300}
|
||||
/>
|
||||
{/* Bookmark and watched icons top right, bookmark to the left of watched */}
|
||||
{inLibrary && (
|
||||
<View style={[styles.libraryBadge, { position: 'absolute', top: 8, right: 36, backgroundColor: 'transparent', zIndex: 2 }] }>
|
||||
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
|
||||
</View>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{/* 'series'/'movie' text in original place */}
|
||||
<View style={styles.itemTypeContainer}>
|
||||
<Text style={[styles.itemTypeText, { color: currentTheme.colors.white }]}>
|
||||
<Text style={[styles.itemTypeText, { color: currentTheme.colors.white }]}>
|
||||
{item.type === 'movie' ? 'MOVIE' : 'SERIES'}
|
||||
</Text>
|
||||
</View>
|
||||
{item.imdbRating && (
|
||||
<View style={styles.ratingContainer}>
|
||||
<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}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -482,7 +553,7 @@ const SearchScreen = () => {
|
|||
{item.name}
|
||||
</Text>
|
||||
{item.year && (
|
||||
<Text style={[styles.yearText, { color: currentTheme.colors.mediumGray }]}>
|
||||
<Text style={[styles.yearText, { color: currentTheme.colors.mediumGray }]}>
|
||||
{item.year}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -506,6 +577,17 @@ const SearchScreen = () => {
|
|||
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
|
||||
const headerHeight = headerBaseHeight + topSpacing + 60;
|
||||
|
||||
useEffect(() => {
|
||||
const watchedSub = DeviceEventEmitter.addListener('watchedStatusChanged', () => setRefreshFlag(f => !f));
|
||||
const librarySub = catalogService.subscribeToLibraryUpdates(() => setRefreshFlag(f => !f));
|
||||
const focusSub = navigation.addListener('focus', () => setRefreshFlag(f => !f));
|
||||
return () => {
|
||||
watchedSub.remove();
|
||||
librarySub();
|
||||
focusSub();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
|
||||
|
|
@ -520,13 +602,11 @@ const SearchScreen = () => {
|
|||
backgroundColor="transparent"
|
||||
translucent
|
||||
/>
|
||||
|
||||
{/* Fixed position header background to prevent shifts */}
|
||||
<View style={[styles.headerBackground, {
|
||||
height: headerHeight,
|
||||
backgroundColor: currentTheme.colors.darkBackground
|
||||
}]} />
|
||||
|
||||
<View style={{ flex: 1 }}>
|
||||
{/* Header Section with proper top spacing */}
|
||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
||||
|
|
@ -580,7 +660,6 @@ const SearchScreen = () => {
|
|||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content Container */}
|
||||
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
{searching ? (
|
||||
|
|
@ -600,10 +679,10 @@ const SearchScreen = () => {
|
|||
size={64}
|
||||
color={currentTheme.colors.lightGray}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||
Keep typing...
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||
Type at least 2 characters to search
|
||||
</Text>
|
||||
</Animated.View>
|
||||
|
|
@ -617,10 +696,10 @@ const SearchScreen = () => {
|
|||
size={64}
|
||||
color={currentTheme.colors.lightGray}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||
No results found
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||
Try different keywords or check your spelling
|
||||
</Text>
|
||||
</Animated.View>
|
||||
|
|
@ -634,48 +713,110 @@ const SearchScreen = () => {
|
|||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{!query.trim() && renderRecentSearches()}
|
||||
|
||||
{movieResults.length > 0 && (
|
||||
<Animated.View
|
||||
style={styles.carouselContainer}
|
||||
entering={FadeIn.duration(300)}
|
||||
>
|
||||
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
|
||||
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
|
||||
Movies ({movieResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={movieResults}
|
||||
renderItem={renderHorizontalItem}
|
||||
renderItem={({ item, index }) => (
|
||||
<SearchResultItem
|
||||
item={item}
|
||||
index={index}
|
||||
navigation={navigation}
|
||||
setSelectedItem={setSelectedItem}
|
||||
setMenuVisible={setMenuVisible}
|
||||
currentTheme={currentTheme}
|
||||
refreshFlag={refreshFlag}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => `movie-${item.id}`}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalListContent}
|
||||
extraData={refreshFlag}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
{seriesResults.length > 0 && (
|
||||
<Animated.View
|
||||
style={styles.carouselContainer}
|
||||
entering={FadeIn.duration(300).delay(50)}
|
||||
>
|
||||
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
|
||||
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
|
||||
TV Shows ({seriesResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={seriesResults}
|
||||
renderItem={renderHorizontalItem}
|
||||
renderItem={({ item, index }) => (
|
||||
<SearchResultItem
|
||||
item={item}
|
||||
index={index}
|
||||
navigation={navigation}
|
||||
setSelectedItem={setSelectedItem}
|
||||
setMenuVisible={setMenuVisible}
|
||||
currentTheme={currentTheme}
|
||||
refreshFlag={refreshFlag}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => `series-${item.id}`}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalListContent}
|
||||
extraData={refreshFlag}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
</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 AsyncStorage.setItem(key, newWatched ? 'true' : 'false');
|
||||
setIsWatched(newWatched);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
|
|
@ -954,6 +1095,24 @@ const styles = StyleSheet.create({
|
|||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
watchedIndicator: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
borderRadius: 12,
|
||||
padding: 2,
|
||||
zIndex: 2,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
libraryBadge: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 36,
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
zIndex: 2,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export default SearchScreen;
|
||||
Loading…
Reference in a new issue