trakt wathclist integration test
This commit is contained in:
parent
08f356cfa4
commit
a7f850d577
7 changed files with 510 additions and 9 deletions
|
|
@ -11,6 +11,7 @@ import { DropUpMenu } from './DropUpMenu';
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { storageService } from '../../services/storageService';
|
||||
import { TraktService } from '../../services/traktService';
|
||||
import { useTraktContext } from '../../contexts/TraktContext';
|
||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||
|
||||
interface ContentItemProps {
|
||||
|
|
@ -89,6 +90,9 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
const [isWatched, setIsWatched] = useState(false);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
// Trakt integration
|
||||
const { isAuthenticated, isInWatchlist, isInCollection, addToWatchlist, removeFromWatchlist, addToCollection, removeFromCollection } = useTraktContext();
|
||||
|
||||
useEffect(() => {
|
||||
// Reset image error state when item changes, allowing for retry on re-render
|
||||
setImageError(false);
|
||||
|
|
@ -180,8 +184,30 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
Share.share({ message, url, title: item.name });
|
||||
break;
|
||||
}
|
||||
case 'trakt-watchlist': {
|
||||
if (isInWatchlist(item.id, item.type as 'movie' | 'show')) {
|
||||
await removeFromWatchlist(item.id, item.type as 'movie' | 'show');
|
||||
Toast.info('Removed from Trakt Watchlist');
|
||||
} else {
|
||||
await addToWatchlist(item.id, item.type as 'movie' | 'show');
|
||||
Toast.success('Added to Trakt Watchlist');
|
||||
}
|
||||
setMenuVisible(false);
|
||||
break;
|
||||
}
|
||||
case 'trakt-collection': {
|
||||
if (isInCollection(item.id, item.type as 'movie' | 'show')) {
|
||||
await removeFromCollection(item.id, item.type as 'movie' | 'show');
|
||||
Toast.info('Removed from Trakt Collection');
|
||||
} else {
|
||||
await addToCollection(item.id, item.type as 'movie' | 'show');
|
||||
Toast.success('Added to Trakt Collection');
|
||||
}
|
||||
setMenuVisible(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [item, inLibrary, isWatched]);
|
||||
}, [item, inLibrary, isWatched, isInWatchlist, isInCollection, addToWatchlist, removeFromWatchlist, addToCollection, removeFromCollection]);
|
||||
|
||||
const handleMenuClose = useCallback(() => {
|
||||
setMenuVisible(false);
|
||||
|
|
@ -282,6 +308,16 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
<Feather name="bookmark" size={16} color={currentTheme.colors.white} />
|
||||
</View>
|
||||
)}
|
||||
{isAuthenticated && isInWatchlist(item.id, item.type as 'movie' | 'show') && (
|
||||
<View style={styles.traktWatchlistBadge}>
|
||||
<MaterialIcons name="playlist-add-check" size={16} color="#E74C3C" />
|
||||
</View>
|
||||
)}
|
||||
{isAuthenticated && isInCollection(item.id, item.type as 'movie' | 'show') && (
|
||||
<View style={styles.traktCollectionBadge}>
|
||||
<MaterialIcons name="video-library" size={16} color="#3498DB" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{settings.showPosterTitles && (
|
||||
|
|
@ -359,6 +395,22 @@ const styles = StyleSheet.create({
|
|||
borderRadius: 8,
|
||||
padding: 4,
|
||||
},
|
||||
traktWatchlistBadge: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
backgroundColor: 'rgba(231, 76, 60, 0.9)',
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
},
|
||||
traktCollectionBadge: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
backgroundColor: 'rgba(52, 152, 219, 0.9)',
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
},
|
||||
title: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useTraktContext } from '../../contexts/TraktContext';
|
||||
import { colors } from '../../styles/colors';
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
|
|
@ -43,6 +44,9 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
const isDarkMode = useColorScheme() === 'dark';
|
||||
const SNAP_THRESHOLD = 100;
|
||||
|
||||
// Trakt integration
|
||||
const { isAuthenticated, isInWatchlist, isInCollection } = useTraktContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
opacity.value = withTiming(1, { duration: 200 });
|
||||
|
|
@ -92,6 +96,9 @@ 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 inTraktWatchlist = isAuthenticated && isInWatchlist(item.id, item.type);
|
||||
const inTraktCollection = isAuthenticated && isInCollection(item.id, item.type);
|
||||
|
||||
let menuOptions = [
|
||||
{
|
||||
icon: 'bookmark',
|
||||
|
|
@ -117,6 +124,22 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
}
|
||||
];
|
||||
|
||||
// Add Trakt options if authenticated
|
||||
if (isAuthenticated) {
|
||||
menuOptions.push(
|
||||
{
|
||||
icon: 'playlist-add-check',
|
||||
label: inTraktWatchlist ? 'Remove from Trakt Watchlist' : 'Add to Trakt Watchlist',
|
||||
action: 'trakt-watchlist'
|
||||
},
|
||||
{
|
||||
icon: 'video-library',
|
||||
label: inTraktCollection ? 'Remove from Trakt Collection' : 'Add to Trakt Collection',
|
||||
action: 'trakt-collection'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// If used in LibraryScreen, only show 'Remove from Library' if item is in library
|
||||
if (isSavedProp === true) {
|
||||
menuOptions = menuOptions.filter(opt => opt.action !== 'library' || isSaved);
|
||||
|
|
|
|||
|
|
@ -94,6 +94,12 @@ interface HeroSectionProps {
|
|||
getPlayButtonText: () => string;
|
||||
setBannerImage: (bannerImage: string | null) => void;
|
||||
groupedEpisodes?: { [seasonNumber: number]: any[] };
|
||||
// Trakt integration props
|
||||
isAuthenticated?: boolean;
|
||||
isInWatchlist?: boolean;
|
||||
isInCollection?: boolean;
|
||||
onToggleWatchlist?: () => void;
|
||||
onToggleCollection?: () => void;
|
||||
dynamicBackgroundColor?: string;
|
||||
handleBack: () => void;
|
||||
tmdbId?: number | null;
|
||||
|
|
@ -114,7 +120,13 @@ const ActionButtons = memo(({
|
|||
groupedEpisodes,
|
||||
metadata,
|
||||
aiChatEnabled,
|
||||
settings
|
||||
settings,
|
||||
// Trakt integration props
|
||||
isAuthenticated,
|
||||
isInWatchlist,
|
||||
isInCollection,
|
||||
onToggleWatchlist,
|
||||
onToggleCollection
|
||||
}: {
|
||||
handleShowStreams: () => void;
|
||||
toggleLibrary: () => void;
|
||||
|
|
@ -130,6 +142,12 @@ const ActionButtons = memo(({
|
|||
metadata: any;
|
||||
aiChatEnabled?: boolean;
|
||||
settings: any;
|
||||
// Trakt integration props
|
||||
isAuthenticated?: boolean;
|
||||
isInWatchlist?: boolean;
|
||||
isInCollection?: boolean;
|
||||
onToggleWatchlist?: () => void;
|
||||
onToggleCollection?: () => void;
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
|
@ -365,6 +383,59 @@ const ActionButtons = memo(({
|
|||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Trakt Action Buttons */}
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.traktButton, isTablet && styles.tabletTraktButton]}
|
||||
onPress={onToggleWatchlist}
|
||||
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}
|
||||
>
|
||||
{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={isInCollection ? "video-library" : "video-library"}
|
||||
size={isTablet ? 28 : 24}
|
||||
color={isInCollection ? "#3498DB" : currentTheme.colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === 'series' && (
|
||||
<TouchableOpacity
|
||||
style={[styles.iconButton, isTablet && styles.tabletIconButton]}
|
||||
|
|
@ -792,6 +863,12 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
dynamicBackgroundColor,
|
||||
handleBack,
|
||||
tmdbId,
|
||||
// Trakt integration props
|
||||
isAuthenticated,
|
||||
isInWatchlist,
|
||||
isInCollection,
|
||||
onToggleWatchlist,
|
||||
onToggleCollection
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const { isAuthenticated: isTraktAuthenticated } = useTraktContext();
|
||||
|
|
@ -1700,6 +1777,12 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
metadata={metadata}
|
||||
aiChatEnabled={settings?.aiChatEnabled}
|
||||
settings={settings}
|
||||
// Trakt integration props
|
||||
isAuthenticated={isAuthenticated}
|
||||
isInWatchlist={isInWatchlist}
|
||||
isInCollection={isInCollection}
|
||||
onToggleWatchlist={onToggleWatchlist}
|
||||
onToggleCollection={onToggleCollection}
|
||||
/>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
|
|
@ -1886,6 +1969,16 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
traktButton: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
borderWidth: 1.5,
|
||||
borderColor: 'rgba(255,255,255,0.7)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
playButtonText: {
|
||||
color: '#000',
|
||||
fontWeight: '700',
|
||||
|
|
@ -2210,6 +2303,11 @@ const styles = StyleSheet.create({
|
|||
height: 60,
|
||||
borderRadius: 30,
|
||||
},
|
||||
tabletTraktButton: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
},
|
||||
tabletHeroTitle: {
|
||||
fontSize: 36,
|
||||
fontWeight: '900',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,13 @@ interface TraktContextProps {
|
|||
markMovieAsWatched: (imdbId: string, watchedAt?: Date) => Promise<boolean>;
|
||||
markEpisodeAsWatched: (imdbId: string, season: number, episode: number, watchedAt?: Date) => Promise<boolean>;
|
||||
forceSyncTraktProgress?: () => Promise<boolean>;
|
||||
// Trakt content management
|
||||
addToWatchlist: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
|
||||
removeFromWatchlist: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
|
||||
addToCollection: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
|
||||
removeFromCollection: (imdbId: string, type: 'movie' | 'show') => Promise<boolean>;
|
||||
isInWatchlist: (imdbId: string, type: 'movie' | 'show') => boolean;
|
||||
isInCollection: (imdbId: string, type: 'movie' | 'show') => boolean;
|
||||
}
|
||||
|
||||
const TraktContext = createContext<TraktContextProps | undefined>(undefined);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ export function useTraktIntegration() {
|
|||
const [continueWatching, setContinueWatching] = useState<TraktPlaybackItem[]>([]);
|
||||
const [ratedContent, setRatedContent] = useState<TraktRatingItem[]>([]);
|
||||
const [lastAuthCheck, setLastAuthCheck] = useState<number>(Date.now());
|
||||
|
||||
// State for real-time status tracking
|
||||
const [watchlistItems, setWatchlistItems] = useState<Set<string>>(new Set());
|
||||
const [collectionItems, setCollectionItems] = useState<Set<string>>(new Set());
|
||||
|
||||
// Check authentication status
|
||||
const checkAuthStatus = useCallback(async () => {
|
||||
|
|
@ -108,6 +112,39 @@ export function useTraktIntegration() {
|
|||
setCollectionShows(collectionShows);
|
||||
setContinueWatching(continueWatching);
|
||||
setRatedContent(ratings);
|
||||
|
||||
// Populate watchlist and collection sets for quick lookups
|
||||
const newWatchlistItems = new Set<string>();
|
||||
const newCollectionItems = new Set<string>();
|
||||
|
||||
// Add movies to sets
|
||||
watchlistMovies.forEach(item => {
|
||||
if (item.movie?.ids?.imdb) {
|
||||
newWatchlistItems.add(`movie:${item.movie.ids.imdb}`);
|
||||
}
|
||||
});
|
||||
|
||||
collectionMovies.forEach(item => {
|
||||
if (item.movie?.ids?.imdb) {
|
||||
newCollectionItems.add(`movie:${item.movie.ids.imdb}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Add shows to sets
|
||||
watchlistShows.forEach(item => {
|
||||
if (item.show?.ids?.imdb) {
|
||||
newWatchlistItems.add(`show:${item.show.ids.imdb}`);
|
||||
}
|
||||
});
|
||||
|
||||
collectionShows.forEach(item => {
|
||||
if (item.show?.ids?.imdb) {
|
||||
newCollectionItems.add(`show:${item.show.ids.imdb}`);
|
||||
}
|
||||
});
|
||||
|
||||
setWatchlistItems(newWatchlistItems);
|
||||
setCollectionItems(newCollectionItems);
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error loading all collections:', error);
|
||||
} finally {
|
||||
|
|
@ -163,6 +200,105 @@ export function useTraktIntegration() {
|
|||
}
|
||||
}, [isAuthenticated, loadWatchedItems]);
|
||||
|
||||
// Add content to Trakt watchlist
|
||||
const addToWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
const success = await traktService.addToWatchlist(imdbId, type);
|
||||
if (success) {
|
||||
// Ensure consistent IMDb ID format (with 'tt' prefix)
|
||||
const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
setWatchlistItems(prev => new Set(prev).add(`${type}:${normalizedImdbId}`));
|
||||
// Don't refresh immediately - let the local state handle the UI update
|
||||
// The data will be refreshed on next app focus or manual refresh
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error adding to watchlist:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Remove content from Trakt watchlist
|
||||
const removeFromWatchlist = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
const success = await traktService.removeFromWatchlist(imdbId, type);
|
||||
if (success) {
|
||||
// Ensure consistent IMDb ID format (with 'tt' prefix)
|
||||
const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
setWatchlistItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(`${type}:${normalizedImdbId}`);
|
||||
return newSet;
|
||||
});
|
||||
// Don't refresh immediately - let the local state handle the UI update
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error removing from watchlist:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Add content to Trakt collection
|
||||
const addToCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
const success = await traktService.addToCollection(imdbId, type);
|
||||
if (success) {
|
||||
// Ensure consistent IMDb ID format (with 'tt' prefix)
|
||||
const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
setCollectionItems(prev => new Set(prev).add(`${type}:${normalizedImdbId}`));
|
||||
// Don't refresh immediately - let the local state handle the UI update
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error adding to collection:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Remove content from Trakt collection
|
||||
const removeFromCollection = useCallback(async (imdbId: string, type: 'movie' | 'show'): Promise<boolean> => {
|
||||
if (!isAuthenticated) return false;
|
||||
|
||||
try {
|
||||
const success = await traktService.removeFromCollection(imdbId, type);
|
||||
if (success) {
|
||||
// Ensure consistent IMDb ID format (with 'tt' prefix)
|
||||
const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
setCollectionItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(`${type}:${normalizedImdbId}`);
|
||||
return newSet;
|
||||
});
|
||||
// Don't refresh immediately - let the local state handle the UI update
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error('[useTraktIntegration] Error removing from collection:', error);
|
||||
return false;
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Check if content is in Trakt watchlist
|
||||
const isInWatchlist = useCallback((imdbId: string, type: 'movie' | 'show'): boolean => {
|
||||
// Ensure consistent IMDb ID format (with 'tt' prefix)
|
||||
const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
return watchlistItems.has(`${type}:${normalizedImdbId}`);
|
||||
}, [watchlistItems]);
|
||||
|
||||
// Check if content is in Trakt collection
|
||||
const isInCollection = useCallback((imdbId: string, type: 'movie' | 'show'): boolean => {
|
||||
// Ensure consistent IMDb ID format (with 'tt' prefix)
|
||||
const normalizedImdbId = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
return collectionItems.has(`${type}:${normalizedImdbId}`);
|
||||
}, [collectionItems]);
|
||||
|
||||
// Mark an episode as watched
|
||||
const markEpisodeAsWatched = useCallback(async (
|
||||
imdbId: string,
|
||||
|
|
@ -530,6 +666,13 @@ export function useTraktIntegration() {
|
|||
getTraktPlaybackProgress,
|
||||
syncAllProgress,
|
||||
fetchAndMergeTraktProgress,
|
||||
forceSyncTraktProgress // For manual testing
|
||||
forceSyncTraktProgress, // For manual testing
|
||||
// Trakt content management
|
||||
addToWatchlist,
|
||||
removeFromWatchlist,
|
||||
addToCollection,
|
||||
removeFromCollection,
|
||||
isInWatchlist,
|
||||
isInCollection
|
||||
};
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/nativ
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useTraktContext } from '../contexts/TraktContext';
|
||||
import { useMetadata } from '../hooks/useMetadata';
|
||||
import { useDominantColor, preloadDominantColor } from '../hooks/useDominantColor';
|
||||
import { CastSection } from '../components/metadata/CastSection';
|
||||
|
|
@ -86,6 +87,9 @@ const MetadataScreen: React.FC = () => {
|
|||
const { top: safeAreaTop } = useSafeAreaInsets();
|
||||
const { pauseTrailer } = useTrailer();
|
||||
|
||||
// Trakt integration
|
||||
const { isAuthenticated, isInWatchlist, isInCollection, addToWatchlist, removeFromWatchlist, addToCollection, removeFromCollection } = useTraktContext();
|
||||
|
||||
// Optimized state management - reduced state variables
|
||||
const [isContentReady, setIsContentReady] = useState(false);
|
||||
const [showCastModal, setShowCastModal] = useState(false);
|
||||
|
|
@ -923,6 +927,24 @@ const MetadataScreen: React.FC = () => {
|
|||
getPlayButtonText={watchProgressData.getPlayButtonText}
|
||||
setBannerImage={assetData.setBannerImage}
|
||||
groupedEpisodes={groupedEpisodes}
|
||||
// Trakt integration props
|
||||
isAuthenticated={isAuthenticated}
|
||||
isInWatchlist={isInWatchlist(id, type as 'movie' | 'show')}
|
||||
isInCollection={isInCollection(id, type as 'movie' | 'show')}
|
||||
onToggleWatchlist={async () => {
|
||||
if (isInWatchlist(id, type as 'movie' | 'show')) {
|
||||
await removeFromWatchlist(id, type as 'movie' | 'show');
|
||||
} else {
|
||||
await addToWatchlist(id, type as 'movie' | 'show');
|
||||
}
|
||||
}}
|
||||
onToggleCollection={async () => {
|
||||
if (isInCollection(id, type as 'movie' | 'show')) {
|
||||
await removeFromCollection(id, type as 'movie' | 'show');
|
||||
} else {
|
||||
await addToCollection(id, type as 'movie' | 'show');
|
||||
}
|
||||
}}
|
||||
dynamicBackgroundColor={dynamicBackgroundColor}
|
||||
handleBack={handleBack}
|
||||
tmdbId={tmdbId}
|
||||
|
|
|
|||
|
|
@ -1212,10 +1212,10 @@ export class TraktService {
|
|||
|
||||
// Try multiple search approaches
|
||||
const searchUrls = [
|
||||
`${TRAKT_API_URL}/search/${type}?id_type=imdb&id=${cleanImdbId}`,
|
||||
`${TRAKT_API_URL}/search/${type}?query=${encodeURIComponent(cleanImdbId)}&id_type=imdb`,
|
||||
`${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?id_type=imdb&id=${cleanImdbId}`,
|
||||
`${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?query=${encodeURIComponent(cleanImdbId)}&id_type=imdb`,
|
||||
// Also try with the full tt-prefixed ID in case the API accepts it
|
||||
`${TRAKT_API_URL}/search/${type}?id_type=imdb&id=tt${cleanImdbId}`
|
||||
`${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?id_type=imdb&id=tt${cleanImdbId}`
|
||||
];
|
||||
|
||||
for (const searchUrl of searchUrls) {
|
||||
|
|
@ -1240,7 +1240,7 @@ export class TraktService {
|
|||
logger.log(`[TraktService] Search response data:`, data);
|
||||
|
||||
if (data && data.length > 0) {
|
||||
const traktId = data[0][type]?.ids?.trakt;
|
||||
const traktId = data[0][type === 'show' ? 'show' : type]?.ids?.trakt;
|
||||
if (traktId) {
|
||||
logger.log(`[TraktService] Found Trakt ID: ${traktId} for IMDb ID: ${cleanImdbId}`);
|
||||
return traktId;
|
||||
|
|
@ -2339,7 +2339,7 @@ export class TraktService {
|
|||
try {
|
||||
logger.log(`[TraktService] Searching Trakt for ${type} with TMDB ID: ${tmdbId}`);
|
||||
|
||||
const response = await fetch(`${TRAKT_API_URL}/search/${type}?id_type=tmdb&id=${tmdbId}`, {
|
||||
const response = await fetch(`${TRAKT_API_URL}/search/${type === 'show' ? 'shows' : type}?id_type=tmdb&id=${tmdbId}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'trakt-api-version': '2',
|
||||
|
|
@ -2356,7 +2356,7 @@ export class TraktService {
|
|||
const data = await response.json();
|
||||
logger.log(`[TraktService] TMDB search response:`, data);
|
||||
if (data && data.length > 0) {
|
||||
const traktId = data[0][type]?.ids?.trakt;
|
||||
const traktId = data[0][type === 'show' ? 'show' : type]?.ids?.trakt;
|
||||
if (traktId) {
|
||||
logger.log(`[TraktService] Found Trakt ID via TMDB: ${traktId} for TMDB ID: ${tmdbId}`);
|
||||
return traktId;
|
||||
|
|
@ -2463,6 +2463,162 @@ export class TraktService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add content to Trakt watchlist
|
||||
*/
|
||||
public async addToWatchlist(imdbId: string, type: 'movie' | 'show'): Promise<boolean> {
|
||||
try {
|
||||
if (!await this.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const payload = type === 'movie'
|
||||
? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] }
|
||||
: { shows: [{ ids: { imdb: imdbIdWithPrefix } }] };
|
||||
|
||||
await this.apiRequest('/sync/watchlist', 'POST', payload);
|
||||
logger.log(`[TraktService] Added ${type} to watchlist: ${imdbId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`[TraktService] Failed to add ${type} to watchlist:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove content from Trakt watchlist
|
||||
*/
|
||||
public async removeFromWatchlist(imdbId: string, type: 'movie' | 'show'): Promise<boolean> {
|
||||
try {
|
||||
if (!await this.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const payload = type === 'movie'
|
||||
? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] }
|
||||
: { shows: [{ ids: { imdb: imdbIdWithPrefix } }] };
|
||||
|
||||
await this.apiRequest('/sync/watchlist/remove', 'POST', payload);
|
||||
logger.log(`[TraktService] Removed ${type} from watchlist: ${imdbId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`[TraktService] Failed to remove ${type} from watchlist:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add content to Trakt collection
|
||||
*/
|
||||
public async addToCollection(imdbId: string, type: 'movie' | 'show'): Promise<boolean> {
|
||||
try {
|
||||
if (!await this.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const payload = type === 'movie'
|
||||
? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] }
|
||||
: { shows: [{ ids: { imdb: imdbIdWithPrefix } }] };
|
||||
|
||||
await this.apiRequest('/sync/collection', 'POST', payload);
|
||||
logger.log(`[TraktService] Added ${type} to collection: ${imdbId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`[TraktService] Failed to add ${type} to collection:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove content from Trakt collection
|
||||
*/
|
||||
public async removeFromCollection(imdbId: string, type: 'movie' | 'show'): Promise<boolean> {
|
||||
try {
|
||||
if (!await this.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const payload = type === 'movie'
|
||||
? { movies: [{ ids: { imdb: imdbIdWithPrefix } }] }
|
||||
: { shows: [{ ids: { imdb: imdbIdWithPrefix } }] };
|
||||
|
||||
await this.apiRequest('/sync/collection/remove', 'POST', payload);
|
||||
logger.log(`[TraktService] Removed ${type} from collection: ${imdbId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`[TraktService] Failed to remove ${type} from collection:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content is in Trakt watchlist
|
||||
*/
|
||||
public async isInWatchlist(imdbId: string, type: 'movie' | 'show'): Promise<boolean> {
|
||||
try {
|
||||
if (!await this.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const watchlistItems = type === 'movie'
|
||||
? await this.getWatchlistMovies()
|
||||
: await this.getWatchlistShows();
|
||||
|
||||
return watchlistItems.some(item => {
|
||||
const itemImdbId = type === 'movie'
|
||||
? item.movie?.ids?.imdb
|
||||
: item.show?.ids?.imdb;
|
||||
return itemImdbId === imdbIdWithPrefix;
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[TraktService] Failed to check if ${type} is in watchlist:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content is in Trakt collection
|
||||
*/
|
||||
public async isInCollection(imdbId: string, type: 'movie' | 'show'): Promise<boolean> {
|
||||
try {
|
||||
if (!await this.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure IMDb ID includes the 'tt' prefix
|
||||
const imdbIdWithPrefix = imdbId.startsWith('tt') ? imdbId : `tt${imdbId}`;
|
||||
|
||||
const collectionItems = type === 'movie'
|
||||
? await this.getCollectionMovies()
|
||||
: await this.getCollectionShows();
|
||||
|
||||
return collectionItems.some(item => {
|
||||
const itemImdbId = type === 'movie'
|
||||
? item.movie?.ids?.imdb
|
||||
: item.show?.ids?.imdb;
|
||||
return itemImdbId === imdbIdWithPrefix;
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[TraktService] Failed to check if ${type} is in collection:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle app state changes to reduce memory pressure
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue