mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 08:41:57 +00:00
Merge branch 'main' of https://github.com/tapframe/NuvioStreaming
This commit is contained in:
commit
8159cfeadb
4 changed files with 87 additions and 48 deletions
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue