diff --git a/App.tsx b/App.tsx index bd29765..39207b5 100644 --- a/App.tsx +++ b/App.tsx @@ -18,7 +18,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { StatusBar } from 'expo-status-bar'; import { Provider as PaperProvider } from 'react-native-paper'; import { enableScreens, enableFreeze } from 'react-native-screens'; -import AppNavigator, { +import AppNavigator, { CustomNavigationDarkTheme, CustomDarkTheme } from './src/navigation/AppNavigator'; @@ -41,6 +41,7 @@ import { aiService } from './src/services/aiService'; import { AccountProvider, useAccount } from './src/contexts/AccountContext'; import { ToastProvider } from './src/contexts/ToastContext'; import { mmkvStorage } from './src/services/mmkvStorage'; +import AnnouncementOverlay from './src/components/AnnouncementOverlay'; Sentry.init({ dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992', @@ -82,12 +83,13 @@ const ThemedApp = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const engine = (global as any).HermesInternal ? 'Hermes' : 'JSC'; console.log('JS Engine:', engine); - } catch {} + } catch { } }, []); const { currentTheme } = useTheme(); const [isAppReady, setIsAppReady] = useState(false); const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState(null); - + const [showAnnouncement, setShowAnnouncement] = useState(false); + // Update popup functionality const { showUpdatePopup, @@ -100,7 +102,17 @@ const ThemedApp = () => { // GitHub major/minor release overlay const githubUpdate = useGithubMajorUpdate(); - + + // Announcement data + const announcements = [ + { + icon: 'zap', + title: 'Debrid Integration', + description: 'Unlock 4K high-quality streams with lightning-fast speeds. Connect your TorBox account to access cached premium content with zero buffering.', + tag: 'NEW', + }, + ]; + // Check onboarding status and initialize services useEffect(() => { const initializeApp = async () => { @@ -108,28 +120,37 @@ const ThemedApp = () => { // Check onboarding status const onboardingCompleted = await mmkvStorage.getItem('hasCompletedOnboarding'); setHasCompletedOnboarding(onboardingCompleted === 'true'); - + // Initialize update service await UpdateService.initialize(); - + // Initialize memory monitoring service to prevent OutOfMemoryError memoryMonitorService; // Just accessing it starts the monitoring console.log('Memory monitoring service initialized'); - + // Initialize AI service await aiService.initialize(); console.log('AI service initialized'); - + + // Check if announcement should be shown (version 1.0.0) + const announcementShown = await mmkvStorage.getItem('announcement_v1.0.0_shown'); + if (!announcementShown && onboardingCompleted === 'true') { + // Show announcement only after app is ready + setTimeout(() => { + setShowAnnouncement(true); + }, 1000); + } + } catch (error) { console.error('Error initializing app:', error); // Default to showing onboarding if we can't check setHasCompletedOnboarding(false); } }; - + initializeApp(); }, []); - + // Create custom themes based on current theme const customDarkTheme = { ...CustomDarkTheme, @@ -138,7 +159,7 @@ const ThemedApp = () => { primary: currentTheme.colors.primary, } }; - + const customNavigationTheme = { ...CustomNavigationDarkTheme, colors: { @@ -153,15 +174,33 @@ const ThemedApp = () => { const handleSplashComplete = () => { setIsAppReady(true); }; - + + // Navigation reference + const navigationRef = React.useRef(null); + + // Handler for navigating to debrid integration + const handleNavigateToDebrid = () => { + if (navigationRef.current) { + navigationRef.current.navigate('DebridIntegration'); + } + }; + + // Handler for announcement close + const handleAnnouncementClose = async () => { + setShowAnnouncement(false); + // Mark announcement as shown + await mmkvStorage.setItem('announcement_v1.0.0_shown', 'true'); + }; + // Don't render anything until we know the onboarding status const shouldShowApp = isAppReady && hasCompletedOnboarding !== null; const initialRouteName = hasCompletedOnboarding ? 'MainTabs' : 'Onboarding'; - + return ( - @@ -186,6 +225,13 @@ const ThemedApp = () => { onDismiss={githubUpdate.onDismiss} onLater={githubUpdate.onLater} /> + diff --git a/src/components/AnnouncementOverlay.tsx b/src/components/AnnouncementOverlay.tsx new file mode 100644 index 0000000..4a352e2 --- /dev/null +++ b/src/components/AnnouncementOverlay.tsx @@ -0,0 +1,308 @@ +import React, { useEffect, useRef } from 'react'; +import { + View, + Text, + StyleSheet, + Modal, + TouchableOpacity, + Animated, + Dimensions, + ScrollView, +} from 'react-native'; +import { useTheme } from '../contexts/ThemeContext'; +import { Feather } from '@expo/vector-icons'; + +const { width, height } = Dimensions.get('window'); + +interface Announcement { + icon: string; + title: string; + description: string; + tag?: string; +} + +interface AnnouncementOverlayProps { + visible: boolean; + onClose: () => void; + onActionPress?: () => void; + title?: string; + announcements: Announcement[]; + actionButtonText?: string; +} + +const AnnouncementOverlay: React.FC = ({ + visible, + onClose, + onActionPress, + title = "What's New", + announcements, + actionButtonText = "Got it!", +}) => { + const { currentTheme } = useTheme(); + const colors = currentTheme.colors; + + const scaleAnim = useRef(new Animated.Value(0.8)).current; + const opacityAnim = useRef(new Animated.Value(0)).current; + + useEffect(() => { + if (visible) { + Animated.parallel([ + Animated.spring(scaleAnim, { + toValue: 1, + tension: 50, + friction: 7, + useNativeDriver: true, + }), + Animated.timing(opacityAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + } else { + scaleAnim.setValue(0.8); + opacityAnim.setValue(0); + } + }, [visible]); + + const handleClose = () => { + Animated.parallel([ + Animated.spring(scaleAnim, { + toValue: 0.8, + tension: 50, + friction: 7, + useNativeDriver: true, + }), + Animated.timing(opacityAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]).start(() => { + onClose(); + }); + }; + + const handleAction = () => { + if (onActionPress) { + handleClose(); + // Delay navigation slightly to allow animation to complete + setTimeout(() => { + onActionPress(); + }, 300); + } else { + handleClose(); + } + }; + + return ( + + + + + + {/* Close Button */} + + + + + {/* Header */} + + + + + {title} + + Exciting updates in this release + + + + {/* Announcements */} + + {announcements.map((announcement, index) => ( + + + + + + + + {announcement.title} + + {announcement.tag && ( + + {announcement.tag} + + )} + + + {announcement.description} + + + + ))} + + + {/* Action Button */} + + {actionButtonText} + + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.85)', + }, + container: { + width: width * 0.9, + maxWidth: 500, + maxHeight: height * 0.8, + }, + card: { + backgroundColor: '#1a1a1a', + borderRadius: 24, + padding: 24, + shadowColor: '#000', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.3, + shadowRadius: 16, + }, + closeButton: { + position: 'absolute', + top: 16, + right: 16, + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: '#2a2a2a', + justifyContent: 'center', + alignItems: 'center', + zIndex: 10, + }, + header: { + alignItems: 'center', + marginBottom: 24, + }, + iconContainer: { + width: 64, + height: 64, + borderRadius: 32, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 16, + }, + title: { + fontSize: 28, + fontWeight: '700', + letterSpacing: 0.5, + marginBottom: 8, + }, + subtitle: { + fontSize: 14, + textAlign: 'center', + opacity: 0.9, + }, + scrollView: { + maxHeight: height * 0.45, + marginBottom: 20, + }, + announcementItem: { + backgroundColor: '#252525', + flexDirection: 'row', + padding: 16, + borderRadius: 16, + marginBottom: 12, + }, + announcementIcon: { + width: 48, + height: 48, + borderRadius: 24, + justifyContent: 'center', + alignItems: 'center', + marginRight: 16, + }, + announcementContent: { + flex: 1, + }, + announcementHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 6, + }, + announcementTitle: { + fontSize: 16, + fontWeight: '700', + letterSpacing: 0.3, + flex: 1, + }, + tag: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 6, + marginLeft: 8, + }, + tagText: { + fontSize: 10, + fontWeight: '700', + color: '#FFFFFF', + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + announcementDescription: { + fontSize: 14, + lineHeight: 20, + opacity: 0.9, + }, + button: { + borderRadius: 12, + paddingVertical: 16, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 8, + elevation: 4, + }, + buttonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '700', + letterSpacing: 0.5, + }, +}); + +export default AnnouncementOverlay; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 3d93e03..bf7fa4e 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -764,6 +764,21 @@ const SettingsScreen: React.FC = () => { renderControl={ChevronRight} isTablet={isTablet} /> + { + try { + await mmkvStorage.removeItem('announcement_v1.0.0_shown'); + openAlert('Success', 'Announcement reset. Restart the app to see the announcement overlay.'); + } catch (error) { + openAlert('Error', 'Failed to reset announcement.'); + } + }} + renderControl={ChevronRight} + isTablet={isTablet} + />