mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
local backup improvements
This commit is contained in:
parent
88313e6d06
commit
60e27da57d
5 changed files with 320 additions and 33 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import * as Updates from 'expo-updates';
|
||||
import { backupService, BackupOptions } from '../services/backupService';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { logger } from '../utils/logger';
|
||||
|
|
@ -40,6 +41,20 @@ const BackupRestoreSettings: React.FC<BackupRestoreSettingsProps> = ({ isTablet
|
|||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
const restartApp = async () => {
|
||||
try {
|
||||
await Updates.reloadAsync();
|
||||
} catch (error) {
|
||||
logger.error('[BackupRestoreSettings] Failed to restart app:', error);
|
||||
// Fallback: show error message
|
||||
openAlert(
|
||||
'Restart Failed',
|
||||
'Failed to restart the app. Please manually close and reopen the app to see your restored data.',
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create backup
|
||||
const handleCreateBackup = useCallback(async () => {
|
||||
|
|
@ -162,7 +177,14 @@ const BackupRestoreSettings: React.FC<BackupRestoreSettingsProps> = ({ isTablet
|
|||
openAlert(
|
||||
'Restore Complete',
|
||||
'Your data has been successfully restored. Please restart the app to see all changes.',
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{
|
||||
label: 'Restart App',
|
||||
onPress: restartApp,
|
||||
style: { fontWeight: 'bold' }
|
||||
}
|
||||
]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[BackupRestoreSettings] Failed to restore backup:', error);
|
||||
|
|
|
|||
|
|
@ -26,8 +26,11 @@ const AuthScreen: React.FC = () => {
|
|||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [mode, setMode] = useState<'signin' | 'signup'>('signin');
|
||||
const signupDisabled = true; // Signup disabled due to upcoming system replacement
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showWarningDetails, setShowWarningDetails] = useState(false);
|
||||
const authCardOpacity = useRef(new Animated.Value(1)).current;
|
||||
|
||||
// Subtle, performant animations
|
||||
const introOpacity = useRef(new Animated.Value(0)).current;
|
||||
|
|
@ -141,6 +144,16 @@ const AuthScreen: React.FC = () => {
|
|||
|
||||
const handleSubmit = async () => {
|
||||
if (loading) return;
|
||||
|
||||
// Prevent signup if disabled
|
||||
if (mode === 'signup' && signupDisabled) {
|
||||
const msg = 'Sign up is currently disabled due to upcoming system changes';
|
||||
setError(msg);
|
||||
Toast.error(msg);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEmailValid) {
|
||||
const msg = 'Enter a valid email address';
|
||||
setError(msg);
|
||||
|
|
@ -187,6 +200,27 @@ const AuthScreen: React.FC = () => {
|
|||
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any);
|
||||
};
|
||||
|
||||
const toggleWarningDetails = () => {
|
||||
if (showWarningDetails) {
|
||||
// Fade in auth card
|
||||
Animated.timing(authCardOpacity, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
} else {
|
||||
// Fade out auth card
|
||||
Animated.timing(authCardOpacity, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
setShowWarningDetails(!showWarningDetails);
|
||||
};
|
||||
|
||||
// showToast helper replaced with direct calls to toast.* API
|
||||
|
||||
return (
|
||||
|
|
@ -254,13 +288,72 @@ const AuthScreen: React.FC = () => {
|
|||
</Text>
|
||||
</Animated.View>
|
||||
|
||||
{/* Important Warning Message */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.warningContainer,
|
||||
{
|
||||
opacity: introOpacity,
|
||||
transform: [{ translateY: introTranslateY }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={[styles.warningCard, { backgroundColor: 'rgba(255, 193, 7, 0.1)', borderColor: 'rgba(255, 193, 7, 0.3)' }]}
|
||||
onPress={toggleWarningDetails}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialIcons name="warning" size={20} color="#FFC107" style={styles.warningIcon} />
|
||||
<View style={styles.warningContent}>
|
||||
<Text style={[styles.warningTitle, { color: '#FFC107' }]}>
|
||||
Important Notice
|
||||
</Text>
|
||||
<Text style={[styles.warningText, { color: currentTheme.colors.white }]}>
|
||||
This authentication system will be completely replaced by local backup/restore functionality by October 8th. Please create backup files as your cloud data will be permanently destroyed.
|
||||
</Text>
|
||||
<Text style={[styles.readMoreText, { color: '#FFC107' }]}>
|
||||
Read more {showWarningDetails ? '▼' : '▶'}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Expanded Details */}
|
||||
{showWarningDetails && (
|
||||
<Animated.View style={[styles.warningDetails, { backgroundColor: 'rgba(255, 193, 7, 0.05)', borderColor: 'rgba(255, 193, 7, 0.2)' }]}>
|
||||
<View style={styles.detailsContent}>
|
||||
<Text style={[styles.detailsTitle, { color: '#FFC107' }]}>
|
||||
Why is this system being discontinued?
|
||||
</Text>
|
||||
<Text style={[styles.detailsText, { color: currentTheme.colors.white }]}>
|
||||
• Lack of real-time support for addon synchronization{'\n'}
|
||||
• Database synchronization issues with addons and settings{'\n'}
|
||||
• Unreliable cloud data management{'\n'}
|
||||
• Performance problems with remote data access
|
||||
</Text>
|
||||
|
||||
<Text style={[styles.detailsTitle, { color: '#FFC107', marginTop: 16 }]}>
|
||||
Benefits of Local Backup System:
|
||||
</Text>
|
||||
<Text style={[styles.detailsText, { color: currentTheme.colors.white }]}>
|
||||
• Instant addon synchronization across devices{'\n'}
|
||||
• Reliable offline access to all your data{'\n'}
|
||||
• Complete control over your backup files{'\n'}
|
||||
• Faster performance with local data storage{'\n'}
|
||||
• No dependency on external servers{'\n'}
|
||||
• Easy migration between devices
|
||||
</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? headerHeight : 0}
|
||||
>
|
||||
{/* Main Card */}
|
||||
<View style={styles.centerContainer}>
|
||||
{/* Main Card - Hide when warning details are expanded */}
|
||||
<Animated.View style={[styles.centerContainer, { opacity: authCardOpacity }]}>
|
||||
<Animated.View style={[styles.card, {
|
||||
backgroundColor: Platform.OS === 'android' ? '#121212' : 'rgba(255,255,255,0.02)',
|
||||
borderColor: Platform.OS === 'android' ? '#1f1f1f' : 'rgba(255,255,255,0.06)',
|
||||
|
|
@ -312,12 +405,19 @@ const AuthScreen: React.FC = () => {
|
|||
<TouchableOpacity
|
||||
style={[
|
||||
styles.switchButton,
|
||||
signupDisabled && styles.disabledButton,
|
||||
]}
|
||||
onPress={() => setMode('signup')}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => !signupDisabled && setMode('signup')}
|
||||
activeOpacity={signupDisabled ? 1 : 0.8}
|
||||
disabled={signupDisabled}
|
||||
>
|
||||
<Text style={[styles.switchText, { color: mode === 'signup' ? '#fff' : currentTheme.colors.textMuted }]}>
|
||||
Sign Up
|
||||
<Text style={[
|
||||
styles.switchText,
|
||||
{
|
||||
color: mode === 'signup' ? '#fff' : (signupDisabled ? 'rgba(255,255,255,0.3)' : currentTheme.colors.textMuted)
|
||||
}
|
||||
]}>
|
||||
Sign Up {signupDisabled && '(Disabled)'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -482,18 +582,29 @@ const AuthScreen: React.FC = () => {
|
|||
</Animated.View>
|
||||
|
||||
{/* Switch Mode */}
|
||||
<TouchableOpacity
|
||||
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
|
||||
activeOpacity={0.7}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Text style={[styles.switchModeText, { color: currentTheme.colors.textMuted }]}>
|
||||
{mode === 'signin' ? "Don't have an account? " : 'Already have an account? '}
|
||||
<Text style={{ color: currentTheme.colors.primary, fontWeight: '600' }}>
|
||||
{mode === 'signin' ? 'Sign up' : 'Sign in'}
|
||||
{!signupDisabled && (
|
||||
<TouchableOpacity
|
||||
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
|
||||
activeOpacity={0.7}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Text style={[styles.switchModeText, { color: currentTheme.colors.textMuted }]}>
|
||||
{mode === 'signin' ? "Don't have an account? " : 'Already have an account? '}
|
||||
<Text style={{ color: currentTheme.colors.primary, fontWeight: '600' }}>
|
||||
{mode === 'signin' ? 'Sign up' : 'Sign in'}
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Signup disabled message */}
|
||||
{signupDisabled && mode === 'signin' && (
|
||||
<View style={{ marginTop: 16, alignItems: 'center' }}>
|
||||
<Text style={[styles.switchModeText, { color: 'rgba(255,255,255,0.5)', fontSize: 13 }]}>
|
||||
New account creation is temporarily disabled
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Skip sign in - more prominent when coming from onboarding */}
|
||||
<TouchableOpacity
|
||||
|
|
@ -520,7 +631,7 @@ const AuthScreen: React.FC = () => {
|
|||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
</View>
|
||||
</Animated.View>
|
||||
</KeyboardAvoidingView>
|
||||
{/* Toasts rendered globally in App root */}
|
||||
</SafeAreaView>
|
||||
|
|
@ -580,8 +691,9 @@ const styles = StyleSheet.create({
|
|||
centerContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 28,
|
||||
},
|
||||
card: {
|
||||
|
|
@ -686,6 +798,63 @@ const styles = StyleSheet.create({
|
|||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
warningContainer: {
|
||||
paddingHorizontal: 20,
|
||||
marginTop: 24,
|
||||
marginBottom: 8,
|
||||
},
|
||||
warningCard: {
|
||||
flexDirection: 'row',
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
warningIcon: {
|
||||
marginRight: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
warningContent: {
|
||||
flex: 1,
|
||||
},
|
||||
warningTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
marginBottom: 6,
|
||||
},
|
||||
warningText: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
fontWeight: '500',
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
readMoreText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginTop: 8,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
warningDetails: {
|
||||
marginTop: 8,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
detailsContent: {
|
||||
padding: 16,
|
||||
},
|
||||
detailsTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
marginBottom: 8,
|
||||
},
|
||||
detailsText: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
|
||||
export default AuthScreen;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import * as Updates from 'expo-updates';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { backupService, BackupOptions } from '../services/backupService';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
|
@ -41,6 +42,20 @@ const BackupScreen: React.FC = () => {
|
|||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
const restartApp = async () => {
|
||||
try {
|
||||
await Updates.reloadAsync();
|
||||
} catch (error) {
|
||||
logger.error('[BackupScreen] Failed to restart app:', error);
|
||||
// Fallback: show error message
|
||||
openAlert(
|
||||
'Restart Failed',
|
||||
'Failed to restart the app. Please manually close and reopen the app to see your restored data.',
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Create backup
|
||||
const handleCreateBackup = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -162,7 +177,14 @@ const BackupScreen: React.FC = () => {
|
|||
openAlert(
|
||||
'Restore Complete',
|
||||
'Your data has been successfully restored. Please restart the app to see all changes.',
|
||||
[{ label: 'OK', onPress: () => {} }]
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => {} },
|
||||
{
|
||||
label: 'Restart App',
|
||||
onPress: restartApp,
|
||||
style: { fontWeight: 'bold' }
|
||||
}
|
||||
]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[BackupScreen] Failed to restore backup:', error);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import Animated, {
|
|||
} from 'react-native-reanimated';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||
import { useAccount } from '../contexts/AccountContext';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
|
|
@ -79,7 +78,6 @@ const onboardingData: OnboardingSlide[] = [
|
|||
const OnboardingScreen = () => {
|
||||
const { currentTheme } = useTheme();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { user } = useAccount();
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
const progressValue = useSharedValue(0);
|
||||
|
|
@ -137,15 +135,11 @@ const OnboardingScreen = () => {
|
|||
const handleGetStarted = async () => {
|
||||
try {
|
||||
await AsyncStorage.setItem('hasCompletedOnboarding', 'true');
|
||||
// After onboarding, route to login if no user; otherwise go to app
|
||||
if (!user) {
|
||||
navigation.reset({ index: 0, routes: [{ name: 'Account', params: { fromOnboarding: true } as any }] });
|
||||
} else {
|
||||
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] });
|
||||
}
|
||||
// After onboarding, go directly to main app
|
||||
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] });
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Error saving onboarding status:', error);
|
||||
navigation.reset({ index: 0, routes: [{ name: user ? 'MainTabs' : 'Account', params: !user ? ({ fromOnboarding: true } as any) : undefined }] });
|
||||
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -453,8 +453,43 @@ export class BackupService {
|
|||
|
||||
private async getTraktSettings(): Promise<any> {
|
||||
try {
|
||||
// Get general Trakt settings
|
||||
const traktSettingsJson = await AsyncStorage.getItem('trakt_settings');
|
||||
return traktSettingsJson ? JSON.parse(traktSettingsJson) : {};
|
||||
const traktSettings = traktSettingsJson ? JSON.parse(traktSettingsJson) : {};
|
||||
|
||||
// Get authentication tokens
|
||||
const [
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenExpiry,
|
||||
autosyncEnabled,
|
||||
syncFrequency,
|
||||
completionThreshold
|
||||
] = await Promise.all([
|
||||
AsyncStorage.getItem('trakt_access_token'),
|
||||
AsyncStorage.getItem('trakt_refresh_token'),
|
||||
AsyncStorage.getItem('trakt_token_expiry'),
|
||||
AsyncStorage.getItem('trakt_autosync_enabled'),
|
||||
AsyncStorage.getItem('trakt_sync_frequency'),
|
||||
AsyncStorage.getItem('trakt_completion_threshold')
|
||||
]);
|
||||
|
||||
return {
|
||||
...traktSettings,
|
||||
authentication: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenExpiry: tokenExpiry ? parseInt(tokenExpiry, 10) : null
|
||||
},
|
||||
autosync: {
|
||||
enabled: autosyncEnabled ? (() => {
|
||||
try { return JSON.parse(autosyncEnabled); }
|
||||
catch { return true; }
|
||||
})() : true,
|
||||
frequency: syncFrequency ? parseInt(syncFrequency, 10) : 60000,
|
||||
completionThreshold: completionThreshold ? parseInt(completionThreshold, 10) : 95
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('[BackupService] Failed to get Trakt settings:', error);
|
||||
return {};
|
||||
|
|
@ -607,8 +642,53 @@ export class BackupService {
|
|||
|
||||
private async restoreTraktSettings(traktSettings: any): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem('trakt_settings', JSON.stringify(traktSettings));
|
||||
logger.info('[BackupService] Trakt settings restored');
|
||||
// Restore general Trakt settings
|
||||
if (traktSettings && typeof traktSettings === 'object') {
|
||||
const { authentication, autosync, ...generalSettings } = traktSettings;
|
||||
|
||||
// Restore general settings
|
||||
await AsyncStorage.setItem('trakt_settings', JSON.stringify(generalSettings));
|
||||
|
||||
// Restore authentication tokens if available
|
||||
if (authentication) {
|
||||
const tokenPromises = [];
|
||||
|
||||
if (authentication.accessToken) {
|
||||
tokenPromises.push(AsyncStorage.setItem('trakt_access_token', authentication.accessToken));
|
||||
}
|
||||
|
||||
if (authentication.refreshToken) {
|
||||
tokenPromises.push(AsyncStorage.setItem('trakt_refresh_token', authentication.refreshToken));
|
||||
}
|
||||
|
||||
if (authentication.tokenExpiry) {
|
||||
tokenPromises.push(AsyncStorage.setItem('trakt_token_expiry', authentication.tokenExpiry.toString()));
|
||||
}
|
||||
|
||||
await Promise.all(tokenPromises);
|
||||
}
|
||||
|
||||
// Restore autosync settings if available
|
||||
if (autosync) {
|
||||
const autosyncPromises = [];
|
||||
|
||||
if (autosync.enabled !== undefined) {
|
||||
autosyncPromises.push(AsyncStorage.setItem('trakt_autosync_enabled', JSON.stringify(autosync.enabled)));
|
||||
}
|
||||
|
||||
if (autosync.frequency !== undefined) {
|
||||
autosyncPromises.push(AsyncStorage.setItem('trakt_sync_frequency', autosync.frequency.toString()));
|
||||
}
|
||||
|
||||
if (autosync.completionThreshold !== undefined) {
|
||||
autosyncPromises.push(AsyncStorage.setItem('trakt_completion_threshold', autosync.completionThreshold.toString()));
|
||||
}
|
||||
|
||||
await Promise.all(autosyncPromises);
|
||||
}
|
||||
|
||||
logger.info('[BackupService] Trakt settings and authentication restored');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[BackupService] Failed to restore Trakt settings:', error);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue