mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-28 03:43:02 +00:00
library critical bug fix
This commit is contained in:
parent
49b814a36d
commit
c317e8562e
4 changed files with 435 additions and 183 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue