Added the longpress menu for the library menu
This commit is contained in:
parent
7fa4d20da0
commit
35ace0214a
4 changed files with 168 additions and 14 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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,65 @@ const LibraryScreen = () => {
|
|||
{showTraktContent ? renderTraktContent() : renderContent()}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* DropUpMenu integration */}
|
||||
{selectedItem && (
|
||||
<DropUpMenu
|
||||
visible={menuVisible}
|
||||
onClose={() => 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}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue