This commit is contained in:
tapframe 2025-09-20 16:26:52 +05:30
commit 8159cfeadb
4 changed files with 87 additions and 48 deletions

View file

@ -59,6 +59,17 @@ const POSTER_WIDTH = posterLayout.posterWidth;
const PLACEHOLDER_BLURHASH = 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'; const PLACEHOLDER_BLURHASH = 'LEHV6nWB2yk8pyo0adR*.7kCMdnj';
const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, deferMs = 0 }: ContentItemProps) => { const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, deferMs = 0 }: ContentItemProps) => {
// Track inLibrary status locally to force re-render
const [inLibrary, setInLibrary] = useState(!!item.inLibrary);
useEffect(() => {
// Subscribe to library updates and update local state if this item's status changes
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]);
const [menuVisible, setMenuVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false);
const [isWatched, setIsWatched] = useState(false); const [isWatched, setIsWatched] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false); const [imageLoaded, setImageLoaded] = useState(false);
@ -95,7 +106,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
const handleOptionSelect = useCallback((option: string) => { const handleOptionSelect = useCallback((option: string) => {
switch (option) { switch (option) {
case 'library': case 'library':
if (item.inLibrary) { if (inLibrary) {
catalogService.removeFromLibrary(item.type, item.id); catalogService.removeFromLibrary(item.type, item.id);
} else { } else {
catalogService.addToLibrary(item); catalogService.addToLibrary(item);
@ -109,7 +120,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
case 'share': case 'share':
break; break;
} }
}, [item]); }, [item, inLibrary]);
const handleMenuClose = useCallback(() => { const handleMenuClose = useCallback(() => {
setMenuVisible(false); setMenuVisible(false);
@ -238,7 +249,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
</View> </View>
)} )}
{imageError && ( {imageError && (
<View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation1 }]}> <View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation1 }]}>
<MaterialIcons name="broken-image" size={24} color={currentTheme.colors.textMuted} /> <MaterialIcons name="broken-image" size={24} color={currentTheme.colors.textMuted} />
</View> </View>
)} )}
@ -247,7 +258,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} /> <MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
</View> </View>
)} )}
{item.inLibrary && ( {inLibrary && (
<View style={styles.libraryBadge}> <View style={styles.libraryBadge}>
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} /> <MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
</View> </View>
@ -266,6 +277,8 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
onClose={handleMenuClose} onClose={handleMenuClose}
item={item} item={item}
onOptionSelect={handleOptionSelect} onOptionSelect={handleOptionSelect}
isSaved={inLibrary}
isWatched={isWatched}
/> />
</> </>
); );

View file

@ -33,9 +33,11 @@ interface DropUpMenuProps {
onClose: () => void; onClose: () => void;
item: StreamingContent; item: StreamingContent;
onOptionSelect: (option: string) => void; onOptionSelect: (option: string) => void;
isSaved?: boolean; // allow parent to pass saved status directly
isWatched?: boolean; // allow parent to pass watched status directly
} }
export const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => { export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: isSavedProp, isWatched: isWatchedProp }: DropUpMenuProps) => {
const translateY = useSharedValue(300); const translateY = useSharedValue(300);
const opacity = useSharedValue(0); const opacity = useSharedValue(0);
const isDarkMode = useColorScheme() === 'dark'; const isDarkMode = useColorScheme() === 'dark';
@ -87,15 +89,18 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMen
borderTopRightRadius: 24, borderTopRightRadius: 24,
})); }));
// Robustly determine if the item is in the library (saved)
const isSaved = typeof isSavedProp === 'boolean' ? isSavedProp : !!item.inLibrary;
const isWatched = !!isWatchedProp;
const menuOptions = [ const menuOptions = [
{ {
icon: item.inLibrary ? 'bookmark' : 'bookmark-border', icon: isSaved ? 'bookmark' : 'bookmark-border',
label: item.inLibrary ? 'Remove from Library' : 'Add to Library', label: isSaved ? 'Remove from Library' : 'Add to Library',
action: 'library' action: 'library'
}, },
{ {
icon: 'check-circle', icon: 'check-circle',
label: 'Mark as Watched', label: isWatched ? 'Mark as Unwatched' : 'Mark as Watched',
action: 'watched' action: 'watched'
}, },
{ {

View file

@ -116,7 +116,7 @@ const NotificationSettingsScreen = () => {
const resetAllNotifications = async () => { const resetAllNotifications = async () => {
Alert.alert( Alert.alert(
'Reset Notifications', 'Reset Notifications',
'This will cancel all scheduled notifications. Are you sure?', 'This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?',
[ [
{ {
text: 'Cancel', text: 'Cancel',
@ -127,7 +127,11 @@ const NotificationSettingsScreen = () => {
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
try { try {
await notificationService.cancelAllNotifications(); // Cancel all notifications for all series, but do not remove from saved
const scheduledNotifications = notificationService.getScheduledNotifications?.() || [];
for (const notification of scheduledNotifications) {
await notificationService.cancelNotification(notification.id);
}
Alert.alert('Success', 'All notifications have been reset'); Alert.alert('Success', 'All notifications have been reset');
} catch (error) { } catch (error) {
logger.error('Error resetting notifications:', error); logger.error('Error resetting notifications:', error);
@ -164,9 +168,24 @@ const NotificationSettingsScreen = () => {
const handleTestNotification = async () => { const handleTestNotification = async () => {
try { try {
// Cancel previous test notification if exists // Remove all previous test notifications before scheduling a new one
if (testNotificationId) { const scheduled = notificationService.getScheduledNotifications?.() || [];
await notificationService.cancelNotification(testNotificationId); const testNotifications = scheduled.filter(n => n.id.startsWith('test-notification-'));
if (testNotifications.length > 0 && typeof notificationService.cancelNotification === 'function') {
for (const n of testNotifications) {
await notificationService.cancelNotification(n.id);
}
}
// Temporarily override timeBeforeAiring to 0 for the test notification
let originalTimeBeforeAiring: number | undefined = undefined;
if (typeof notificationService.getSettings === 'function') {
const currentSettings = await notificationService.getSettings();
originalTimeBeforeAiring = currentSettings.timeBeforeAiring;
if (typeof notificationService.updateSettings === 'function') {
await notificationService.updateSettings({ timeBeforeAiring: 0 });
}
} }
const testNotification = { const testNotification = {
@ -176,15 +195,24 @@ const NotificationSettingsScreen = () => {
episodeTitle: 'Test Episode', episodeTitle: 'Test Episode',
season: 1, season: 1,
episode: 1, episode: 1,
releaseDate: new Date(Date.now() + 60000).toISOString(), // 1 minute from now releaseDate: new Date(Date.now() + 5000).toISOString(), // 5 seconds from now
notified: false notified: false
}; };
const notificationId = await notificationService.scheduleEpisodeNotification(testNotification); const notificationId = await notificationService.scheduleEpisodeNotification(testNotification);
// Restore original timeBeforeAiring
if (
typeof notificationService.updateSettings === 'function' &&
originalTimeBeforeAiring !== undefined
) {
await notificationService.updateSettings({ timeBeforeAiring: originalTimeBeforeAiring });
}
if (notificationId) { if (notificationId) {
setTestNotificationId(notificationId); setTestNotificationId(notificationId);
setCountdown(60); // Start 60 second countdown setCountdown(0); // No countdown for instant notification
Alert.alert('Success', 'Test notification scheduled for 1 minute from now'); Alert.alert('Success', 'Test notification scheduled to fire instantly');
} else { } else {
Alert.alert('Error', 'Failed to schedule test notification. Make sure notifications are enabled.'); Alert.alert('Error', 'Failed to schedule test notification. Make sure notifications are enabled.');
} }
@ -425,7 +453,7 @@ const NotificationSettingsScreen = () => {
<Text style={[styles.resetButtonText, { color: currentTheme.colors.primary }]}> <Text style={[styles.resetButtonText, { color: currentTheme.colors.primary }]}>
{countdown !== null {countdown !== null
? `Notification in ${countdown}s...` ? `Notification in ${countdown}s...`
: 'Test Notification (1min)'} : 'Test Notification (5 sec)'}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -442,10 +470,6 @@ const NotificationSettingsScreen = () => {
</Text> </Text>
</View> </View>
)} )}
<Text style={[styles.resetDescription, { color: currentTheme.colors.lightGray }]}>
This will cancel all scheduled notifications. You'll need to re-enable them manually.
</Text>
</View> </View>
</> </>
)} )}

View file

@ -1,4 +1,5 @@
import { stremioService, Meta, Manifest } from './stremioService'; import { stremioService, Meta, Manifest } from './stremioService';
import { notificationService } from './notificationService';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import axios from 'axios'; import axios from 'axios';
import { TMDBService } from './tmdbService'; import { TMDBService } from './tmdbService';
@ -690,15 +691,14 @@ class CatalogService {
try { this.libraryAddListeners.forEach(l => l(content)); } catch {} try { this.libraryAddListeners.forEach(l => l(content)); } catch {}
// Auto-setup notifications for series when added to library // Auto-setup notifications for series when added to library
// if (content.type === 'series') { if (content.type === 'series') {
// try { try {
// const { notificationService } = await import('./notificationService'); await notificationService.updateNotificationsForSeries(content.id);
// await notificationService.updateNotificationsForSeries(content.id); console.log(`[CatalogService] Auto-setup notifications for series: ${content.name}`);
// console.log(`[CatalogService] Auto-setup notifications for series: ${content.name}`); } catch (error) {
// } catch (error) { console.error(`[CatalogService] Failed to setup notifications for ${content.name}:`, error);
// console.error(`[CatalogService] Failed to setup notifications for ${content.name}:`, error); }
// } }
// }
} }
public async removeFromLibrary(type: string, id: string): Promise<void> { public async removeFromLibrary(type: string, id: string): Promise<void> {
@ -707,24 +707,21 @@ class CatalogService {
this.saveLibrary(); this.saveLibrary();
this.notifyLibrarySubscribers(); this.notifyLibrarySubscribers();
try { this.libraryRemoveListeners.forEach(l => l(type, id)); } catch {} try { this.libraryRemoveListeners.forEach(l => l(type, id)); } catch {}
// Cancel notifications for series when removed from library // Cancel notifications for series when removed from library
// if (type === 'series') { if (type === 'series') {
// try { try {
// const { notificationService } = await import('./notificationService'); // Cancel all notifications for this series
// // Cancel all notifications for this series const scheduledNotifications = notificationService.getScheduledNotifications();
// const scheduledNotifications = await notificationService.getScheduledNotifications(); const seriesToCancel = scheduledNotifications.filter(notification => notification.seriesId === id);
// const seriesToCancel = scheduledNotifications.filter(notification => notification.seriesId === id); for (const notification of seriesToCancel) {
// await notificationService.cancelNotification(notification.id);
// for (const notification of seriesToCancel) { }
// await notificationService.cancelNotification(notification.id); console.log(`[CatalogService] Cancelled ${seriesToCancel.length} notifications for removed series: ${id}`);
// } } catch (error) {
// console.error(`[CatalogService] Failed to cancel notifications for removed series ${id}:`, error);
// console.log(`[CatalogService] Cancelled ${seriesToCancel.length} notifications for removed series: ${id}`); }
// } catch (error) { }
// console.error(`[CatalogService] Failed to cancel notifications for removed series ${id}:`, error);
// }
// }
} }
private addToRecentContent(content: StreamingContent): void { private addToRecentContent(content: StreamingContent): void {