diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index 86eb7769..9e491e45 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -1,4 +1,6 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { toast } from '@backpackapp-io/react-native-toast'; +import { DeviceEventEmitter } from 'react-native'; import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Animated } from 'react-native'; import { Image as ExpoImage } from 'expo-image'; import { MaterialIcons } from '@expo/vector-icons'; @@ -74,8 +76,13 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe // Load watched state from AsyncStorage when item changes useEffect(() => { - AsyncStorage.getItem(`watched_${item.id}`).then(val => setIsWatched(val === 'true')); - }, [item.id]); + 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); @@ -114,14 +121,21 @@ 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(prevWatched => { const newWatched = !prevWatched; - AsyncStorage.setItem(`watched_${item.id}`, newWatched ? 'true' : 'false'); + 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); diff --git a/src/components/home/DropUpMenu.tsx b/src/components/home/DropUpMenu.tsx index d84ca623..d75725c2 100644 --- a/src/components/home/DropUpMenu.tsx +++ b/src/components/home/DropUpMenu.tsx @@ -92,10 +92,10 @@ 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', - label: isSaved ? 'Remove from Library' : 'Add to Library', + icon: 'bookmark', + label: 'Remove from Library', action: 'library' }, { @@ -115,6 +115,11 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is } ]; + // If used in LibraryScreen, only show 'Remove from Library' + if (isSavedProp === true) { + menuOptions = menuOptions.filter(opt => opt.action !== 'library' || opt.label === 'Remove from Library'); + } + const backgroundColor = isDarkMode ? '#1A1A1A' : '#FFFFFF'; return ( diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 293aed72..854e5264 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -1337,4 +1337,17 @@ const styles = StyleSheet.create({ }, }); -export default React.memo(HomeScreen); \ No newline at end of file +import { DeviceEventEmitter } from 'react-native'; + +const HomeScreenWithFocusSync = (props: any) => { + const navigation = useNavigation>(); + useEffect(() => { + const unsubscribe = navigation.addListener('focus', () => { + DeviceEventEmitter.emit('watchedStatusChanged'); + }); + return () => unsubscribe(); + }, [navigation]); + return ; +}; + +export default React.memo(HomeScreenWithFocusSync); \ No newline at end of file diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 71a6848c..71d238a9 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -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(null); + // DropUpMenu state + const [menuVisible, setMenuVisible] = useState(false); + const [selectedItem, setSelectedItem] = useState(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 = () => { navigation.navigate('Metadata', { id: item.id, type: item.type })} + onLongPress={() => { + setSelectedItem(item); + setMenuVisible(true); + }} activeOpacity={0.7} > - + - + {item.watched && ( + + + + )} {item.progress !== undefined && item.progress < 1 && ( { )} - + {item.name} @@ -932,6 +987,65 @@ const LibraryScreen = () => { {showTraktContent ? renderTraktContent() : renderContent()} + + {/* DropUpMenu integration */} + {selectedItem && ( + setMenuVisible(false)} + item={selectedItem} + isWatched={!!selectedItem.watched} + onOptionSelect={async (option) => { + if (!selectedItem) return; + if (option === 'share') { + let url = ''; + if (selectedItem.imdbId) { + url = `https://www.imdb.com/title/${selectedItem.imdbId}/`; + } else if (selectedItem.traktId) { + url = `https://trakt.tv/${selectedItem.type === 'movie' ? 'movies' : 'shows'}/${selectedItem.traktId}`; + } else { + url = selectedItem.poster || ''; + } + try { + await Share.share({ + message: `${selectedItem.name}\n${url}`, + url, + title: selectedItem.name, + }); + } catch (error) { + toast('Failed to share', { duration: 1200 }); + } + } else if (option === 'library') { + try { + // Always remove from library in LibraryScreen + 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 }); + } + } else if (option === '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 }); + } + } + }} + isSaved={!!selectedItem.inLibrary} + /> + )} ); }; @@ -947,6 +1061,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, },