diff --git a/assets/nuvio-sync-icon-og.png b/assets/nuvio-sync-icon-og.png new file mode 100644 index 000000000..28bf19e55 Binary files /dev/null and b/assets/nuvio-sync-icon-og.png differ diff --git a/assets/text_only_og.png b/assets/text_only_og.png new file mode 100644 index 000000000..35eca9a46 Binary files /dev/null and b/assets/text_only_og.png differ diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx index fc7858082..373bb5129 100644 --- a/src/screens/AuthScreen.tsx +++ b/src/screens/AuthScreen.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Dimensions, Animated, Easing, Keyboard } from 'react-native'; +import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Animated, Easing, Keyboard, StatusBar, useWindowDimensions } from 'react-native'; import { mmkvStorage } from '../services/mmkvStorage'; import { LinearGradient } from 'expo-linear-gradient'; import { MaterialIcons } from '@expo/vector-icons'; @@ -10,8 +10,8 @@ import * as Haptics from 'expo-haptics'; import { useToast } from '../contexts/ToastContext'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -const { width, height } = Dimensions.get('window'); const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__'; +const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24']; const normalizeAuthErrorMessage = (input: string): string => { const raw = (input || '').trim(); @@ -40,12 +40,16 @@ const normalizeAuthErrorMessage = (input: string): string => { }; const AuthScreen: React.FC = () => { + const { width, height } = useWindowDimensions(); + const isTablet = width >= 768; const { currentTheme } = useTheme(); const { signIn, signUp } = useAccount(); const navigation = useNavigation(); const route = useRoute(); const fromOnboarding = !!route?.params?.fromOnboarding; const insets = useSafeAreaInsets(); + const safeTopInset = Math.max(insets.top, Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : 0); + const backButtonTop = safeTopInset + 8; const { showError, showSuccess } = useToast(); const [email, setEmail] = useState(''); @@ -70,8 +74,6 @@ const AuthScreen: React.FC = () => { const modeAnim = useRef(new Animated.Value(0)).current; // 0 = signin, 1 = signup const [switchWidth, setSwitchWidth] = useState(0); // Legacy local toast state removed in favor of global toast - const [headerHeight, setHeaderHeight] = useState(0); - const headerHideAnim = useRef(new Animated.Value(0)).current; // 0 visible, 1 hidden const [keyboardHeight, setKeyboardHeight] = useState(0); useEffect(() => { @@ -137,21 +139,9 @@ const AuthScreen: React.FC = () => { const onShow = (e: any) => { const kh = e?.endCoordinates?.height ?? 0; setKeyboardHeight(kh); - Animated.timing(headerHideAnim, { - toValue: 1, - duration: 180, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }).start(); }; const onHide = () => { setKeyboardHeight(0); - Animated.timing(headerHideAnim, { - toValue: 0, - duration: 180, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }).start(); }; const subShow = Keyboard.addListener(showEvt, onShow as any); const subHide = Keyboard.addListener(hideEvt, onHide as any); @@ -159,7 +149,7 @@ const AuthScreen: React.FC = () => { subShow.remove(); subHide.remove(); }; - }, [headerHideAnim]); + }, []); const isEmailValid = useMemo(() => /\S+@\S+\.\S+/.test(email.trim()), [email]); const isPasswordValid = useMemo(() => password.length >= 6, [password]); @@ -231,14 +221,10 @@ const AuthScreen: React.FC = () => { return ( - {Platform.OS !== 'android' ? ( - - ) : ( - - )} + {/* Background Pattern (iOS only) */} {Platform.OS !== 'android' && ( @@ -260,64 +246,55 @@ const AuthScreen: React.FC = () => { )} - {/* Header outside KeyboardAvoidingView to avoid being overlapped */} - setHeaderHeight(e.nativeEvent.layout.height)} - style={[ - styles.header, - { - opacity: Animated.multiply( - introOpacity, - headerHideAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }) - ), - transform: [ - { - translateY: Animated.add( - introTranslateY, - headerHideAnim.interpolate({ inputRange: [0, 1], outputRange: [0, -12] }) - ), - }, - ], - }, - ]} - > - {navigation.canGoBack() && ( - navigation.goBack()} style={[styles.backButton, Platform.OS === 'android' ? { top: Math.max(insets.top + 6, 18) } : null]} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> - - - )} - - {mode === 'signin' ? 'Welcome back' : 'Create your account'} - - - Sync your addons, progress and settings across devices - - + {navigation.canGoBack() && ( + navigation.goBack()} + style={[styles.backButton, { top: backButtonTop }]} + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} + > + + + )} 0 ? { - paddingTop: Platform.OS === 'ios' ? 6 : 10, - transform: [ - { - translateY: - Platform.OS === 'ios' - ? -Math.min(120, keyboardHeight * 0.35) - : -Math.min(84, keyboardHeight * 0.22), - }, - ], + justifyContent: 'flex-start', + paddingTop: Platform.OS === 'ios' ? 12 : safeTopInset + 8, } : null, ]} > - 0 ? styles.centerHeaderCompact : null, + { + opacity: introOpacity, + transform: [{ translateY: introTranslateY }], + }, + ]} + > + + {mode === 'signin' ? 'Welcome back' : 'Create your account'} + + {keyboardHeight === 0 && ( + + Sync your addons, progress and settings across devices + + )} + + + 0 ? styles.cardCompact : null, { backgroundColor: Platform.OS === 'android' ? '#121212' : 'rgba(255,255,255,0.02)', borderColor: Platform.OS === 'android' ? '#1f1f1f' : 'rgba(255,255,255,0.06)', ...(Platform.OS !== 'android' ? { @@ -601,9 +578,20 @@ const styles = StyleSheet.create({ }, header: { alignItems: 'center', - paddingTop: 64, paddingBottom: 8, }, + centerHeader: { + width: '100%', + alignItems: 'center', + marginBottom: 18, + paddingHorizontal: 20, + }, + centerHeaderTablet: { + maxWidth: 620, + }, + centerHeaderCompact: { + marginBottom: 10, + }, logoContainer: { position: 'relative', alignItems: 'center', @@ -639,11 +627,14 @@ const styles = StyleSheet.create({ centerContainer: { flex: 1, alignItems: 'center', - justifyContent: 'flex-start', + justifyContent: 'center', paddingHorizontal: 20, - paddingTop: 20, + paddingTop: 0, paddingBottom: 28, }, + centerContainerTablet: { + paddingHorizontal: 36, + }, card: { width: '100%', maxWidth: 400, @@ -652,6 +643,13 @@ const styles = StyleSheet.create({ borderWidth: 1, elevation: 20, }, + cardTablet: { + maxWidth: 560, + }, + cardCompact: { + padding: 18, + borderRadius: 16, + }, switchRow: { flexDirection: 'row', borderRadius: 14, @@ -739,7 +737,7 @@ const styles = StyleSheet.create({ backButton: { position: 'absolute', left: 16, - top: 8, + zIndex: 20, }, switchModeText: { textAlign: 'center', diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 5c54597a0..cd545c8f6 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -379,6 +379,23 @@ const SettingsScreen: React.FC = () => { case 'account': return ( + {showCloudSyncItem && ( + + } + renderControl={() => } + onPress={() => (navigation as any).navigate('SyncSettings')} + isLast={!showTraktItem && !showSimklItem} + isTablet={isTablet} + /> + )} {showTraktItem && ( { customIcon={} renderControl={() => } onPress={() => navigation.navigate('TraktSettings')} - isLast={!showSimklItem && !showCloudSyncItem} + isLast={!showSimklItem} isTablet={isTablet} /> )} @@ -397,17 +414,6 @@ const SettingsScreen: React.FC = () => { customIcon={} renderControl={() => } onPress={() => navigation.navigate('SimklSettings')} - isLast={!showCloudSyncItem} - isTablet={isTablet} - /> - )} - {showCloudSyncItem && ( - } - onPress={() => (navigation as any).navigate('SyncSettings')} isLast={true} isTablet={isTablet} /> @@ -698,6 +704,22 @@ const SettingsScreen: React.FC = () => { {/* Account */} {(settingsConfig?.categories?.['account']?.visible !== false) && (showTraktItem || showSimklItem || showCloudSyncItem) && ( + {showCloudSyncItem && ( + + } + renderControl={() => } + onPress={() => (navigation as any).navigate('SyncSettings')} + isLast={!showTraktItem && !showSimklItem} + /> + )} {showTraktItem && ( { customIcon={} renderControl={() => } onPress={() => navigation.navigate('TraktSettings')} - isLast={!showSimklItem && !showCloudSyncItem} + isLast={!showSimklItem} /> )} {showSimklItem && ( @@ -715,16 +737,6 @@ const SettingsScreen: React.FC = () => { customIcon={} renderControl={() => } onPress={() => navigation.navigate('SimklSettings')} - isLast={!showCloudSyncItem} - /> - )} - {showCloudSyncItem && ( - } - onPress={() => (navigation as any).navigate('SyncSettings')} isLast={true} /> )} @@ -952,7 +964,7 @@ const SettingsScreen: React.FC = () => { @@ -1222,6 +1234,14 @@ const styles = StyleSheet.create({ width: 180, height: 180, }, + syncLogoIcon: { + width: 20, + height: 20, + }, + syncLogoIconTablet: { + width: 24, + height: 24, + }, brandLogoContainer: { alignItems: 'center', justifyContent: 'center', diff --git a/src/screens/SyncSettingsScreen.tsx b/src/screens/SyncSettingsScreen.tsx index cc73b531b..78daebd6b 100644 --- a/src/screens/SyncSettingsScreen.tsx +++ b/src/screens/SyncSettingsScreen.tsx @@ -1,28 +1,37 @@ import React, { useCallback, useMemo, useState } from 'react'; import { ActivityIndicator, + SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TouchableOpacity, + useWindowDimensions, View, } from 'react-native'; import { NavigationProp, useFocusEffect, useNavigation } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialIcons } from '@expo/vector-icons'; import { RootStackParamList } from '../navigation/AppNavigator'; -import ScreenHeader from '../components/common/ScreenHeader'; import { useTheme } from '../contexts/ThemeContext'; import CustomAlert from '../components/CustomAlert'; import { supabaseSyncService, SupabaseUser, RemoteSyncStats } from '../services/supabaseSyncService'; import { useAccount } from '../contexts/AccountContext'; +import { useTraktContext } from '../contexts/TraktContext'; +import { useSimklContext } from '../contexts/SimklContext'; +import { useTranslation } from 'react-i18next'; const SyncSettingsScreen: React.FC = () => { const { currentTheme } = useTheme(); + const { t } = useTranslation(); + const { width } = useWindowDimensions(); + const isTablet = width >= 768; const navigation = useNavigation>(); const insets = useSafeAreaInsets(); const { user, signOut } = useAccount(); + const { isAuthenticated: traktAuthenticated } = useTraktContext(); + const { isAuthenticated: simklAuthenticated } = useSimklContext(); const [loading, setLoading] = useState(false); const [syncCodeLoading, setSyncCodeLoading] = useState(false); @@ -84,6 +93,15 @@ const SyncSettingsScreen: React.FC = () => { { label: 'Linked Devices', value: remoteStats.linkedDevices }, ]; }, [remoteStats]); + const isSignedIn = Boolean(user); + const externalSyncServices = useMemo( + () => [ + traktAuthenticated ? 'Trakt' : null, + simklAuthenticated ? 'Simkl' : null, + ].filter(Boolean) as string[], + [traktAuthenticated, simklAuthenticated] + ); + const externalSyncActive = externalSyncServices.length > 0; const handleManualSync = async () => { setSyncCodeLoading(true); @@ -123,24 +141,26 @@ const SyncSettingsScreen: React.FC = () => { } }; - if (loading) { - return ( - - - navigation.goBack()} /> + return ( + + + + navigation.goBack()} style={styles.backButton}> + + {t('settings.title')} + + + + Nuvio Sync + + {loading ? ( - - ); - } + ) : ( + <> - return ( - - - navigation.goBack()} /> - - + @@ -152,6 +172,18 @@ const SyncSettingsScreen: React.FC = () => { + + + + External Sync Priority + + + {externalSyncActive + ? `${externalSyncServices.join(' + ')} is active. Watch progress and library updates are managed by these services instead of Nuvio cloud database.` + : 'If Trakt or Simkl sync is enabled, watch progress and library updates will use those services instead of Nuvio cloud database.'} + + + @@ -161,7 +193,7 @@ const SyncSettingsScreen: React.FC = () => { {user?.email ? `Signed in as ${user.email}` : 'Not signed in'} - {!user ? ( + {!isSignedIn ? ( navigation.navigate('Account')} @@ -188,83 +220,109 @@ const SyncSettingsScreen: React.FC = () => { - - - - Connection - - {authLabel} - - Effective owner: {ownerId || 'Unavailable'} - - {!supabaseSyncService.isConfigured() && ( - - Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync. - - )} - - - - - - Database Stats - - {!remoteStats ? ( - - Sign in to load remote data counts. - - ) : ( - - {statItems.map((item) => ( - - {item.value} - {item.label} - - ))} + {!isSignedIn ? ( + + + + Before You Sync - )} - - - - - - Actions + + Sign in to start cloud sync and keep your data consistent across devices. + + + • Addons and plugin settings + • Watch progress and library + • Linked devices and sync stats + + {!supabaseSyncService.isConfigured() && ( + + Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync. + + )} - - Pull to refresh this device from cloud, or upload this device as the latest source. - - - - {syncCodeLoading ? ( - - ) : ( - Pull From Cloud + ) : ( + <> + + + + Connection + + {authLabel} + + Effective owner: {ownerId || 'Unavailable'} + + {!supabaseSyncService.isConfigured() && ( + + Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync. + )} - - - Upload This Device - - - + + + + + + Database Stats + + {!remoteStats ? ( + + Sign in to load remote data counts. + + ) : ( + + {statItems.map((item) => ( + + {item.value} + {item.label} + + ))} + + )} + + + + + + Actions + + + Pull to refresh this device from cloud, or upload this device as the latest source. + + + + {syncCodeLoading ? ( + + ) : ( + Pull From Cloud + )} + + + Upload This Device + + + + + )} + + )} { actions={alertActions} onClose={() => setAlertVisible(false)} /> - + ); }; @@ -286,10 +344,42 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingTop: 8, + }, + backButton: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + }, + backText: { + marginLeft: 8, + fontSize: 16, + fontWeight: '600', + }, + headerActions: { + minWidth: 32, + }, + screenTitle: { + fontSize: 32, + fontWeight: '800', + paddingHorizontal: 16, + marginTop: 4, + marginBottom: 10, + }, content: { padding: 16, gap: 14, }, + contentTablet: { + alignSelf: 'center', + width: '100%', + maxWidth: 980, + }, heroCard: { borderWidth: 1, borderRadius: 16, @@ -318,6 +408,23 @@ const styles = StyleSheet.create({ padding: 14, gap: 10, }, + noteCard: { + borderWidth: 1, + borderRadius: 14, + padding: 14, + gap: 8, + }, + preAuthCard: { + gap: 12, + }, + preAuthList: { + gap: 6, + marginTop: 2, + }, + preAuthItem: { + fontSize: 13, + lineHeight: 18, + }, sectionHeader: { flexDirection: 'row', alignItems: 'center', diff --git a/src/screens/settings/AboutSettingsScreen.tsx b/src/screens/settings/AboutSettingsScreen.tsx index 04e3d3d48..18f39bb19 100644 --- a/src/screens/settings/AboutSettingsScreen.tsx +++ b/src/screens/settings/AboutSettingsScreen.tsx @@ -391,7 +391,7 @@ export const AboutFooter: React.FC<{ displayDownloads: number | null }> = ({ dis