mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 09:35:42 +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 { 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<any>();
|
||||
const route = useRoute<any>();
|
||||
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 (
|
||||
<View style={{ flex: 1 }}>
|
||||
{Platform.OS !== 'android' ? (
|
||||
<LinearGradient
|
||||
colors={['#0D1117', '#161B22', '#21262D']}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
) : (
|
||||
<View style={[StyleSheet.absoluteFill, { backgroundColor: '#0D1117' }]} />
|
||||
)}
|
||||
<LinearGradient
|
||||
colors={AUTH_BG_GRADIENT}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
|
||||
{/* Background Pattern (iOS only) */}
|
||||
{Platform.OS !== 'android' && (
|
||||
|
|
@ -260,64 +246,55 @@ const AuthScreen: React.FC = () => {
|
|||
)}
|
||||
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
{/* Header outside KeyboardAvoidingView to avoid being overlapped */}
|
||||
<Animated.View
|
||||
onLayout={(e) => 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() && (
|
||||
<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>
|
||||
{navigation.canGoBack() && (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={[styles.backButton, { top: backButtonTop }]}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? headerHeight : 0}
|
||||
keyboardVerticalOffset={0}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.centerContainer,
|
||||
isTablet ? styles.centerContainerTablet : null,
|
||||
keyboardHeight > 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,
|
||||
]}
|
||||
>
|
||||
<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)',
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -379,6 +379,23 @@ const SettingsScreen: React.FC = () => {
|
|||
case 'account':
|
||||
return (
|
||||
<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 && (
|
||||
<SettingItem
|
||||
title={t('trakt.title')}
|
||||
|
|
@ -386,7 +403,7 @@ const SettingsScreen: React.FC = () => {
|
|||
customIcon={<TraktIcon size={isTablet ? 24 : 20} />}
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('TraktSettings')}
|
||||
isLast={!showSimklItem && !showCloudSyncItem}
|
||||
isLast={!showSimklItem}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -397,17 +414,6 @@ const SettingsScreen: React.FC = () => {
|
|||
customIcon={<SimklIcon size={isTablet ? 24 : 20} />}
|
||||
renderControl={() => <ChevronRight />}
|
||||
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}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
|
|
@ -698,6 +704,22 @@ const SettingsScreen: React.FC = () => {
|
|||
{/* Account */}
|
||||
{(settingsConfig?.categories?.['account']?.visible !== false) && (showTraktItem || showSimklItem || showCloudSyncItem) && (
|
||||
<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 && (
|
||||
<SettingItem
|
||||
title={t('trakt.title')}
|
||||
|
|
@ -705,7 +727,7 @@ const SettingsScreen: React.FC = () => {
|
|||
customIcon={<TraktIcon size={20} />}
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('TraktSettings')}
|
||||
isLast={!showSimklItem && !showCloudSyncItem}
|
||||
isLast={!showSimklItem}
|
||||
/>
|
||||
)}
|
||||
{showSimklItem && (
|
||||
|
|
@ -715,16 +737,6 @@ const SettingsScreen: React.FC = () => {
|
|||
customIcon={<SimklIcon size={20} />}
|
||||
renderControl={() => <ChevronRight />}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -952,7 +964,7 @@ const SettingsScreen: React.FC = () => {
|
|||
|
||||
<View style={styles.brandLogoContainer}>
|
||||
<FastImage
|
||||
source={require('../../assets/nuviotext.png')}
|
||||
source={require('../../assets/text_only_og.png')}
|
||||
style={styles.brandLogo}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<NavigationProp<RootStackParamList>>();
|
||||
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 (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<ScreenHeader title="Nuvio Sync" showBackButton onBackPress={() => navigation.goBack()} />
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.header}>
|
||||
<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}>
|
||||
<ActivityIndicator color={currentTheme.colors.primary} size="large" />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
) : (
|
||||
<>
|
||||
|
||||
return (
|
||||
<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 }]}>
|
||||
<ScrollView contentContainerStyle={[styles.content, isTablet ? styles.contentTablet : null, { paddingBottom: insets.bottom + 24 }]}>
|
||||
<View style={[styles.heroCard, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.heroTopRow}>
|
||||
<View style={styles.heroTitleWrap}>
|
||||
|
|
@ -152,6 +172,18 @@ const SyncSettingsScreen: React.FC = () => {
|
|||
</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.sectionHeader}>
|
||||
<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'}
|
||||
</Text>
|
||||
<View style={styles.buttonRow}>
|
||||
{!user ? (
|
||||
{!isSignedIn ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => navigation.navigate('Account')}
|
||||
|
|
@ -188,83 +220,109 @@ const SyncSettingsScreen: React.FC = () => {
|
|||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<MaterialIcons name="link" size={18} color={currentTheme.colors.highEmphasis} />
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Connection</Text>
|
||||
</View>
|
||||
<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>
|
||||
))}
|
||||
{!isSignedIn ? (
|
||||
<View style={[styles.card, styles.preAuthCard, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<MaterialIcons name="sync-lock" size={18} color={currentTheme.colors.highEmphasis} />
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Before You Sync</Text>
|
||||
</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>
|
||||
<Text style={[styles.cardText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Sign in to start cloud sync and keep your data consistent across devices.
|
||||
</Text>
|
||||
<View style={styles.preAuthList}>
|
||||
<Text style={[styles.preAuthItem, { color: currentTheme.colors.mediumEmphasis }]}>• Addons and plugin settings</Text>
|
||||
<Text style={[styles.preAuthItem, { color: currentTheme.colors.mediumEmphasis }]}>• Watch progress and library</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>
|
||||
<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>
|
||||
) : (
|
||||
<>
|
||||
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<MaterialIcons name="link" size={18} color={currentTheme.colors.highEmphasis} />
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.highEmphasis }]}>Connection</Text>
|
||||
</View>
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
|
|
@ -273,7 +331,7 @@ const SyncSettingsScreen: React.FC = () => {
|
|||
actions={alertActions}
|
||||
onClose={() => setAlertVisible(false)}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ export const AboutFooter: React.FC<{ displayDownloads: number | null }> = ({ dis
|
|||
|
||||
<View style={styles.brandLogoContainer}>
|
||||
<FastImage
|
||||
source={require('../../../assets/nuviotext.png')}
|
||||
source={require('../../../assets/text_only_og.png')}
|
||||
style={styles.brandLogo}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue