mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Add Nuvio Sync feature and update branding assets
This commit is contained in:
parent
575382a629
commit
46fe7f7cdf
6 changed files with 313 additions and 188 deletions
BIN
assets/nuvio-sync-icon-og.png
Normal file
BIN
assets/nuvio-sync-icon-og.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
assets/text_only_og.png
Normal file
BIN
assets/text_only_og.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue