library critical bug fix

This commit is contained in:
tapframe 2025-10-26 13:32:40 +05:30
parent 49b814a36d
commit c317e8562e
4 changed files with 435 additions and 183 deletions

View file

@ -520,8 +520,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
setTmdbId(cachedScreen.tmdbId); setTmdbId(cachedScreen.tmdbId);
} }
// Check if item is in library // Check if item is in library
const isInLib = catalogService.getLibraryItems().some(item => item.id === id); (async () => {
setInLibrary(isInLib); const items = await catalogService.getLibraryItems();
const isInLib = items.some(item => item.id === id);
setInLibrary(isInLib);
})();
setLoading(false); setLoading(false);
return; return;
} else { } else {
@ -612,8 +615,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
setMetadata(formattedMovie); setMetadata(formattedMovie);
cacheService.setMetadata(id, type, formattedMovie); cacheService.setMetadata(id, type, formattedMovie);
const isInLib = catalogService.getLibraryItems().some(item => item.id === id); (async () => {
setInLibrary(isInLib); const items = await catalogService.getLibraryItems();
const isInLib = items.some(item => item.id === id);
setInLibrary(isInLib);
})();
setLoading(false); setLoading(false);
return; return;
} }
@ -691,8 +697,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
setTmdbId(parseInt(tmdbId)); setTmdbId(parseInt(tmdbId));
loadSeriesData().catch((error) => { if (__DEV__) console.error(error); }); loadSeriesData().catch((error) => { if (__DEV__) console.error(error); });
const isInLib = catalogService.getLibraryItems().some(item => item.id === id); (async () => {
setInLibrary(isInLib); const items = await catalogService.getLibraryItems();
const isInLib = items.some(item => item.id === id);
setInLibrary(isInLib);
})();
setLoading(false); setLoading(false);
return; return;
} }
@ -947,8 +956,11 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
} }
setMetadata(finalMetadata); setMetadata(finalMetadata);
cacheService.setMetadata(id, type, finalMetadata); cacheService.setMetadata(id, type, finalMetadata);
const isInLib = catalogService.getLibraryItems().some(item => item.id === id); (async () => {
setInLibrary(isInLib); const items = await catalogService.getLibraryItems();
const isInLib = items.some(item => item.id === id);
setInLibrary(isInLib);
})();
} else { } else {
// Extract the error from the rejected promise // Extract the error from the rejected promise
const reason = (content as any)?.reason; const reason = (content as any)?.reason;

View file

@ -17,12 +17,16 @@ import Animated, {
useAnimatedStyle, useAnimatedStyle,
withSpring, withSpring,
withTiming, withTiming,
withRepeat,
withSequence,
FadeInDown, FadeInDown,
FadeInUp, FadeInUp,
useAnimatedScrollHandler, useAnimatedScrollHandler,
runOnJS, runOnJS,
interpolateColor, interpolateColor,
interpolate, interpolate,
Extrapolation,
useAnimatedReaction,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { NavigationProp, useNavigation } from '@react-navigation/native'; import { NavigationProp, useNavigation } from '@react-navigation/native';
@ -31,6 +35,206 @@ import { mmkvStorage } from '../services/mmkvStorage';
const { width, height } = Dimensions.get('window'); 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 (
<TouchableOpacity
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
activeOpacity={1}
>
<Animated.View
style={[
styles.button,
styles.nextButton,
{ backgroundColor },
animatedStyle,
]}
>
<Text style={[styles.buttonText, { color: 'white' }]}>{text}</Text>
<MaterialIcons
name={icon as any}
size={20}
color="white"
style={styles.buttonIcon}
/>
</Animated.View>
</TouchableOpacity>
);
};
// 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 (
<View style={styles.slide}>
{/* Premium glow effect */}
<Animated.View
style={[
styles.glowContainer,
animatedGlowStyle,
]}
>
<LinearGradient
colors={item.gradient}
style={styles.glowCircle}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
</Animated.View>
<LinearGradient
colors={item.gradient}
style={styles.iconContainer}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Animated.View style={[styles.iconWrapper, animatedIconStyle, floatingIconStyle]}>
<MaterialIcons
name={item.icon}
size={95}
color="white"
/>
</Animated.View>
</LinearGradient>
<Animated.View
entering={FadeInUp.delay(300).duration(600)}
style={styles.textContainer}
>
<Animated.Text
entering={FadeInUp.delay(400).duration(500)}
style={[styles.title, { color: 'white' }]}
>
{item.title}
</Animated.Text>
<Animated.Text
entering={FadeInUp.delay(500).duration(500)}
style={[styles.subtitle, { color: 'rgba(255,255,255,0.9)' }]}
>
{item.subtitle}
</Animated.Text>
<Animated.Text
entering={FadeInUp.delay(600).duration(500)}
style={[styles.description, { color: 'rgba(255,255,255,0.85)' }]}
>
{item.description}
</Animated.Text>
</Animated.View>
</View>
);
};
interface OnboardingSlide { interface OnboardingSlide {
id: string; id: string;
title: string; title: string;
@ -84,30 +288,22 @@ const OnboardingScreen = () => {
const scrollX = useSharedValue(0); const scrollX = useSharedValue(0);
const currentSlide = onboardingData[currentIndex]; const currentSlide = onboardingData[currentIndex];
// Update progress when index changes
React.useEffect(() => {
progressValue.value = withSpring(
(currentIndex + 1) / onboardingData.length,
SPRING_CONFIG
);
}, [currentIndex]);
const onScroll = useAnimatedScrollHandler({ const onScroll = useAnimatedScrollHandler({
onScroll: (event) => { onScroll: (event) => {
scrollX.value = event.contentOffset.x; 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(() => ({ const animatedProgressStyle = useAnimatedStyle(() => ({
width: withSpring(`${((currentIndex + 1) / onboardingData.length) * 100}%`), width: `${progressValue.value * 100}%`,
})); }));
const handleNext = () => { const handleNext = () => {
@ -147,94 +343,91 @@ const OnboardingScreen = () => {
const isActive = index === currentIndex; const isActive = index === currentIndex;
return ( return (
<View style={styles.slide}> <SlideContent
<LinearGradient item={item}
colors={item.gradient} isActive={isActive}
style={styles.iconContainer} />
start={{ x: 0, y: 0 }} );
end={{ x: 1, y: 1 }} };
>
<Animated.View
entering={FadeInDown.delay(300).duration(800)}
style={styles.iconWrapper}
>
<MaterialIcons
name={item.icon}
size={80}
color="white"
/>
</Animated.View>
</LinearGradient>
<Animated.View const renderPaginationDot = (index: number) => {
entering={FadeInUp.delay(500).duration(800)} const scale = useSharedValue(index === currentIndex ? 1 : 0.8);
style={styles.textContainer} const opacity = useSharedValue(index === currentIndex ? 1 : 0.4);
>
<Text style={[styles.title, { color: 'white' }]}> React.useEffect(() => {
{item.title} scale.value = withSpring(
</Text> index === currentIndex ? 1.3 : 0.8,
<Text style={[styles.subtitle, { color: 'rgba(255,255,255,0.9)' }]}> SPRING_CONFIG
{item.subtitle} );
</Text> opacity.value = withTiming(
<Text style={[styles.description, { color: 'rgba(255,255,255,0.85)' }]}> index === currentIndex ? 1 : 0.4,
{item.description} SLIDE_TIMING
</Text> );
</Animated.View> }, [currentIndex, index]);
</View>
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
return (
<Animated.View
key={index}
style={[
styles.paginationDot,
{
backgroundColor: index === currentIndex
? currentTheme.colors.primary
: currentTheme.colors.elevation2,
},
animatedStyle,
]}
/>
); );
}; };
const renderPagination = () => ( const renderPagination = () => (
<View style={styles.pagination}> <View style={styles.pagination}>
{onboardingData.map((_, index) => ( {onboardingData.map((_, index) => renderPaginationDot(index))}
<View
key={index}
style={[
styles.paginationDot,
{
backgroundColor: index === currentIndex
? currentTheme.colors.primary
: currentTheme.colors.elevation2,
opacity: index === currentIndex ? 1 : 0.4,
},
]}
/>
))}
</View> </View>
); );
// 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 ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={styles.container}>
{/* Layered animated gradient backgrounds */} {/* Animated gradient background that transitions between slides */}
{onboardingData.map((slide, index) => ( <Animated.View style={StyleSheet.absoluteFill}>
<Animated.View key={`bg-${index}`} style={[styles.backgroundPanel, getAnimatedBackgroundStyle(index)]}> {onboardingData.map((slide, index) => (
<LinearGradient <Animated.View
colors={[slide.gradient[0], slide.gradient[1]]} key={`bg-${index}`}
start={{ x: 0, y: 0 }} style={[StyleSheet.absoluteFill, getBackgroundSlideStyle(index)]}
end={{ x: 1, y: 1 }} >
style={StyleSheet.absoluteFill} <LinearGradient
/> colors={slide.gradient}
</Animated.View> start={{ x: 0, y: 0 }}
))} end={{ x: 1, y: 1 }}
<LinearGradient style={StyleSheet.absoluteFill}
colors={["rgba(0,0,0,0.2)", "rgba(0,0,0,0.45)"]} />
start={{ x: 0, y: 0 }} <View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.25)' }]} />
end={{ x: 0, y: 1 }} </Animated.View>
style={styles.overlayPanel} ))}
/> </Animated.View>
{/* Decorative gradient blobs that change with current slide */}
<LinearGradient
colors={[currentSlide.gradient[1], 'transparent']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.blobTopRight}
/>
<LinearGradient
colors={[currentSlide.gradient[0], 'transparent']}
start={{ x: 1, y: 1 }}
end={{ x: 0, y: 0 }}
style={styles.blobBottomLeft}
/>
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent /> <StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
{/* Content container with status bar padding */} {/* Content container with status bar padding */}
@ -282,24 +475,12 @@ const OnboardingScreen = () => {
{renderPagination()} {renderPagination()}
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<TouchableOpacity <AnimatedButton
style={[
styles.button,
styles.nextButton,
{ backgroundColor: currentTheme.colors.primary }
]}
onPress={handleNext} onPress={handleNext}
> backgroundColor={currentTheme.colors.primary}
<Text style={[styles.buttonText, { color: 'white' }]}> text={currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Next'}
{currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Next'} icon={currentIndex === onboardingData.length - 1 ? 'check' : 'arrow-forward'}
</Text> />
<MaterialIcons
name={currentIndex === onboardingData.length - 1 ? 'check' : 'arrow-forward'}
size={20}
color="white"
style={styles.buttonIcon}
/>
</TouchableOpacity>
</View> </View>
</View> </View>
</View> </View>
@ -344,87 +525,67 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
paddingHorizontal: 40, 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: { fullScreenContainer: {
flex: 1, flex: 1,
paddingTop: Platform.OS === 'ios' ? 44 : StatusBar.currentHeight || 24, paddingTop: Platform.OS === 'ios' ? 44 : StatusBar.currentHeight || 24,
}, },
blobTopRight: { glowContainer: {
position: 'absolute', position: 'absolute',
top: -60, width: 200,
right: -60, height: 200,
width: 220, borderRadius: 100,
height: 220, alignItems: 'center',
borderRadius: 110, justifyContent: 'center',
opacity: 0.35, top: '35%',
transform: [{ rotate: '15deg' }],
}, },
blobBottomLeft: { glowCircle: {
position: 'absolute', width: '100%',
bottom: -70, height: '100%',
left: -70, borderRadius: 100,
width: 260, opacity: 0.4,
height: 260,
borderRadius: 130,
opacity: 0.28,
transform: [{ rotate: '-20deg' }],
}, },
iconContainer: { iconContainer: {
width: 160, width: 180,
height: 160, height: 180,
borderRadius: 80, borderRadius: 90,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginBottom: 60, marginBottom: 60,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 10, height: 15,
}, },
shadowOpacity: 0.3, shadowOpacity: 0.5,
shadowRadius: 20, shadowRadius: 25,
elevation: 15, elevation: 20,
}, },
iconWrapper: { iconWrapper: {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
zIndex: 10,
}, },
textContainer: { textContainer: {
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 20, paddingHorizontal: 20,
}, },
title: { title: {
fontSize: 28, fontSize: 32,
fontWeight: 'bold', fontWeight: 'bold',
textAlign: 'center', textAlign: 'center',
marginBottom: 8, marginBottom: 12,
}, },
subtitle: { subtitle: {
fontSize: 18, fontSize: 20,
fontWeight: '600', fontWeight: '600',
textAlign: 'center', textAlign: 'center',
marginBottom: 16, marginBottom: 20,
}, },
description: { description: {
fontSize: 16, fontSize: 16,
textAlign: 'center', textAlign: 'center',
lineHeight: 24, lineHeight: 24,
maxWidth: 280, maxWidth: 300,
}, },
footer: { footer: {
paddingHorizontal: 20, paddingHorizontal: 20,
@ -437,10 +598,10 @@ const styles = StyleSheet.create({
marginBottom: 40, marginBottom: 40,
}, },
paginationDot: { paginationDot: {
width: 8, width: 10,
height: 8, height: 10,
borderRadius: 4, borderRadius: 5,
marginHorizontal: 4, marginHorizontal: 6,
}, },
buttonContainer: { buttonContainer: {
alignItems: 'center', alignItems: 'center',
@ -462,9 +623,7 @@ const styles = StyleSheet.create({
shadowRadius: 8, shadowRadius: 8,
elevation: 8, elevation: 8,
}, },
nextButton: { nextButton: {},
// Additional styles for next button can go here
},
buttonText: { buttonText: {
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '600',

View file

@ -155,11 +155,39 @@ class CatalogService {
private librarySubscribers: ((items: StreamingContent[]) => void)[] = []; private librarySubscribers: ((items: StreamingContent[]) => void)[] = [];
private libraryAddListeners: ((item: StreamingContent) => void)[] = []; private libraryAddListeners: ((item: StreamingContent) => void)[] = [];
private libraryRemoveListeners: ((type: string, id: string) => void)[] = []; private libraryRemoveListeners: ((type: string, id: string) => void)[] = [];
private initPromise: Promise<void>;
private isInitialized: boolean = false;
private constructor() { private constructor() {
this.initializeScope(); this.initPromise = this.initialize();
this.loadLibrary(); }
this.loadRecentContent();
private async initialize(): Promise<void> {
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<void> {
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<void> { private async initializeScope(): Promise<void> {
@ -168,6 +196,8 @@ class CatalogService {
if (!currentScope) { if (!currentScope) {
await mmkvStorage.setItem('@user:current', 'local'); await mmkvStorage.setItem('@user:current', 'local');
logger.log('[CatalogService] Initialized @user:current scope to "local"'); logger.log('[CatalogService] Initialized @user:current scope to "local"');
} else {
logger.log(`[CatalogService] Using existing scope: "${currentScope}"`);
} }
} catch (error) { } catch (error) {
logger.error('[CatalogService] Failed to initialize scope:', error); logger.error('[CatalogService] Failed to initialize scope:', error);
@ -194,7 +224,29 @@ class CatalogService {
} }
} }
if (storedLibrary) { 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<string, StreamingContent> = {};
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}`); logger.log(`[CatalogService] Library loaded successfully with ${Object.keys(this.library).length} items from scope: ${scope}`);
} else { } else {
logger.log(`[CatalogService] No library data found for scope: ${scope}`); logger.log(`[CatalogService] No library data found for scope: ${scope}`);
@ -209,15 +261,25 @@ class CatalogService {
} }
private async saveLibrary(): Promise<void> { private async saveLibrary(): Promise<void> {
// Only wait for initialization if we're not already initializing (avoid circular dependency)
if (this.isInitialized) {
await this.ensureInitialized();
}
try { try {
const itemCount = Object.keys(this.library).length;
const scope = (await mmkvStorage.getItem('@user:current')) || 'local'; const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
const scopedKey = `@user:${scope}:stremio-library`; const scopedKey = `@user:${scope}:stremio-library`;
const libraryData = JSON.stringify(this.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(scopedKey, libraryData);
await mmkvStorage.setItem(this.LEGACY_LIBRARY_KEY, 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) { } catch (error: any) {
logger.error('Failed to save library:', error); 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[] { public async getLibraryItems(): Promise<StreamingContent[]> {
return Object.values(this.library); 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 { public subscribeToLibraryUpdates(callback: (items: StreamingContent[]) => void): () => void {
this.librarySubscribers.push(callback); this.librarySubscribers.push(callback);
// Initial callback with current items // Initial callback with current items
callback(this.getLibraryItems()); this.getLibraryItems().then(items => callback(items));
// Return unsubscribe function // Return unsubscribe function
return () => { return () => {
@ -875,12 +941,19 @@ class CatalogService {
} }
public async addToLibrary(content: StreamingContent): Promise<void> { public async addToLibrary(content: StreamingContent): Promise<void> {
logger.log(`[CatalogService] addToLibrary() called for: ${content.type}:${content.id} (${content.name})`);
await this.ensureInitialized();
const key = `${content.type}:${content.id}`; 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] = { this.library[key] = {
...content, ...content,
addedToLibraryAt: Date.now() // Add timestamp 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(); this.notifyLibrarySubscribers();
try { this.libraryAddListeners.forEach(l => l(content)); } catch {} try { this.libraryAddListeners.forEach(l => l(content)); } catch {}
@ -896,9 +969,17 @@ class CatalogService {
} }
public async removeFromLibrary(type: string, id: string): Promise<void> { public async removeFromLibrary(type: string, id: string): Promise<void> {
logger.log(`[CatalogService] removeFromLibrary() called for: ${type}:${id}`);
await this.ensureInitialized();
const key = `${type}:${id}`; 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]; 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(); this.notifyLibrarySubscribers();
try { this.libraryRemoveListeners.forEach(l => l(type, id)); } catch {} try { this.libraryRemoveListeners.forEach(l => l(type, id)); } catch {}

View file

@ -407,7 +407,7 @@ class NotificationService {
// logger.log('[NotificationService] Starting comprehensive background sync'); // logger.log('[NotificationService] Starting comprehensive background sync');
// Get library items // Get library items
const libraryItems = catalogService.getLibraryItems(); const libraryItems = await catalogService.getLibraryItems();
await this.syncNotificationsForLibrary(libraryItems); await this.syncNotificationsForLibrary(libraryItems);
// Sync Trakt items if authenticated // Sync Trakt items if authenticated