From c317e8562e1d3b741beefb4f4f32537dce42afa1 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 26 Oct 2025 13:32:40 +0530 Subject: [PATCH] library critical bug fix --- src/hooks/useMetadata.ts | 28 +- src/screens/OnboardingScreen.tsx | 487 ++++++++++++++++++---------- src/services/catalogService.ts | 101 +++++- src/services/notificationService.ts | 2 +- 4 files changed, 435 insertions(+), 183 deletions(-) diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index a139b5e..b861ebb 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -520,8 +520,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setTmdbId(cachedScreen.tmdbId); } // Check if item is in library - const isInLib = catalogService.getLibraryItems().some(item => item.id === id); - setInLibrary(isInLib); + (async () => { + const items = await catalogService.getLibraryItems(); + const isInLib = items.some(item => item.id === id); + setInLibrary(isInLib); + })(); setLoading(false); return; } else { @@ -612,8 +615,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setMetadata(formattedMovie); cacheService.setMetadata(id, type, formattedMovie); - const isInLib = catalogService.getLibraryItems().some(item => item.id === id); - setInLibrary(isInLib); + (async () => { + const items = await catalogService.getLibraryItems(); + const isInLib = items.some(item => item.id === id); + setInLibrary(isInLib); + })(); setLoading(false); return; } @@ -691,8 +697,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat setTmdbId(parseInt(tmdbId)); loadSeriesData().catch((error) => { if (__DEV__) console.error(error); }); - const isInLib = catalogService.getLibraryItems().some(item => item.id === id); - setInLibrary(isInLib); + (async () => { + const items = await catalogService.getLibraryItems(); + const isInLib = items.some(item => item.id === id); + setInLibrary(isInLib); + })(); setLoading(false); return; } @@ -947,8 +956,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat } setMetadata(finalMetadata); cacheService.setMetadata(id, type, finalMetadata); - const isInLib = catalogService.getLibraryItems().some(item => item.id === id); - setInLibrary(isInLib); + (async () => { + const items = await catalogService.getLibraryItems(); + const isInLib = items.some(item => item.id === id); + setInLibrary(isInLib); + })(); } else { // Extract the error from the rejected promise const reason = (content as any)?.reason; diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index 215bc2b..c99125f 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -17,12 +17,16 @@ import Animated, { useAnimatedStyle, withSpring, withTiming, + withRepeat, + withSequence, FadeInDown, FadeInUp, useAnimatedScrollHandler, runOnJS, interpolateColor, interpolate, + Extrapolation, + useAnimatedReaction, } from 'react-native-reanimated'; import { useTheme } from '../contexts/ThemeContext'; import { NavigationProp, useNavigation } from '@react-navigation/native'; @@ -31,6 +35,206 @@ import { mmkvStorage } from '../services/mmkvStorage'; const { width, height } = Dimensions.get('window'); +// Animation configuration +const SPRING_CONFIG = { + damping: 15, + stiffness: 150, + mass: 1, +}; + +const SLIDE_TIMING = { + duration: 400, +}; + +// Animated Button Component +const AnimatedButton = ({ + onPress, + backgroundColor, + text, + icon, +}: { + onPress: () => void; + backgroundColor: string; + text: string; + icon: string; +}) => { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + const handlePressIn = () => { + scale.value = withSpring(0.95, SPRING_CONFIG); + }; + + const handlePressOut = () => { + scale.value = withSpring(1, SPRING_CONFIG); + }; + + return ( + + + {text} + + + + ); +}; + +// Slide Content Component with animations +const SlideContent = ({ item, isActive }: { item: OnboardingSlide; isActive: boolean }) => { + // Premium icon animations: scale, floating, rotation, and glow + const iconScale = useSharedValue(isActive ? 1 : 0.8); + const iconOpacity = useSharedValue(isActive ? 1 : 0); + const iconTranslateY = useSharedValue(isActive ? 0 : 20); + const iconRotation = useSharedValue(0); + const glowIntensity = useSharedValue(isActive ? 1 : 0); + + React.useEffect(() => { + if (isActive) { + iconScale.value = withSpring(1.1, SPRING_CONFIG); + iconOpacity.value = withTiming(1, SLIDE_TIMING); + iconTranslateY.value = withSpring(0, SPRING_CONFIG); + iconRotation.value = withSpring(0, SPRING_CONFIG); + glowIntensity.value = withSpring(1, SPRING_CONFIG); + } else { + iconScale.value = 0.8; + iconOpacity.value = 0; + iconTranslateY.value = 20; + iconRotation.value = -15; + glowIntensity.value = 0; + } + }, [isActive]); + + const animatedIconStyle = useAnimatedStyle(() => { + return { + transform: [ + { scale: iconScale.value }, + { translateY: iconTranslateY.value }, + { rotate: `${iconRotation.value}deg` }, + ], + opacity: iconOpacity.value, + }; + }); + + // Premium floating animation for active icon + const floatAnim = useSharedValue(0); + React.useEffect(() => { + if (isActive) { + floatAnim.value = withRepeat( + withSequence( + withTiming(10, { duration: 2500 }), + withTiming(-10, { duration: 2500 }) + ), + -1, + true + ); + } else { + floatAnim.value = 0; + } + }, [isActive]); + + const floatingIconStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: floatAnim.value }], + })); + + // Glow animation with pulse effect + const pulseAnim = useSharedValue(1); + React.useEffect(() => { + if (isActive) { + pulseAnim.value = withRepeat( + withSequence( + withTiming(1.3, { duration: 2000 }), + withTiming(1.1, { duration: 2000 }) + ), + -1, + true + ); + } + }, [isActive]); + + const animatedGlowStyle = useAnimatedStyle(() => ({ + opacity: glowIntensity.value * 0.5, + transform: [{ scale: pulseAnim.value * 1.2 + iconScale.value * 0.3 }], + })); + + return ( + + {/* Premium glow effect */} + + + + + + + + + + + + + {item.title} + + + {item.subtitle} + + + {item.description} + + + + ); +}; + interface OnboardingSlide { id: string; title: string; @@ -84,30 +288,22 @@ const OnboardingScreen = () => { const scrollX = useSharedValue(0); const currentSlide = onboardingData[currentIndex]; + // Update progress when index changes + React.useEffect(() => { + progressValue.value = withSpring( + (currentIndex + 1) / onboardingData.length, + SPRING_CONFIG + ); + }, [currentIndex]); + const onScroll = useAnimatedScrollHandler({ onScroll: (event) => { scrollX.value = event.contentOffset.x; }, }); - const getAnimatedBackgroundStyle = (slideIndex: number) => { - return useAnimatedStyle(() => { - const inputRange = [(slideIndex - 1) * width, slideIndex * width, (slideIndex + 1) * width]; - const opacity = interpolate( - scrollX.value, - inputRange, - [0, 1, 0], - 'clamp' - ); - - return { - opacity, - }; - }); - }; - const animatedProgressStyle = useAnimatedStyle(() => ({ - width: withSpring(`${((currentIndex + 1) / onboardingData.length) * 100}%`), + width: `${progressValue.value * 100}%`, })); const handleNext = () => { @@ -147,94 +343,91 @@ const OnboardingScreen = () => { const isActive = index === currentIndex; return ( - - - - - - + + ); + }; - - - {item.title} - - - {item.subtitle} - - - {item.description} - - - + const renderPaginationDot = (index: number) => { + const scale = useSharedValue(index === currentIndex ? 1 : 0.8); + const opacity = useSharedValue(index === currentIndex ? 1 : 0.4); + + React.useEffect(() => { + scale.value = withSpring( + index === currentIndex ? 1.3 : 0.8, + SPRING_CONFIG + ); + opacity.value = withTiming( + index === currentIndex ? 1 : 0.4, + SLIDE_TIMING + ); + }, [currentIndex, index]); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + opacity: opacity.value, + })); + + return ( + ); }; const renderPagination = () => ( - {onboardingData.map((_, index) => ( - - ))} + {onboardingData.map((_, index) => renderPaginationDot(index))} ); + // Background slide styles + const getBackgroundSlideStyle = (index: number) => { + 'worklet'; + return useAnimatedStyle(() => { + const inputRange = [(index - 1) * width, index * width, (index + 1) * width]; + const slideOpacity = interpolate( + scrollX.value, + inputRange, + [0, 1, 0], + Extrapolation.CLAMP + ); + + return { opacity: slideOpacity }; + }); + }; + return ( - - {/* Layered animated gradient backgrounds */} - {onboardingData.map((slide, index) => ( - - - - ))} - - {/* Decorative gradient blobs that change with current slide */} - - + + {/* Animated gradient background that transitions between slides */} + + {onboardingData.map((slide, index) => ( + + + + + ))} + + {/* Content container with status bar padding */} @@ -282,24 +475,12 @@ const OnboardingScreen = () => { {renderPagination()} - - - {currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Next'} - - - + backgroundColor={currentTheme.colors.primary} + text={currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Next'} + icon={currentIndex === onboardingData.length - 1 ? 'check' : 'arrow-forward'} + /> @@ -344,87 +525,67 @@ const styles = StyleSheet.create({ justifyContent: 'center', paddingHorizontal: 40, }, - backgroundPanel: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - borderRadius: 0, - }, - overlayPanel: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - borderRadius: 0, - }, fullScreenContainer: { flex: 1, paddingTop: Platform.OS === 'ios' ? 44 : StatusBar.currentHeight || 24, }, - blobTopRight: { + glowContainer: { position: 'absolute', - top: -60, - right: -60, - width: 220, - height: 220, - borderRadius: 110, - opacity: 0.35, - transform: [{ rotate: '15deg' }], + width: 200, + height: 200, + borderRadius: 100, + alignItems: 'center', + justifyContent: 'center', + top: '35%', }, - blobBottomLeft: { - position: 'absolute', - bottom: -70, - left: -70, - width: 260, - height: 260, - borderRadius: 130, - opacity: 0.28, - transform: [{ rotate: '-20deg' }], + glowCircle: { + width: '100%', + height: '100%', + borderRadius: 100, + opacity: 0.4, }, iconContainer: { - width: 160, - height: 160, - borderRadius: 80, + width: 180, + height: 180, + borderRadius: 90, alignItems: 'center', justifyContent: 'center', marginBottom: 60, shadowColor: '#000', shadowOffset: { width: 0, - height: 10, + height: 15, }, - shadowOpacity: 0.3, - shadowRadius: 20, - elevation: 15, + shadowOpacity: 0.5, + shadowRadius: 25, + elevation: 20, }, iconWrapper: { alignItems: 'center', justifyContent: 'center', + zIndex: 10, }, textContainer: { alignItems: 'center', paddingHorizontal: 20, }, title: { - fontSize: 28, + fontSize: 32, fontWeight: 'bold', textAlign: 'center', - marginBottom: 8, + marginBottom: 12, }, subtitle: { - fontSize: 18, + fontSize: 20, fontWeight: '600', textAlign: 'center', - marginBottom: 16, + marginBottom: 20, }, description: { fontSize: 16, textAlign: 'center', lineHeight: 24, - maxWidth: 280, + maxWidth: 300, }, footer: { paddingHorizontal: 20, @@ -437,10 +598,10 @@ const styles = StyleSheet.create({ marginBottom: 40, }, paginationDot: { - width: 8, - height: 8, - borderRadius: 4, - marginHorizontal: 4, + width: 10, + height: 10, + borderRadius: 5, + marginHorizontal: 6, }, buttonContainer: { alignItems: 'center', @@ -462,9 +623,7 @@ const styles = StyleSheet.create({ shadowRadius: 8, elevation: 8, }, - nextButton: { - // Additional styles for next button can go here - }, + nextButton: {}, buttonText: { fontSize: 16, fontWeight: '600', diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index c8e576a..fdb6b4c 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -155,11 +155,39 @@ class CatalogService { private librarySubscribers: ((items: StreamingContent[]) => void)[] = []; private libraryAddListeners: ((item: StreamingContent) => void)[] = []; private libraryRemoveListeners: ((type: string, id: string) => void)[] = []; + private initPromise: Promise; + private isInitialized: boolean = false; private constructor() { - this.initializeScope(); - this.loadLibrary(); - this.loadRecentContent(); + this.initPromise = this.initialize(); + } + + private async initialize(): Promise { + logger.log('[CatalogService] Starting initialization...'); + try { + logger.log('[CatalogService] Step 1: Initializing scope...'); + await this.initializeScope(); + logger.log('[CatalogService] Step 2: Loading library...'); + await this.loadLibrary(); + logger.log('[CatalogService] Step 3: Loading recent content...'); + await this.loadRecentContent(); + this.isInitialized = true; + logger.log(`[CatalogService] Initialization completed successfully. Library contains ${Object.keys(this.library).length} items.`); + } catch (error) { + logger.error('[CatalogService] Initialization failed:', error); + // Still mark as initialized to prevent blocking forever + this.isInitialized = true; + } + } + + private async ensureInitialized(): Promise { + logger.log(`[CatalogService] ensureInitialized() called. isInitialized: ${this.isInitialized}`); + try { + await this.initPromise; + logger.log(`[CatalogService] ensureInitialized() completed. Library ready with ${Object.keys(this.library).length} items.`); + } catch (error) { + logger.error('[CatalogService] Error waiting for initialization:', error); + } } private async initializeScope(): Promise { @@ -168,6 +196,8 @@ class CatalogService { if (!currentScope) { await mmkvStorage.setItem('@user:current', 'local'); logger.log('[CatalogService] Initialized @user:current scope to "local"'); + } else { + logger.log(`[CatalogService] Using existing scope: "${currentScope}"`); } } catch (error) { logger.error('[CatalogService] Failed to initialize scope:', error); @@ -194,7 +224,29 @@ class CatalogService { } } if (storedLibrary) { - this.library = JSON.parse(storedLibrary); + const parsedLibrary = JSON.parse(storedLibrary); + logger.log(`[CatalogService] Raw library data type: ${Array.isArray(parsedLibrary) ? 'ARRAY' : 'OBJECT'}, keys: ${JSON.stringify(Object.keys(parsedLibrary).slice(0, 5))}`); + + // Convert array format to object format if needed + if (Array.isArray(parsedLibrary)) { + logger.log(`[CatalogService] WARNING: Library is stored as ARRAY format. Converting to OBJECT format.`); + const libraryObject: Record = {}; + for (const item of parsedLibrary) { + const key = `${item.type}:${item.id}`; + libraryObject[key] = item; + } + this.library = libraryObject; + logger.log(`[CatalogService] Converted ${parsedLibrary.length} items from array to object format`); + // Re-save in correct format (don't call ensureInitialized here since we're still initializing) + const scope = (await mmkvStorage.getItem('@user:current')) || 'local'; + const scopedKey = `@user:${scope}:stremio-library`; + const libraryData = JSON.stringify(this.library); + await mmkvStorage.setItem(scopedKey, libraryData); + await mmkvStorage.setItem(this.LEGACY_LIBRARY_KEY, libraryData); + logger.log(`[CatalogService] Re-saved library in correct format`); + } else { + this.library = parsedLibrary; + } logger.log(`[CatalogService] Library loaded successfully with ${Object.keys(this.library).length} items from scope: ${scope}`); } else { logger.log(`[CatalogService] No library data found for scope: ${scope}`); @@ -209,15 +261,25 @@ class CatalogService { } private async saveLibrary(): Promise { + // Only wait for initialization if we're not already initializing (avoid circular dependency) + if (this.isInitialized) { + await this.ensureInitialized(); + } try { + const itemCount = Object.keys(this.library).length; const scope = (await mmkvStorage.getItem('@user:current')) || 'local'; const scopedKey = `@user:${scope}:stremio-library`; const libraryData = JSON.stringify(this.library); + + logger.log(`[CatalogService] Saving library with ${itemCount} items to scope: "${scope}" (key: ${scopedKey})`); + await mmkvStorage.setItem(scopedKey, libraryData); await mmkvStorage.setItem(this.LEGACY_LIBRARY_KEY, libraryData); - logger.log(`[CatalogService] Library saved successfully with ${Object.keys(this.library).length} items to scope: ${scope}`); + + logger.log(`[CatalogService] Library saved successfully with ${itemCount} items`); } catch (error: any) { logger.error('Failed to save library:', error); + logger.error(`[CatalogService] Library save failed details - scope: ${(await mmkvStorage.getItem('@user:current')) || 'unknown'}, itemCount: ${Object.keys(this.library).length}`); } } @@ -856,14 +918,18 @@ class CatalogService { }; } - public getLibraryItems(): StreamingContent[] { - return Object.values(this.library); + public async getLibraryItems(): Promise { + logger.log(`[CatalogService] getLibraryItems() called. Library contains ${Object.keys(this.library).length} items`); + await this.ensureInitialized(); + const items = Object.values(this.library); + logger.log(`[CatalogService] getLibraryItems() returning ${items.length} items`); + return items; } public subscribeToLibraryUpdates(callback: (items: StreamingContent[]) => void): () => void { this.librarySubscribers.push(callback); // Initial callback with current items - callback(this.getLibraryItems()); + this.getLibraryItems().then(items => callback(items)); // Return unsubscribe function return () => { @@ -875,12 +941,19 @@ class CatalogService { } public async addToLibrary(content: StreamingContent): Promise { + logger.log(`[CatalogService] addToLibrary() called for: ${content.type}:${content.id} (${content.name})`); + await this.ensureInitialized(); const key = `${content.type}:${content.id}`; + const itemCountBefore = Object.keys(this.library).length; + logger.log(`[CatalogService] Adding to library with key: "${key}". Current library keys: [${Object.keys(this.library).length}] items`); this.library[key] = { ...content, addedToLibraryAt: Date.now() // Add timestamp }; - this.saveLibrary(); + const itemCountAfter = Object.keys(this.library).length; + logger.log(`[CatalogService] Library updated: ${itemCountBefore} -> ${itemCountAfter} items. New library keys: [${Object.keys(this.library).slice(0, 5).join(', ')}${Object.keys(this.library).length > 5 ? '...' : ''}]`); + await this.saveLibrary(); + logger.log(`[CatalogService] addToLibrary() completed for: ${content.type}:${content.id}`); this.notifyLibrarySubscribers(); try { this.libraryAddListeners.forEach(l => l(content)); } catch {} @@ -896,9 +969,17 @@ class CatalogService { } public async removeFromLibrary(type: string, id: string): Promise { + logger.log(`[CatalogService] removeFromLibrary() called for: ${type}:${id}`); + await this.ensureInitialized(); const key = `${type}:${id}`; + const itemCountBefore = Object.keys(this.library).length; + const itemExisted = key in this.library; + logger.log(`[CatalogService] Removing key: "${key}". Currently library has ${itemCountBefore} items with keys: [${Object.keys(this.library).slice(0, 5).join(', ')}${Object.keys(this.library).length > 5 ? '...' : ''}]`); delete this.library[key]; - this.saveLibrary(); + const itemCountAfter = Object.keys(this.library).length; + logger.log(`[CatalogService] Library updated: ${itemCountBefore} -> ${itemCountAfter} items (existed: ${itemExisted})`); + await this.saveLibrary(); + logger.log(`[CatalogService] removeFromLibrary() completed for: ${type}:${id}`); this.notifyLibrarySubscribers(); try { this.libraryRemoveListeners.forEach(l => l(type, id)); } catch {} diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index 9874859..d1c385a 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -407,7 +407,7 @@ class NotificationService { // logger.log('[NotificationService] Starting comprehensive background sync'); // Get library items - const libraryItems = catalogService.getLibraryItems(); + const libraryItems = await catalogService.getLibraryItems(); await this.syncNotificationsForLibrary(libraryItems); // Sync Trakt items if authenticated