Add Nuvio Sync feature and update branding assets

This commit is contained in:
tapframe 2026-02-17 03:31:29 +05:30
parent 575382a629
commit 46fe7f7cdf
6 changed files with 313 additions and 188 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/text_only_og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; 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 { mmkvStorage } from '../services/mmkvStorage';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
@ -10,8 +10,8 @@ import * as Haptics from 'expo-haptics';
import { useToast } from '../contexts/ToastContext'; import { useToast } from '../contexts/ToastContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
const { width, height } = Dimensions.get('window');
const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__'; const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__';
const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24'];
const normalizeAuthErrorMessage = (input: string): string => { const normalizeAuthErrorMessage = (input: string): string => {
const raw = (input || '').trim(); const raw = (input || '').trim();
@ -40,12 +40,16 @@ const normalizeAuthErrorMessage = (input: string): string => {
}; };
const AuthScreen: React.FC = () => { const AuthScreen: React.FC = () => {
const { width, height } = useWindowDimensions();
const isTablet = width >= 768;
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { signIn, signUp } = useAccount(); const { signIn, signUp } = useAccount();
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const route = useRoute<any>(); const route = useRoute<any>();
const fromOnboarding = !!route?.params?.fromOnboarding; const fromOnboarding = !!route?.params?.fromOnboarding;
const insets = useSafeAreaInsets(); 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 { showError, showSuccess } = useToast();
const [email, setEmail] = useState(''); 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 modeAnim = useRef(new Animated.Value(0)).current; // 0 = signin, 1 = signup
const [switchWidth, setSwitchWidth] = useState(0); const [switchWidth, setSwitchWidth] = useState(0);
// Legacy local toast state removed in favor of global toast // 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); const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => { useEffect(() => {
@ -137,21 +139,9 @@ const AuthScreen: React.FC = () => {
const onShow = (e: any) => { const onShow = (e: any) => {
const kh = e?.endCoordinates?.height ?? 0; const kh = e?.endCoordinates?.height ?? 0;
setKeyboardHeight(kh); setKeyboardHeight(kh);
Animated.timing(headerHideAnim, {
toValue: 1,
duration: 180,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}).start();
}; };
const onHide = () => { const onHide = () => {
setKeyboardHeight(0); 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 subShow = Keyboard.addListener(showEvt, onShow as any);
const subHide = Keyboard.addListener(hideEvt, onHide as any); const subHide = Keyboard.addListener(hideEvt, onHide as any);
@ -159,7 +149,7 @@ const AuthScreen: React.FC = () => {
subShow.remove(); subShow.remove();
subHide.remove(); subHide.remove();
}; };
}, [headerHideAnim]); }, []);
const isEmailValid = useMemo(() => /\S+@\S+\.\S+/.test(email.trim()), [email]); const isEmailValid = useMemo(() => /\S+@\S+\.\S+/.test(email.trim()), [email]);
const isPasswordValid = useMemo(() => password.length >= 6, [password]); const isPasswordValid = useMemo(() => password.length >= 6, [password]);
@ -231,14 +221,10 @@ const AuthScreen: React.FC = () => {
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
{Platform.OS !== 'android' ? ( <LinearGradient
<LinearGradient colors={AUTH_BG_GRADIENT}
colors={['#0D1117', '#161B22', '#21262D']} style={StyleSheet.absoluteFill}
style={StyleSheet.absoluteFill} />
/>
) : (
<View style={[StyleSheet.absoluteFill, { backgroundColor: '#0D1117' }]} />
)}
{/* Background Pattern (iOS only) */} {/* Background Pattern (iOS only) */}
{Platform.OS !== 'android' && ( {Platform.OS !== 'android' && (
@ -260,64 +246,55 @@ const AuthScreen: React.FC = () => {
)} )}
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView style={{ flex: 1 }}>
{/* Header outside KeyboardAvoidingView to avoid being overlapped */} {navigation.canGoBack() && (
<Animated.View <TouchableOpacity
onLayout={(e) => setHeaderHeight(e.nativeEvent.layout.height)} onPress={() => navigation.goBack()}
style={[ style={[styles.backButton, { top: backButtonTop }]}
styles.header, hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
{ >
opacity: Animated.multiply( <MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} />
introOpacity, </TouchableOpacity>
headerHideAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }) )}
),
transform: [
{
translateY: Animated.add(
introTranslateY,
headerHideAnim.interpolate({ inputRange: [0, 1], outputRange: [0, -12] })
),
},
],
},
]}
>
{navigation.canGoBack() && (
<TouchableOpacity onPress={() => 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 }}>
<MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} />
</TouchableOpacity>
)}
<Animated.Text style={[styles.heading, { color: currentTheme.colors.white, opacity: titleOpacity, transform: [{ translateY: titleTranslateY }] }]}>
{mode === 'signin' ? 'Welcome back' : 'Create your account'}
</Animated.Text>
<Text style={[styles.subheading, { color: currentTheme.colors.textMuted }] }>
Sync your addons, progress and settings across devices
</Text>
</Animated.View>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? headerHeight : 0} keyboardVerticalOffset={0}
> >
<Animated.View <Animated.View
style={[ style={[
styles.centerContainer, styles.centerContainer,
isTablet ? styles.centerContainerTablet : null,
keyboardHeight > 0 keyboardHeight > 0
? { ? {
paddingTop: Platform.OS === 'ios' ? 6 : 10, justifyContent: 'flex-start',
transform: [ paddingTop: Platform.OS === 'ios' ? 12 : safeTopInset + 8,
{
translateY:
Platform.OS === 'ios'
? -Math.min(120, keyboardHeight * 0.35)
: -Math.min(84, keyboardHeight * 0.22),
},
],
} }
: null, : null,
]} ]}
> >
<Animated.View style={[styles.card, { <Animated.View
style={[
styles.centerHeader,
isTablet ? styles.centerHeaderTablet : null,
keyboardHeight > 0 ? styles.centerHeaderCompact : null,
{
opacity: introOpacity,
transform: [{ translateY: introTranslateY }],
},
]}
>
<Animated.Text style={[styles.heading, { color: currentTheme.colors.white, opacity: titleOpacity, transform: [{ translateY: titleTranslateY }] }]}>
{mode === 'signin' ? 'Welcome back' : 'Create your account'}
</Animated.Text>
{keyboardHeight === 0 && (
<Text style={[styles.subheading, { color: currentTheme.colors.textMuted }]}>
Sync your addons, progress and settings across devices
</Text>
)}
</Animated.View>
<Animated.View style={[styles.card, isTablet ? styles.cardTablet : null, keyboardHeight > 0 ? styles.cardCompact : null, {
backgroundColor: Platform.OS === 'android' ? '#121212' : 'rgba(255,255,255,0.02)', backgroundColor: Platform.OS === 'android' ? '#121212' : 'rgba(255,255,255,0.02)',
borderColor: Platform.OS === 'android' ? '#1f1f1f' : 'rgba(255,255,255,0.06)', borderColor: Platform.OS === 'android' ? '#1f1f1f' : 'rgba(255,255,255,0.06)',
...(Platform.OS !== 'android' ? { ...(Platform.OS !== 'android' ? {
@ -601,9 +578,20 @@ const styles = StyleSheet.create({
}, },
header: { header: {
alignItems: 'center', alignItems: 'center',
paddingTop: 64,
paddingBottom: 8, paddingBottom: 8,
}, },
centerHeader: {
width: '100%',
alignItems: 'center',
marginBottom: 18,
paddingHorizontal: 20,
},
centerHeaderTablet: {
maxWidth: 620,
},
centerHeaderCompact: {
marginBottom: 10,
},
logoContainer: { logoContainer: {
position: 'relative', position: 'relative',
alignItems: 'center', alignItems: 'center',
@ -639,11 +627,14 @@ const styles = StyleSheet.create({
centerContainer: { centerContainer: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: 'flex-start', justifyContent: 'center',
paddingHorizontal: 20, paddingHorizontal: 20,
paddingTop: 20, paddingTop: 0,
paddingBottom: 28, paddingBottom: 28,
}, },
centerContainerTablet: {
paddingHorizontal: 36,
},
card: { card: {
width: '100%', width: '100%',
maxWidth: 400, maxWidth: 400,
@ -652,6 +643,13 @@ const styles = StyleSheet.create({
borderWidth: 1, borderWidth: 1,
elevation: 20, elevation: 20,
}, },
cardTablet: {
maxWidth: 560,
},
cardCompact: {
padding: 18,
borderRadius: 16,
},
switchRow: { switchRow: {
flexDirection: 'row', flexDirection: 'row',
borderRadius: 14, borderRadius: 14,
@ -739,7 +737,7 @@ const styles = StyleSheet.create({
backButton: { backButton: {
position: 'absolute', position: 'absolute',
left: 16, left: 16,
top: 8, zIndex: 20,
}, },
switchModeText: { switchModeText: {
textAlign: 'center', textAlign: 'center',

View file

@ -379,6 +379,23 @@ const SettingsScreen: React.FC = () => {
case 'account': case 'account':
return ( return (
<SettingsCard title={t('settings.sections.account')} isTablet={isTablet}> <SettingsCard title={t('settings.sections.account')} isTablet={isTablet}>
{showCloudSyncItem && (
<SettingItem
title="Nuvio Sync"
description="Sync data across your Nuvio devices"
customIcon={
<FastImage
source={require('../../assets/nuvio-sync-icon-og.png')}
style={[styles.syncLogoIcon, isTablet ? styles.syncLogoIconTablet : null]}
resizeMode={FastImage.resizeMode.contain}
/>
}
renderControl={() => <ChevronRight />}
onPress={() => (navigation as any).navigate('SyncSettings')}
isLast={!showTraktItem && !showSimklItem}
isTablet={isTablet}
/>
)}
{showTraktItem && ( {showTraktItem && (
<SettingItem <SettingItem
title={t('trakt.title')} title={t('trakt.title')}
@ -386,7 +403,7 @@ const SettingsScreen: React.FC = () => {
customIcon={<TraktIcon size={isTablet ? 24 : 20} />} customIcon={<TraktIcon size={isTablet ? 24 : 20} />}
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('TraktSettings')} onPress={() => navigation.navigate('TraktSettings')}
isLast={!showSimklItem && !showCloudSyncItem} isLast={!showSimklItem}
isTablet={isTablet} isTablet={isTablet}
/> />
)} )}
@ -397,17 +414,6 @@ const SettingsScreen: React.FC = () => {
customIcon={<SimklIcon size={isTablet ? 24 : 20} />} customIcon={<SimklIcon size={isTablet ? 24 : 20} />}
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('SimklSettings')} onPress={() => navigation.navigate('SimklSettings')}
isLast={!showCloudSyncItem}
isTablet={isTablet}
/>
)}
{showCloudSyncItem && (
<SettingItem
title="Nuvio Sync"
description="Sync data across your Nuvio devices"
icon="refresh-cw"
renderControl={() => <ChevronRight />}
onPress={() => (navigation as any).navigate('SyncSettings')}
isLast={true} isLast={true}
isTablet={isTablet} isTablet={isTablet}
/> />
@ -698,6 +704,22 @@ const SettingsScreen: React.FC = () => {
{/* Account */} {/* Account */}
{(settingsConfig?.categories?.['account']?.visible !== false) && (showTraktItem || showSimklItem || showCloudSyncItem) && ( {(settingsConfig?.categories?.['account']?.visible !== false) && (showTraktItem || showSimklItem || showCloudSyncItem) && (
<SettingsCard title={t('settings.account').toUpperCase()}> <SettingsCard title={t('settings.account').toUpperCase()}>
{showCloudSyncItem && (
<SettingItem
title="Nuvio Sync"
description="Sync data across your Nuvio devices"
customIcon={
<FastImage
source={require('../../assets/nuvio-sync-icon-og.png')}
style={styles.syncLogoIcon}
resizeMode={FastImage.resizeMode.contain}
/>
}
renderControl={() => <ChevronRight />}
onPress={() => (navigation as any).navigate('SyncSettings')}
isLast={!showTraktItem && !showSimklItem}
/>
)}
{showTraktItem && ( {showTraktItem && (
<SettingItem <SettingItem
title={t('trakt.title')} title={t('trakt.title')}
@ -705,7 +727,7 @@ const SettingsScreen: React.FC = () => {
customIcon={<TraktIcon size={20} />} customIcon={<TraktIcon size={20} />}
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('TraktSettings')} onPress={() => navigation.navigate('TraktSettings')}
isLast={!showSimklItem && !showCloudSyncItem} isLast={!showSimklItem}
/> />
)} )}
{showSimklItem && ( {showSimklItem && (
@ -715,16 +737,6 @@ const SettingsScreen: React.FC = () => {
customIcon={<SimklIcon size={20} />} customIcon={<SimklIcon size={20} />}
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
onPress={() => navigation.navigate('SimklSettings')} onPress={() => navigation.navigate('SimklSettings')}
isLast={!showCloudSyncItem}
/>
)}
{showCloudSyncItem && (
<SettingItem
title="Nuvio Sync"
description="Sync data across your Nuvio devices"
icon="refresh-cw"
renderControl={() => <ChevronRight />}
onPress={() => (navigation as any).navigate('SyncSettings')}
isLast={true} isLast={true}
/> />
)} )}
@ -952,7 +964,7 @@ const SettingsScreen: React.FC = () => {
<View style={styles.brandLogoContainer}> <View style={styles.brandLogoContainer}>
<FastImage <FastImage
source={require('../../assets/nuviotext.png')} source={require('../../assets/text_only_og.png')}
style={styles.brandLogo} style={styles.brandLogo}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
@ -1222,6 +1234,14 @@ const styles = StyleSheet.create({
width: 180, width: 180,
height: 180, height: 180,
}, },
syncLogoIcon: {
width: 20,
height: 20,
},
syncLogoIconTablet: {
width: 24,
height: 24,
},
brandLogoContainer: { brandLogoContainer: {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',

View file

@ -1,28 +1,37 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
SafeAreaView,
ScrollView, ScrollView,
StatusBar, StatusBar,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
useWindowDimensions,
View, View,
} from 'react-native'; } from 'react-native';
import { NavigationProp, useFocusEffect, useNavigation } from '@react-navigation/native'; import { NavigationProp, useFocusEffect, useNavigation } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import ScreenHeader from '../components/common/ScreenHeader';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import { supabaseSyncService, SupabaseUser, RemoteSyncStats } from '../services/supabaseSyncService'; import { supabaseSyncService, SupabaseUser, RemoteSyncStats } from '../services/supabaseSyncService';
import { useAccount } from '../contexts/AccountContext'; 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 SyncSettingsScreen: React.FC = () => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { t } = useTranslation();
const { width } = useWindowDimensions();
const isTablet = width >= 768;
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { user, signOut } = useAccount(); const { user, signOut } = useAccount();
const { isAuthenticated: traktAuthenticated } = useTraktContext();
const { isAuthenticated: simklAuthenticated } = useSimklContext();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [syncCodeLoading, setSyncCodeLoading] = useState(false); const [syncCodeLoading, setSyncCodeLoading] = useState(false);
@ -84,6 +93,15 @@ const SyncSettingsScreen: React.FC = () => {
{ label: 'Linked Devices', value: remoteStats.linkedDevices }, { label: 'Linked Devices', value: remoteStats.linkedDevices },
]; ];
}, [remoteStats]); }, [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 () => { const handleManualSync = async () => {
setSyncCodeLoading(true); setSyncCodeLoading(true);
@ -123,24 +141,26 @@ const SyncSettingsScreen: React.FC = () => {
} }
}; };
if (loading) { return (
return ( <SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <StatusBar barStyle="light-content" />
<StatusBar barStyle="light-content" /> <View style={styles.header}>
<ScreenHeader title="Nuvio Sync" showBackButton onBackPress={() => navigation.goBack()} /> <TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
<Text style={[styles.backText, { color: currentTheme.colors.highEmphasis }]}>{t('settings.title')}</Text>
</TouchableOpacity>
<View style={styles.headerActions} />
</View>
<Text style={[styles.screenTitle, { color: currentTheme.colors.highEmphasis }]}>Nuvio Sync</Text>
{loading ? (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator color={currentTheme.colors.primary} size="large" /> <ActivityIndicator color={currentTheme.colors.primary} size="large" />
</View> </View>
</View> ) : (
); <>
}
return ( <ScrollView contentContainerStyle={[styles.content, isTablet ? styles.contentTablet : null, { paddingBottom: insets.bottom + 24 }]}>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
<ScreenHeader title="Nuvio Sync" showBackButton onBackPress={() => navigation.goBack()} />
<ScrollView contentContainerStyle={[styles.content, { paddingBottom: insets.bottom + 24 }]}>
<View style={[styles.heroCard, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}> <View style={[styles.heroCard, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
<View style={styles.heroTopRow}> <View style={styles.heroTopRow}>
<View style={styles.heroTitleWrap}> <View style={styles.heroTitleWrap}>
@ -152,6 +172,18 @@ const SyncSettingsScreen: React.FC = () => {
</View> </View>
</View> </View>
<View style={[styles.noteCard, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
<View style={styles.sectionHeader}>
<MaterialIcons name="info-outline" size={18} color={currentTheme.colors.highEmphasis} />
<Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>External Sync Priority</Text>
</View>
<Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>
{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.'}
</Text>
</View>
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}> <View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>
<MaterialIcons name="person-outline" size={18} color={currentTheme.colors.highEmphasis} /> <MaterialIcons name="person-outline" size={18} color={currentTheme.colors.highEmphasis} />
@ -161,7 +193,7 @@ const SyncSettingsScreen: React.FC = () => {
{user?.email ? `Signed in as ${user.email}` : 'Not signed in'} {user?.email ? `Signed in as ${user.email}` : 'Not signed in'}
</Text> </Text>
<View style={styles.buttonRow}> <View style={styles.buttonRow}>
{!user ? ( {!isSignedIn ? (
<TouchableOpacity <TouchableOpacity
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]} style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Account')} onPress={() => navigation.navigate('Account')}
@ -188,83 +220,109 @@ const SyncSettingsScreen: React.FC = () => {
</View> </View>
</View> </View>
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}> {!isSignedIn ? (
<View style={styles.sectionHeader}> <View style={[styles.card, styles.preAuthCard, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
<MaterialIcons name="link" size={18} color={currentTheme.colors.highEmphasis} /> <View style={styles.sectionHeader}>
<Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Connection</Text> <MaterialIcons name="sync-lock" size={18} color={currentTheme.colors.highEmphasis} />
</View> <Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Before You Sync</Text>
<Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>{authLabel}</Text>
<Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>
Effective owner: {ownerId || 'Unavailable'}
</Text>
{!supabaseSyncService.isConfigured() && (
<Text style={[styles.warning, { color: '#ffb454' }]}>
Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync.
</Text>
)}
</View>
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
<View style={styles.sectionHeader}>
<MaterialIcons name="storage" size={18} color={currentTheme.colors.highEmphasis} />
<Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Database Stats</Text>
</View>
{!remoteStats ? (
<Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>
Sign in to load remote data counts.
</Text>
) : (
<View style={styles.statsGrid}>
{statItems.map((item) => (
<View key={item.label} style={[styles.statTile, { backgroundColor: currentTheme.colors.darkBackground, borderColor: currentTheme.colors.elevation2 }]}>
<Text style={[styles.statValue, { color: currentTheme.colors.highEmphasis }]}>{item.value}</Text>
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>{item.label}</Text>
</View>
))}
</View> </View>
)} <Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>
</View> Sign in to start cloud sync and keep your data consistent across devices.
</Text>
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}> <View style={styles.preAuthList}>
<View style={styles.sectionHeader}> <Text style={[styles.preAuthItem, { color: currentTheme.colors.mediumEmphasis }]}> Addons and plugin settings</Text>
<MaterialIcons name="sync" size={18} color={currentTheme.colors.highEmphasis} /> <Text style={[styles.preAuthItem, { color: currentTheme.colors.mediumEmphasis }]}> Watch progress and library</Text>
<Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Actions</Text> <Text style={[styles.preAuthItem, { color: currentTheme.colors.mediumEmphasis }]}> Linked devices and sync stats</Text>
</View>
{!supabaseSyncService.isConfigured() && (
<Text style={[styles.warning, { color: '#ffb454' }]}>
Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync.
</Text>
)}
</View> </View>
<Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}> ) : (
Pull to refresh this device from cloud, or upload this device as the latest source. <>
</Text> <View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
<View style={styles.buttonRow}> <View style={styles.sectionHeader}>
<TouchableOpacity <MaterialIcons name="link" size={18} color={currentTheme.colors.highEmphasis} />
disabled={syncCodeLoading || !supabaseSyncService.isConfigured()} <Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Connection</Text>
style={[ </View>
styles.button, <Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>{authLabel}</Text>
styles.primaryButton, <Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>
{ backgroundColor: currentTheme.colors.primary }, Effective owner: {ownerId || 'Unavailable'}
(syncCodeLoading || !supabaseSyncService.isConfigured()) && styles.buttonDisabled, </Text>
]} {!supabaseSyncService.isConfigured() && (
onPress={handleManualSync} <Text style={[styles.warning, { color: '#ffb454' }]}>
> Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync.
{syncCodeLoading ? ( </Text>
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Pull From Cloud</Text>
)} )}
</TouchableOpacity> </View>
<TouchableOpacity
disabled={syncCodeLoading || !supabaseSyncService.isConfigured()} <View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
style={[ <View style={styles.sectionHeader}>
styles.button, <MaterialIcons name="storage" size={18} color={currentTheme.colors.highEmphasis} />
styles.secondaryButton, <Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Database Stats</Text>
{ backgroundColor: currentTheme.colors.elevation2, borderColor: currentTheme.colors.elevation2 }, </View>
(syncCodeLoading || !supabaseSyncService.isConfigured()) && styles.buttonDisabled, {!remoteStats ? (
]} <Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>
onPress={handleUploadLocalData} Sign in to load remote data counts.
> </Text>
<Text style={styles.buttonText}>Upload This Device</Text> ) : (
</TouchableOpacity> <View style={styles.statsGrid}>
</View> {statItems.map((item) => (
</View> <View key={item.label} style={[styles.statTile, { backgroundColor: currentTheme.colors.darkBackground, borderColor: currentTheme.colors.elevation2 }]}>
<Text style={[styles.statValue, { color: currentTheme.colors.highEmphasis }]}>{item.value}</Text>
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>{item.label}</Text>
</View>
))}
</View>
)}
</View>
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
<View style={styles.sectionHeader}>
<MaterialIcons name="sync" size={18} color={currentTheme.colors.highEmphasis} />
<Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Actions</Text>
</View>
<Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>
Pull to refresh this device from cloud, or upload this device as the latest source.
</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
disabled={syncCodeLoading || !supabaseSyncService.isConfigured()}
style={[
styles.button,
styles.primaryButton,
{ backgroundColor: currentTheme.colors.primary },
(syncCodeLoading || !supabaseSyncService.isConfigured()) && styles.buttonDisabled,
]}
onPress={handleManualSync}
>
{syncCodeLoading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Pull From Cloud</Text>
)}
</TouchableOpacity>
<TouchableOpacity
disabled={syncCodeLoading || !supabaseSyncService.isConfigured()}
style={[
styles.button,
styles.secondaryButton,
{ backgroundColor: currentTheme.colors.elevation2, borderColor: currentTheme.colors.elevation2 },
(syncCodeLoading || !supabaseSyncService.isConfigured()) && styles.buttonDisabled,
]}
onPress={handleUploadLocalData}
>
<Text style={styles.buttonText}>Upload This Device</Text>
</TouchableOpacity>
</View>
</View>
</>
)}
</ScrollView> </ScrollView>
</>
)}
<CustomAlert <CustomAlert
visible={alertVisible} visible={alertVisible}
@ -273,7 +331,7 @@ const SyncSettingsScreen: React.FC = () => {
actions={alertActions} actions={alertActions}
onClose={() => setAlertVisible(false)} onClose={() => setAlertVisible(false)}
/> />
</View> </SafeAreaView>
); );
}; };
@ -286,10 +344,42 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: '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: { content: {
padding: 16, padding: 16,
gap: 14, gap: 14,
}, },
contentTablet: {
alignSelf: 'center',
width: '100%',
maxWidth: 980,
},
heroCard: { heroCard: {
borderWidth: 1, borderWidth: 1,
borderRadius: 16, borderRadius: 16,
@ -318,6 +408,23 @@ const styles = StyleSheet.create({
padding: 14, padding: 14,
gap: 10, 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: { sectionHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',

View file

@ -391,7 +391,7 @@ export const AboutFooter: React.FC<{ displayDownloads: number | null }> = ({ dis
<View style={styles.brandLogoContainer}> <View style={styles.brandLogoContainer}>
<FastImage <FastImage
source={require('../../../assets/nuviotext.png')} source={require('../../../assets/text_only_og.png')}
style={styles.brandLogo} style={styles.brandLogo}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />