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