ui changes
This commit is contained in:
parent
a7f850d577
commit
32bec08f30
11 changed files with 747 additions and 164 deletions
3
App.tsx
3
App.tsx
|
|
@ -40,6 +40,7 @@ import UpdateService from './src/services/updateService';
|
||||||
import { memoryMonitorService } from './src/services/memoryMonitorService';
|
import { memoryMonitorService } from './src/services/memoryMonitorService';
|
||||||
import { aiService } from './src/services/aiService';
|
import { aiService } from './src/services/aiService';
|
||||||
import { AccountProvider, useAccount } from './src/contexts/AccountContext';
|
import { AccountProvider, useAccount } from './src/contexts/AccountContext';
|
||||||
|
import { ToastProvider } from './src/contexts/ToastContext';
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
||||||
|
|
@ -203,7 +204,9 @@ function App(): React.JSX.Element {
|
||||||
<TraktProvider>
|
<TraktProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<TrailerProvider>
|
<TrailerProvider>
|
||||||
|
<ToastProvider>
|
||||||
<ThemedApp />
|
<ThemedApp />
|
||||||
|
</ToastProvider>
|
||||||
</TrailerProvider>
|
</TrailerProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</TraktProvider>
|
</TraktProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { Toast } from 'toastify-react-native';
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import { DeviceEventEmitter } from 'react-native';
|
import { DeviceEventEmitter } from 'react-native';
|
||||||
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Share } from 'react-native';
|
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Share } from 'react-native';
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
|
|
@ -100,6 +100,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
|
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { settings, isLoaded } = useSettings();
|
const { settings, isLoaded } = useSettings();
|
||||||
|
const { showSuccess, showInfo } = useToast();
|
||||||
const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12;
|
const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12;
|
||||||
// Memoize poster width calculation to avoid recalculating on every render
|
// Memoize poster width calculation to avoid recalculating on every render
|
||||||
const posterWidth = React.useMemo(() => {
|
const posterWidth = React.useMemo(() => {
|
||||||
|
|
@ -129,10 +130,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
case 'library':
|
case 'library':
|
||||||
if (inLibrary) {
|
if (inLibrary) {
|
||||||
catalogService.removeFromLibrary(item.type, item.id);
|
catalogService.removeFromLibrary(item.type, item.id);
|
||||||
Toast.info('Removed from Library');
|
showInfo('Removed from Library', 'Removed from your local library');
|
||||||
} else {
|
} else {
|
||||||
catalogService.addToLibrary(item);
|
catalogService.addToLibrary(item);
|
||||||
Toast.success('Added to Library');
|
showSuccess('Added to Library', 'Added to your local library');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'watched': {
|
case 'watched': {
|
||||||
|
|
@ -141,7 +142,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false');
|
await AsyncStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false');
|
||||||
} catch {}
|
} catch {}
|
||||||
Toast.info(targetWatched ? 'Marked as Watched' : 'Marked as Unwatched');
|
showInfo(targetWatched ? 'Marked as Watched' : 'Marked as Unwatched', targetWatched ? 'Item marked as watched' : 'Item marked as unwatched');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
DeviceEventEmitter.emit('watchedStatusChanged');
|
DeviceEventEmitter.emit('watchedStatusChanged');
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
@ -187,10 +188,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
case 'trakt-watchlist': {
|
case 'trakt-watchlist': {
|
||||||
if (isInWatchlist(item.id, item.type as 'movie' | 'show')) {
|
if (isInWatchlist(item.id, item.type as 'movie' | 'show')) {
|
||||||
await removeFromWatchlist(item.id, item.type as 'movie' | 'show');
|
await removeFromWatchlist(item.id, item.type as 'movie' | 'show');
|
||||||
Toast.info('Removed from Trakt Watchlist');
|
showInfo('Removed from Watchlist', 'Removed from your Trakt watchlist');
|
||||||
} else {
|
} else {
|
||||||
await addToWatchlist(item.id, item.type as 'movie' | 'show');
|
await addToWatchlist(item.id, item.type as 'movie' | 'show');
|
||||||
Toast.success('Added to Trakt Watchlist');
|
showSuccess('Added to Watchlist', 'Added to your Trakt watchlist');
|
||||||
}
|
}
|
||||||
setMenuVisible(false);
|
setMenuVisible(false);
|
||||||
break;
|
break;
|
||||||
|
|
@ -198,16 +199,16 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
case 'trakt-collection': {
|
case 'trakt-collection': {
|
||||||
if (isInCollection(item.id, item.type as 'movie' | 'show')) {
|
if (isInCollection(item.id, item.type as 'movie' | 'show')) {
|
||||||
await removeFromCollection(item.id, item.type as 'movie' | 'show');
|
await removeFromCollection(item.id, item.type as 'movie' | 'show');
|
||||||
Toast.info('Removed from Trakt Collection');
|
showInfo('Removed from Collection', 'Removed from your Trakt collection');
|
||||||
} else {
|
} else {
|
||||||
await addToCollection(item.id, item.type as 'movie' | 'show');
|
await addToCollection(item.id, item.type as 'movie' | 'show');
|
||||||
Toast.success('Added to Trakt Collection');
|
showSuccess('Added to Collection', 'Added to your Trakt collection');
|
||||||
}
|
}
|
||||||
setMenuVisible(false);
|
setMenuVisible(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [item, inLibrary, isWatched, isInWatchlist, isInCollection, addToWatchlist, removeFromWatchlist, addToCollection, removeFromCollection]);
|
}, [item, inLibrary, isWatched, isInWatchlist, isInCollection, addToWatchlist, removeFromWatchlist, addToCollection, removeFromCollection, showSuccess, showInfo]);
|
||||||
|
|
||||||
const handleMenuClose = useCallback(() => {
|
const handleMenuClose = useCallback(() => {
|
||||||
setMenuVisible(false);
|
setMenuVisible(false);
|
||||||
|
|
@ -309,12 +310,12 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{isAuthenticated && isInWatchlist(item.id, item.type as 'movie' | 'show') && (
|
{isAuthenticated && isInWatchlist(item.id, item.type as 'movie' | 'show') && (
|
||||||
<View style={styles.traktWatchlistBadge}>
|
<View style={styles.traktWatchlistIcon}>
|
||||||
<MaterialIcons name="playlist-add-check" size={16} color="#E74C3C" />
|
<MaterialIcons name="playlist-add-check" size={16} color="#E74C3C" />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{isAuthenticated && isInCollection(item.id, item.type as 'movie' | 'show') && (
|
{isAuthenticated && isInCollection(item.id, item.type as 'movie' | 'show') && (
|
||||||
<View style={styles.traktCollectionBadge}>
|
<View style={styles.traktCollectionIcon}>
|
||||||
<MaterialIcons name="video-library" size={16} color="#3498DB" />
|
<MaterialIcons name="video-library" size={16} color="#3498DB" />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -395,21 +396,17 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 4,
|
padding: 4,
|
||||||
},
|
},
|
||||||
traktWatchlistBadge: {
|
traktWatchlistIcon: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
left: 8,
|
right: 8,
|
||||||
backgroundColor: 'rgba(231, 76, 60, 0.9)',
|
padding: 2,
|
||||||
borderRadius: 8,
|
|
||||||
padding: 4,
|
|
||||||
},
|
},
|
||||||
traktCollectionBadge: {
|
traktCollectionIcon: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
left: 8,
|
right: 32, // Positioned to the left of watchlist icon
|
||||||
backgroundColor: 'rgba(52, 152, 219, 0.9)',
|
padding: 2,
|
||||||
borderRadius: 8,
|
|
||||||
padding: 4,
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import Animated, {
|
||||||
SharedValue,
|
SharedValue,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import { useTraktContext } from '../../contexts/TraktContext';
|
import { useTraktContext } from '../../contexts/TraktContext';
|
||||||
import { useSettings } from '../../hooks/useSettings';
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
import { useTrailer } from '../../contexts/TrailerContext';
|
import { useTrailer } from '../../contexts/TrailerContext';
|
||||||
|
|
@ -150,6 +151,7 @@ const ActionButtons = memo(({
|
||||||
onToggleCollection?: () => void;
|
onToggleCollection?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
const { showSaved, showTraktSaved, showRemoved, showTraktRemoved, showSuccess, showInfo } = useToast();
|
||||||
|
|
||||||
// Performance optimization: Cache theme colors
|
// Performance optimization: Cache theme colors
|
||||||
const themeColors = useMemo(() => ({
|
const themeColors = useMemo(() => ({
|
||||||
|
|
@ -196,6 +198,51 @@ const ActionButtons = memo(({
|
||||||
}
|
}
|
||||||
}, [id, navigation, settings.enrichMetadataWithTMDB]);
|
}, [id, navigation, settings.enrichMetadataWithTMDB]);
|
||||||
|
|
||||||
|
// Enhanced save handler that combines local library + Trakt watchlist
|
||||||
|
const handleSaveAction = useCallback(async () => {
|
||||||
|
const wasInLibrary = inLibrary;
|
||||||
|
|
||||||
|
// Always toggle local library first
|
||||||
|
toggleLibrary();
|
||||||
|
|
||||||
|
// If authenticated, also toggle Trakt watchlist
|
||||||
|
if (isAuthenticated && onToggleWatchlist) {
|
||||||
|
await onToggleWatchlist();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show appropriate toast
|
||||||
|
if (isAuthenticated) {
|
||||||
|
if (wasInLibrary) {
|
||||||
|
showTraktRemoved();
|
||||||
|
} else {
|
||||||
|
showTraktSaved();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (wasInLibrary) {
|
||||||
|
showRemoved();
|
||||||
|
} else {
|
||||||
|
showSaved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [toggleLibrary, isAuthenticated, onToggleWatchlist, inLibrary, showSaved, showTraktSaved, showRemoved, showTraktRemoved]);
|
||||||
|
|
||||||
|
// Enhanced collection handler with toast notifications
|
||||||
|
const handleCollectionAction = useCallback(async () => {
|
||||||
|
const wasInCollection = isInCollection;
|
||||||
|
|
||||||
|
// Toggle collection
|
||||||
|
if (onToggleCollection) {
|
||||||
|
await onToggleCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show appropriate toast
|
||||||
|
if (wasInCollection) {
|
||||||
|
showInfo('Removed from Collection', 'Removed from your Trakt collection');
|
||||||
|
} else {
|
||||||
|
showSuccess('Added to Collection', 'Added to your Trakt collection');
|
||||||
|
}
|
||||||
|
}, [onToggleCollection, isInCollection, showSuccess, showInfo]);
|
||||||
|
|
||||||
// Optimized play button style calculation
|
// Optimized play button style calculation
|
||||||
const playButtonStyle = useMemo(() => {
|
const playButtonStyle = useMemo(() => {
|
||||||
if (isWatched && type === 'movie') {
|
if (isWatched && type === 'movie') {
|
||||||
|
|
@ -290,6 +337,8 @@ const ActionButtons = memo(({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
|
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
|
||||||
|
{/* Play Button Row - Only Play button */}
|
||||||
|
<View style={styles.playButtonRow}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[playButtonStyle, isTablet && styles.tabletPlayButton]}
|
style={[playButtonStyle, isTablet && styles.tabletPlayButton]}
|
||||||
onPress={handleShowStreams}
|
onPress={handleShowStreams}
|
||||||
|
|
@ -307,10 +356,14 @@ const ActionButtons = memo(({
|
||||||
/>
|
/>
|
||||||
<Text style={[playButtonTextStyle, isTablet && styles.tabletPlayButtonText]}>{finalPlayButtonText}</Text>
|
<Text style={[playButtonTextStyle, isTablet && styles.tabletPlayButtonText]}>{finalPlayButtonText}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Secondary Action Row - All other buttons */}
|
||||||
|
<View style={styles.secondaryActionRow}>
|
||||||
|
{/* Save Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.actionButton, styles.infoButton, isTablet && styles.tabletInfoButton]}
|
style={[styles.actionButton, styles.infoButton, isTablet && styles.tabletInfoButton]}
|
||||||
onPress={toggleLibrary}
|
onPress={handleSaveAction}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
{Platform.OS === 'ios' ? (
|
{Platform.OS === 'ios' ? (
|
||||||
|
|
@ -328,7 +381,7 @@ const ActionButtons = memo(({
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={inLibrary ? "bookmark" : "bookmark-outline"}
|
name={inLibrary ? "bookmark" : "bookmark-outline"}
|
||||||
size={isTablet ? 28 : 24}
|
size={isTablet ? 28 : 24}
|
||||||
color={currentTheme.colors.white}
|
color={inLibrary ? (isAuthenticated && isInWatchlist ? "#E74C3C" : currentTheme.colors.white) : currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.infoButtonText, isTablet && styles.tabletInfoButtonText]}>
|
<Text style={[styles.infoButtonText, isTablet && styles.tabletInfoButtonText]}>
|
||||||
{inLibrary ? 'Saved' : 'Save'}
|
{inLibrary ? 'Saved' : 'Save'}
|
||||||
|
|
@ -383,36 +436,11 @@ const ActionButtons = memo(({
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Trakt Action Buttons */}
|
{/* Trakt Collection Button */}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.actionButton, styles.traktButton, isTablet && styles.tabletTraktButton]}
|
style={[styles.iconButton, isTablet && styles.tabletIconButton]}
|
||||||
onPress={onToggleWatchlist}
|
onPress={handleCollectionAction}
|
||||||
activeOpacity={0.85}
|
|
||||||
>
|
|
||||||
{Platform.OS === 'ios' ? (
|
|
||||||
GlassViewComp && liquidGlassAvailable ? (
|
|
||||||
<GlassViewComp
|
|
||||||
style={styles.blurBackgroundRound}
|
|
||||||
glassEffectStyle="regular"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ExpoBlurView intensity={80} style={styles.blurBackgroundRound} tint="dark" />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<View style={styles.androidFallbackBlurRound} />
|
|
||||||
)}
|
|
||||||
<MaterialIcons
|
|
||||||
name={isInWatchlist ? "playlist-add-check" : "playlist-add"}
|
|
||||||
size={isTablet ? 28 : 24}
|
|
||||||
color={isInWatchlist ? "#E74C3C" : currentTheme.colors.white}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.actionButton, styles.traktButton, isTablet && styles.tabletTraktButton]}
|
|
||||||
onPress={onToggleCollection}
|
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
{Platform.OS === 'ios' ? (
|
{Platform.OS === 'ios' ? (
|
||||||
|
|
@ -433,9 +461,9 @@ const ActionButtons = memo(({
|
||||||
color={isInCollection ? "#3498DB" : currentTheme.colors.white}
|
color={isInCollection ? "#3498DB" : currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Ratings Button (for series) */}
|
||||||
{type === 'series' && (
|
{type === 'series' && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.iconButton, isTablet && styles.tabletIconButton]}
|
style={[styles.iconButton, isTablet && styles.tabletIconButton]}
|
||||||
|
|
@ -461,6 +489,7 @@ const ActionButtons = memo(({
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -1928,8 +1957,8 @@ const styles = StyleSheet.create({
|
||||||
paddingVertical: 0,
|
paddingVertical: 0,
|
||||||
},
|
},
|
||||||
actionButtons: {
|
actionButtons: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'column',
|
||||||
gap: 8,
|
gap: 12,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -1937,6 +1966,27 @@ const styles = StyleSheet.create({
|
||||||
maxWidth: isTablet ? 600 : '100%',
|
maxWidth: isTablet ? 600 : '100%',
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
|
primaryActionRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
playButtonRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
secondaryActionRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
actionButton: {
|
actionButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
@ -2267,7 +2317,7 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
// Tablet-specific styles
|
// Tablet-specific styles
|
||||||
tabletActionButtons: {
|
tabletActionButtons: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'column',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
|
||||||
284
src/components/ui/Toast.tsx
Normal file
284
src/components/ui/Toast.tsx
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
Animated,
|
||||||
|
Dimensions,
|
||||||
|
TouchableOpacity,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
|
||||||
|
const { width: screenWidth } = Dimensions.get('window');
|
||||||
|
|
||||||
|
export interface ToastConfig {
|
||||||
|
id: string;
|
||||||
|
type: 'success' | 'error' | 'warning' | 'info';
|
||||||
|
title: string;
|
||||||
|
message?: string;
|
||||||
|
duration?: number;
|
||||||
|
position?: 'top' | 'bottom';
|
||||||
|
action?: {
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToastProps extends ToastConfig {
|
||||||
|
onRemove: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Toast: React.FC<ToastProps> = ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
duration = 4000,
|
||||||
|
position = 'top',
|
||||||
|
action,
|
||||||
|
onRemove,
|
||||||
|
}) => {
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
const translateY = useRef(new Animated.Value(position === 'top' ? -100 : 100)).current;
|
||||||
|
const opacity = useRef(new Animated.Value(0)).current;
|
||||||
|
const scale = useRef(new Animated.Value(0.8)).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Animate in
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(translateY, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(opacity, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.spring(scale, {
|
||||||
|
toValue: 1,
|
||||||
|
tension: 100,
|
||||||
|
friction: 8,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start();
|
||||||
|
|
||||||
|
// Auto remove
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
removeToast();
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeToast = () => {
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(translateY, {
|
||||||
|
toValue: position === 'top' ? -100 : 100,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(opacity, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(scale, {
|
||||||
|
toValue: 0.8,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start(() => {
|
||||||
|
onRemove(id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToastConfig = () => {
|
||||||
|
// Use the app's theme colors directly
|
||||||
|
const isDarkTheme = true; // App uses dark theme by default
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'success':
|
||||||
|
return {
|
||||||
|
icon: 'check-circle' as const,
|
||||||
|
color: currentTheme.colors.success,
|
||||||
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
borderColor: currentTheme.colors.success,
|
||||||
|
textColor: currentTheme.colors.highEmphasis,
|
||||||
|
messageColor: currentTheme.colors.mediumEmphasis,
|
||||||
|
};
|
||||||
|
case 'error':
|
||||||
|
return {
|
||||||
|
icon: 'error' as const,
|
||||||
|
color: currentTheme.colors.error,
|
||||||
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
borderColor: currentTheme.colors.error,
|
||||||
|
textColor: currentTheme.colors.highEmphasis,
|
||||||
|
messageColor: currentTheme.colors.mediumEmphasis,
|
||||||
|
};
|
||||||
|
case 'warning':
|
||||||
|
return {
|
||||||
|
icon: 'warning' as const,
|
||||||
|
color: currentTheme.colors.warning,
|
||||||
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
borderColor: currentTheme.colors.warning,
|
||||||
|
textColor: currentTheme.colors.highEmphasis,
|
||||||
|
messageColor: currentTheme.colors.mediumEmphasis,
|
||||||
|
};
|
||||||
|
case 'info':
|
||||||
|
return {
|
||||||
|
icon: 'info' as const,
|
||||||
|
color: currentTheme.colors.info,
|
||||||
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
borderColor: currentTheme.colors.info,
|
||||||
|
textColor: currentTheme.colors.highEmphasis,
|
||||||
|
messageColor: currentTheme.colors.mediumEmphasis,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
icon: 'info' as const,
|
||||||
|
color: currentTheme.colors.mediumEmphasis,
|
||||||
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
borderColor: currentTheme.colors.border,
|
||||||
|
textColor: currentTheme.colors.highEmphasis,
|
||||||
|
messageColor: currentTheme.colors.mediumEmphasis,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = getToastConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.container,
|
||||||
|
{
|
||||||
|
transform: [{ translateY }, { scale }],
|
||||||
|
opacity,
|
||||||
|
backgroundColor: config.backgroundColor,
|
||||||
|
borderColor: config.borderColor,
|
||||||
|
top: position === 'top' ? 60 : undefined,
|
||||||
|
bottom: position === 'bottom' ? 60 : undefined,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.content}
|
||||||
|
onPress={removeToast}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<View style={styles.leftSection}>
|
||||||
|
<View style={[styles.iconContainer, { backgroundColor: config.color }]}>
|
||||||
|
<MaterialIcons name={config.icon} size={20} color="white" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.textContainer}>
|
||||||
|
<Text style={[styles.title, { color: config.textColor }]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
{message && (
|
||||||
|
<Text style={[styles.message, { color: config.messageColor }]}>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.rightSection}>
|
||||||
|
{action && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.actionButton, { backgroundColor: config.color }]}
|
||||||
|
onPress={() => {
|
||||||
|
action.onPress();
|
||||||
|
removeToast();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.actionText}>{action.label}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.closeButton}
|
||||||
|
onPress={removeToast}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="close" size={18} color={currentTheme.colors.disabled} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 4,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 8,
|
||||||
|
zIndex: 1000,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16,
|
||||||
|
minHeight: 60,
|
||||||
|
},
|
||||||
|
leftSection: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
textContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
lineHeight: 20,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 18,
|
||||||
|
fontWeight: '400',
|
||||||
|
},
|
||||||
|
rightSection: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
actionText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Toast;
|
||||||
35
src/components/ui/ToastManager.tsx
Normal file
35
src/components/ui/ToastManager.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { View, StyleSheet } from 'react-native';
|
||||||
|
import Toast, { ToastConfig } from './Toast';
|
||||||
|
|
||||||
|
interface ToastManagerProps {
|
||||||
|
toasts: ToastConfig[];
|
||||||
|
onRemoveToast: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToastManager: React.FC<ToastManagerProps> = ({ toasts, onRemoveToast }) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.container} pointerEvents="box-none">
|
||||||
|
{toasts.map((toast) => (
|
||||||
|
<Toast
|
||||||
|
key={toast.id}
|
||||||
|
{...toast}
|
||||||
|
onRemove={onRemoveToast}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 1000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ToastManager;
|
||||||
71
src/contexts/ToastContext.tsx
Normal file
71
src/contexts/ToastContext.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import ToastManager from '../components/ui/ToastManager';
|
||||||
|
import { ToastConfig } from '../components/ui/Toast';
|
||||||
|
import { toastService } from '../services/toastService';
|
||||||
|
|
||||||
|
interface ToastContextType {
|
||||||
|
showSuccess: (title: string, message?: string, options?: Partial<ToastConfig>) => string;
|
||||||
|
showError: (title: string, message?: string, options?: Partial<ToastConfig>) => string;
|
||||||
|
showWarning: (title: string, message?: string, options?: Partial<ToastConfig>) => string;
|
||||||
|
showInfo: (title: string, message?: string, options?: Partial<ToastConfig>) => string;
|
||||||
|
showCustom: (config: Omit<ToastConfig, 'id'>) => string;
|
||||||
|
removeToast: (id: string) => void;
|
||||||
|
removeAllToasts: () => void;
|
||||||
|
// Convenience methods
|
||||||
|
showSaved: () => string;
|
||||||
|
showRemoved: () => string;
|
||||||
|
showTraktSaved: () => string;
|
||||||
|
showTraktRemoved: () => string;
|
||||||
|
showNetworkError: () => string;
|
||||||
|
showAuthError: () => string;
|
||||||
|
showSyncSuccess: (count: number) => string;
|
||||||
|
showProgressSaved: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useToast = (): ToastContextType => {
|
||||||
|
const context = useContext(ToastContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useToast must be used within a ToastProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ToastProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
||||||
|
const [toasts, setToasts] = useState<ToastConfig[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = toastService.subscribe(setToasts);
|
||||||
|
return unsubscribe;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue: ToastContextType = {
|
||||||
|
showSuccess: toastService.success.bind(toastService),
|
||||||
|
showError: toastService.error.bind(toastService),
|
||||||
|
showWarning: toastService.warning.bind(toastService),
|
||||||
|
showInfo: toastService.info.bind(toastService),
|
||||||
|
showCustom: toastService.custom.bind(toastService),
|
||||||
|
removeToast: toastService.remove.bind(toastService),
|
||||||
|
removeAllToasts: toastService.removeAll.bind(toastService),
|
||||||
|
showSaved: toastService.showSaved.bind(toastService),
|
||||||
|
showRemoved: toastService.showRemoved.bind(toastService),
|
||||||
|
showTraktSaved: toastService.showTraktSaved.bind(toastService),
|
||||||
|
showTraktRemoved: toastService.showTraktRemoved.bind(toastService),
|
||||||
|
showNetworkError: toastService.showNetworkError.bind(toastService),
|
||||||
|
showAuthError: toastService.showAuthError.bind(toastService),
|
||||||
|
showSyncSuccess: toastService.showSyncSuccess.bind(toastService),
|
||||||
|
showProgressSaved: toastService.showProgressSaved.bind(toastService),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
<ToastManager toasts={toasts} onRemoveToast={toastService.remove.bind(toastService)} />
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -206,12 +206,6 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
||||||
season_poster_path: tmdbEpisode.season_poster_path || null
|
season_poster_path: tmdbEpisode.season_poster_path || null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debug log for episodes
|
|
||||||
if (episode.releaseDate) {
|
|
||||||
logger.log(`[CalendarData] Episode with date: ${episode.seriesName} - ${episode.title} (${episode.releaseDate})`);
|
|
||||||
} else {
|
|
||||||
logger.log(`[CalendarData] Episode without date: ${episode.seriesName} - ${episode.title}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return episode;
|
return episode;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) =
|
||||||
// Combined hero animations
|
// Combined hero animations
|
||||||
const heroOpacity = useSharedValue(1);
|
const heroOpacity = useSharedValue(1);
|
||||||
const heroScale = useSharedValue(1); // Start at 1 for Android compatibility
|
const heroScale = useSharedValue(1); // Start at 1 for Android compatibility
|
||||||
const heroHeightValue = useSharedValue(height * 0.5);
|
const heroHeightValue = useSharedValue(height * 0.55);
|
||||||
|
|
||||||
// Combined UI element animations
|
// Combined UI element animations
|
||||||
const uiElementsOpacity = useSharedValue(1);
|
const uiElementsOpacity = useSharedValue(1);
|
||||||
|
|
|
||||||
|
|
@ -1015,7 +1015,6 @@ class StremioService {
|
||||||
|
|
||||||
// Filter episodes to only include those within our date range
|
// Filter episodes to only include those within our date range
|
||||||
// This is done immediately after fetching to reduce memory footprint
|
// This is done immediately after fetching to reduce memory footprint
|
||||||
logger.log(`[StremioService] Filtering ${metadata.videos.length} episodes for ${id}, date range: ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
|
||||||
|
|
||||||
const filteredEpisodes = metadata.videos
|
const filteredEpisodes = metadata.videos
|
||||||
.filter(video => {
|
.filter(video => {
|
||||||
|
|
@ -1025,13 +1024,11 @@ class StremioService {
|
||||||
}
|
}
|
||||||
const releaseDate = new Date(video.released);
|
const releaseDate = new Date(video.released);
|
||||||
const inRange = releaseDate >= startDate && releaseDate <= endDate;
|
const inRange = releaseDate >= startDate && releaseDate <= endDate;
|
||||||
logger.log(`[StremioService] Episode ${video.id}: released=${video.released}, inRange=${inRange}`);
|
|
||||||
return inRange;
|
return inRange;
|
||||||
})
|
})
|
||||||
.sort((a, b) => new Date(a.released).getTime() - new Date(b.released).getTime())
|
.sort((a, b) => new Date(a.released).getTime() - new Date(b.released).getTime())
|
||||||
.slice(0, maxEpisodes); // Limit number of episodes to prevent memory overflow
|
.slice(0, maxEpisodes); // Limit number of episodes to prevent memory overflow
|
||||||
|
|
||||||
logger.log(`[StremioService] After filtering: ${filteredEpisodes.length} episodes remain`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
seriesName: metadata.name,
|
seriesName: metadata.name,
|
||||||
|
|
|
||||||
152
src/services/toastService.ts
Normal file
152
src/services/toastService.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { ToastConfig } from '../components/ui/Toast';
|
||||||
|
|
||||||
|
class ToastService {
|
||||||
|
private static instance: ToastService;
|
||||||
|
private toasts: ToastConfig[] = [];
|
||||||
|
private listeners: Array<(toasts: ToastConfig[]) => void> = [];
|
||||||
|
private idCounter = 0;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): ToastService {
|
||||||
|
if (!ToastService.instance) {
|
||||||
|
ToastService.instance = new ToastService();
|
||||||
|
}
|
||||||
|
return ToastService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateId(): string {
|
||||||
|
return `toast_${++this.idCounter}_${Date.now()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyListeners(): void {
|
||||||
|
this.listeners.forEach(listener => listener([...this.toasts]));
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: (toasts: ToastConfig[]) => void): () => void {
|
||||||
|
this.listeners.push(listener);
|
||||||
|
// Immediately call with current toasts
|
||||||
|
listener([...this.toasts]);
|
||||||
|
|
||||||
|
// Return unsubscribe function
|
||||||
|
return () => {
|
||||||
|
const index = this.listeners.indexOf(listener);
|
||||||
|
if (index > -1) {
|
||||||
|
this.listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private addToast(config: Omit<ToastConfig, 'id'>): string {
|
||||||
|
const id = this.generateId();
|
||||||
|
const toast: ToastConfig = {
|
||||||
|
id,
|
||||||
|
duration: 4000,
|
||||||
|
position: 'top',
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.toasts.push(toast);
|
||||||
|
this.notifyListeners();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
success(title: string, message?: string, options?: Partial<ToastConfig>): string {
|
||||||
|
return this.addToast({
|
||||||
|
type: 'success',
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
error(title: string, message?: string, options?: Partial<ToastConfig>): string {
|
||||||
|
return this.addToast({
|
||||||
|
type: 'error',
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
duration: 6000, // Longer duration for errors
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(title: string, message?: string, options?: Partial<ToastConfig>): string {
|
||||||
|
return this.addToast({
|
||||||
|
type: 'warning',
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
info(title: string, message?: string, options?: Partial<ToastConfig>): string {
|
||||||
|
return this.addToast({
|
||||||
|
type: 'info',
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
custom(config: Omit<ToastConfig, 'id'>): string {
|
||||||
|
return this.addToast(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(id: string): void {
|
||||||
|
this.toasts = this.toasts.filter(toast => toast.id !== id);
|
||||||
|
this.notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAll(): void {
|
||||||
|
this.toasts = [];
|
||||||
|
this.notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods for common use cases
|
||||||
|
showSaved(): string {
|
||||||
|
return this.success('Saved', 'Added to your library');
|
||||||
|
}
|
||||||
|
|
||||||
|
showRemoved(): string {
|
||||||
|
return this.info('Removed', 'Removed from your library');
|
||||||
|
}
|
||||||
|
|
||||||
|
showTraktSaved(): string {
|
||||||
|
return this.success('Saved to Trakt', 'Added to watchlist and library');
|
||||||
|
}
|
||||||
|
|
||||||
|
showTraktRemoved(): string {
|
||||||
|
return this.info('Removed from Trakt', 'Removed from watchlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
showNetworkError(): string {
|
||||||
|
return this.error(
|
||||||
|
'Network Error',
|
||||||
|
'Please check your internet connection',
|
||||||
|
{ duration: 8000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAuthError(): string {
|
||||||
|
return this.error(
|
||||||
|
'Authentication Error',
|
||||||
|
'Please log in to Trakt again',
|
||||||
|
{ duration: 8000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showSyncSuccess(count: number): string {
|
||||||
|
return this.success(
|
||||||
|
'Sync Complete',
|
||||||
|
`Synced ${count} items to Trakt`,
|
||||||
|
{ duration: 3000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showProgressSaved(): string {
|
||||||
|
return this.success('Progress Saved', 'Your watch progress has been synced');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toastService = ToastService.getInstance();
|
||||||
|
export default toastService;
|
||||||
|
|
@ -1212,10 +1212,10 @@ export class TraktService {
|
||||||
|
|
||||||
// Try multiple search approaches
|
// Try multiple search approaches
|
||||||
const searchUrls = [
|
const searchUrls = [
|
||||||
`${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?id_type=imdb&id=${cleanImdbId}`,
|
`${TRAKT_API_URL}/search/${type === 'show' ? 'show' : type}?id_type=imdb&id=${cleanImdbId}`,
|
||||||
`${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?query=${encodeURIComponent(cleanImdbId)}&id_type=imdb`,
|
`${TRAKT_API_URL}/search/${type === 'show' ? 'show' : type}?query=${encodeURIComponent(cleanImdbId)}&id_type=imdb`,
|
||||||
// Also try with the full tt-prefixed ID in case the API accepts it
|
// Also try with the full tt-prefixed ID in case the API accepts it
|
||||||
`${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?id_type=imdb&id=tt${cleanImdbId}`
|
`${TRAKT_API_URL}/search/${type === 'show' ? 'show' : type}?id_type=imdb&id=tt${cleanImdbId}`
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const searchUrl of searchUrls) {
|
for (const searchUrl of searchUrls) {
|
||||||
|
|
@ -2339,7 +2339,7 @@ export class TraktService {
|
||||||
try {
|
try {
|
||||||
logger.log(`[TraktService] Searching Trakt for ${type} with TMDB ID: ${tmdbId}`);
|
logger.log(`[TraktService] Searching Trakt for ${type} with TMDB ID: ${tmdbId}`);
|
||||||
|
|
||||||
const response = await fetch(`${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?id_type=tmdb&id=${tmdbId}`, {
|
const response = await fetch(`${TRAKT_API_URL}/search/${type === 'show' ? 'show' : type}?id_type=tmdb&id=${tmdbId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'trakt-api-version': '2',
|
'trakt-api-version': '2',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue