Merge pull request #156 from CrissZollo/fix-action-menu

Action menu fixes and utilisation
This commit is contained in:
tapframe 2025-10-01 00:57:09 +05:30 committed by GitHub
commit 56654e1ced
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 370 additions and 49 deletions

15
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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]);

View file

@ -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 (

View file

@ -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);

View file

@ -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,
},

View file

@ -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;