From a0a138081d9613ec29feb867de007024d072ba60 Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 28 Oct 2025 01:02:33 +0530 Subject: [PATCH] users can now choose between what to backup --- src/hooks/useBackupOptions.ts | 99 ++++++++ src/screens/BackupScreen.tsx | 429 +++++++++++++++++++++++++++++----- 2 files changed, 465 insertions(+), 63 deletions(-) create mode 100644 src/hooks/useBackupOptions.ts diff --git a/src/hooks/useBackupOptions.ts b/src/hooks/useBackupOptions.ts new file mode 100644 index 0000000..17e5427 --- /dev/null +++ b/src/hooks/useBackupOptions.ts @@ -0,0 +1,99 @@ +import { useState, useEffect, useCallback } from 'react'; +import { mmkvStorage } from '../services/mmkvStorage'; +import { BackupOptions } from '../services/backupService'; +import { logger } from '../utils/logger'; + +interface BackupPreferences { + includeLibrary: boolean; + includeWatchProgress: boolean; + includeAddons: boolean; + includeSettings: boolean; + includeTraktData: boolean; + includeLocalScrapers: boolean; + includeApiKeys: boolean; + includeCatalogSettings: boolean; + includeUserPreferences: boolean; +} + +const DEFAULT_PREFERENCES: BackupPreferences = { + includeLibrary: true, + includeWatchProgress: true, + includeAddons: true, + includeSettings: true, + includeTraktData: true, + includeLocalScrapers: true, + includeApiKeys: true, + includeCatalogSettings: true, + includeUserPreferences: true, +}; + +const STORAGE_KEY = 'backup_preferences'; + +export const useBackupOptions = () => { + const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES); + const [isLoading, setIsLoading] = useState(true); + + // Load preferences from storage + useEffect(() => { + const loadPreferences = async () => { + try { + const stored = await mmkvStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + // Merge with defaults to handle any missing keys + setPreferences({ ...DEFAULT_PREFERENCES, ...parsed }); + } + } catch (error) { + logger.error('[useBackupOptions] Failed to load preferences:', error); + setPreferences(DEFAULT_PREFERENCES); + } finally { + setIsLoading(false); + } + }; + + loadPreferences(); + }, []); + + // Save preferences to storage + const savePreferences = useCallback(async (newPreferences: BackupPreferences) => { + try { + await mmkvStorage.setItem(STORAGE_KEY, JSON.stringify(newPreferences)); + } catch (error) { + logger.error('[useBackupOptions] Failed to save preferences:', error); + } + }, []); + + // Update a single preference + const updatePreference = useCallback( + async (key: keyof BackupPreferences, value: boolean) => { + const newPreferences = { ...preferences, [key]: value }; + setPreferences(newPreferences); + await savePreferences(newPreferences); + }, + [preferences, savePreferences] + ); + + // Get backup options in the format expected by backupService + const getBackupOptions = useCallback((): BackupOptions => { + return { + includeLibrary: preferences.includeLibrary, + includeWatchProgress: preferences.includeWatchProgress, + includeDownloads: false, // Downloads are never backed up + includeAddons: preferences.includeAddons, + includeSettings: preferences.includeSettings, + includeTraktData: preferences.includeTraktData, + includeLocalScrapers: preferences.includeLocalScrapers, + includeApiKeys: preferences.includeApiKeys, + includeCatalogSettings: preferences.includeCatalogSettings, + includeUserPreferences: preferences.includeUserPreferences, + }; + }, [preferences]); + + return { + preferences, + isLoading, + updatePreference, + getBackupOptions, + }; +}; + diff --git a/src/screens/BackupScreen.tsx b/src/screens/BackupScreen.tsx index c366534..97cd785 100644 --- a/src/screens/BackupScreen.tsx +++ b/src/screens/BackupScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import { View, Text, @@ -9,21 +9,43 @@ import { SafeAreaView, StatusBar, ScrollView, + Switch, + Animated, + Easing, } from 'react-native'; 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 { backupService } from '../services/backupService'; import { useTheme } from '../contexts/ThemeContext'; import { logger } from '../utils/logger'; import CustomAlert from '../components/CustomAlert'; +import { useBackupOptions } from '../hooks/useBackupOptions'; const BackupScreen: React.FC = () => { const { currentTheme } = useTheme(); const [isLoading, setIsLoading] = useState(false); const navigation = useNavigation(); + const { preferences, updatePreference, getBackupOptions } = useBackupOptions(); + + // Collapsible sections state + const [expandedSections, setExpandedSections] = useState({ + coreData: false, + addonsIntegrations: false, + settingsPreferences: false, + }); + + // Animated values for each section + const coreDataAnim = useRef(new Animated.Value(0)).current; + const addonsAnim = useRef(new Animated.Value(0)).current; + const settingsAnim = useRef(new Animated.Value(0)).current; + + // Chevron rotation animated values + const coreDataChevron = useRef(new Animated.Value(0)).current; + const addonsChevron = useRef(new Animated.Value(0)).current; + const settingsChevron = useRef(new Animated.Value(0)).current; // Alert state const [alertVisible, setAlertVisible] = useState(false); @@ -56,6 +78,43 @@ const BackupScreen: React.FC = () => { } }; + // Toggle section collapse/expand + const toggleSection = useCallback((section: 'coreData' | 'addonsIntegrations' | 'settingsPreferences') => { + const isExpanded = expandedSections[section]; + + let heightAnim: Animated.Value; + let chevronAnim: Animated.Value; + + if (section === 'coreData') { + heightAnim = coreDataAnim; + chevronAnim = coreDataChevron; + } else if (section === 'addonsIntegrations') { + heightAnim = addonsAnim; + chevronAnim = addonsChevron; + } else { + heightAnim = settingsAnim; + chevronAnim = settingsChevron; + } + + // Animate height and chevron rotation + Animated.parallel([ + Animated.timing(heightAnim, { + toValue: isExpanded ? 0 : 1, + duration: 300, + useNativeDriver: false, // Required for height + easing: Easing.inOut(Easing.ease), + }), + Animated.timing(chevronAnim, { + toValue: isExpanded ? 0 : 1, + duration: 300, + useNativeDriver: true, // Transforms support native driver + easing: Easing.inOut(Easing.ease), + }), + ]).start(); + + setExpandedSections(prev => ({...prev, [section]: !isExpanded})); + }, [expandedSections, coreDataAnim, addonsAnim, settingsAnim, coreDataChevron, addonsChevron, settingsChevron]); + // Create backup const handleCreateBackup = useCallback(async () => { try { @@ -64,64 +123,78 @@ const BackupScreen: React.FC = () => { const preview = await backupService.getBackupPreview(); setIsLoading(false); - // Calculate total without downloads - const totalWithoutDownloads = preview.library + preview.watchProgress + preview.addons + preview.scrapers; + // Filter based on preferences + const items: string[] = []; + let total = 0; + + if (preferences.includeLibrary) { + items.push(`Library: ${preview.library} items`); + total += preview.library; + } + + if (preferences.includeWatchProgress) { + items.push(`Watch Progress: ${preview.watchProgress} entries`); + total += preview.watchProgress; + } + + if (preferences.includeAddons) { + items.push(`Addons: ${preview.addons} installed`); + total += preview.addons; + } + + if (preferences.includeLocalScrapers) { + items.push(`Plugins: ${preview.scrapers} configurations`); + total += preview.scrapers; + } + + // Check if no items are selected + const message = items.length > 0 + ? `Backup Contents:\n\n${items.join('\n')}\n\nTotal: ${total} items\n\nThis backup includes your selected app settings, themes, and integration data.` + : `No content selected for backup.\n\nPlease enable at least one option in the Backup Options section above.`; openAlert( 'Create Backup', - `Backup Contents:\n\n` + - `Library: ${preview.library} items\n` + - `Watch Progress: ${preview.watchProgress} entries\n` + - `Addons: ${preview.addons} installed\n` + - `Plugins: ${preview.scrapers} configurations\n\n` + - `Total: ${totalWithoutDownloads} items\n\n` + - `This backup includes all your app settings, themes, and integration data.`, - [ - { label: 'Cancel', onPress: () => {} }, - { - label: 'Create Backup', - onPress: async () => { - try { - setIsLoading(true); + message, + items.length > 0 + ? [ + { label: 'Cancel', onPress: () => {} }, + { + label: 'Create Backup', + onPress: async () => { + try { + setIsLoading(true); - const backupOptions: BackupOptions = { - includeLibrary: true, - includeWatchProgress: true, - includeDownloads: true, - includeAddons: true, - includeSettings: true, - includeTraktData: true, - includeLocalScrapers: true, - }; + const backupOptions = getBackupOptions(); - const fileUri = await backupService.createBackup(backupOptions); + const fileUri = await backupService.createBackup(backupOptions); - // Share the backup file - if (await Sharing.isAvailableAsync()) { - await Sharing.shareAsync(fileUri, { - mimeType: 'application/json', - dialogTitle: 'Share Nuvio Backup', - }); + // Share the backup file + if (await Sharing.isAvailableAsync()) { + await Sharing.shareAsync(fileUri, { + mimeType: 'application/json', + dialogTitle: 'Share Nuvio Backup', + }); + } + + openAlert( + 'Backup Created', + 'Your backup has been created and is ready to share.', + [{ label: 'OK', onPress: () => {} }] + ); + } catch (error) { + logger.error('[BackupScreen] Failed to create backup:', error); + openAlert( + 'Backup Failed', + `Failed to create backup: ${error instanceof Error ? error.message : String(error)}`, + [{ label: 'OK', onPress: () => {} }] + ); + } finally { + setIsLoading(false); + } } - - openAlert( - 'Backup Created', - 'Your backup has been created and is ready to share.', - [{ label: 'OK', onPress: () => {} }] - ); - } catch (error) { - logger.error('[BackupScreen] Failed to create backup:', error); - openAlert( - 'Backup Failed', - `Failed to create backup: ${error instanceof Error ? error.message : String(error)}`, - [{ label: 'OK', onPress: () => {} }] - ); - } finally { - setIsLoading(false); } - } - } - ] + ] + : [{ label: 'OK', onPress: () => {} }] ); } catch (error) { logger.error('[BackupScreen] Failed to get backup preview:', error); @@ -132,7 +205,7 @@ const BackupScreen: React.FC = () => { ); setIsLoading(false); } - }, [openAlert]); + }, [openAlert, preferences, getBackupOptions]); // Restore backup const handleRestoreBackup = useCallback(async () => { @@ -162,15 +235,7 @@ const BackupScreen: React.FC = () => { try { setIsLoading(true); - const restoreOptions: BackupOptions = { - includeLibrary: true, - includeWatchProgress: true, - includeDownloads: true, - includeAddons: true, - includeSettings: true, - includeTraktData: true, - includeLocalScrapers: true, - }; + const restoreOptions = getBackupOptions(); await backupService.restoreBackup(fileUri, restoreOptions); @@ -248,6 +313,181 @@ const BackupScreen: React.FC = () => { onClose={() => setAlertVisible(false)} /> + {/* Backup Options Section */} + + + Backup Options + + + Choose what to include in your backups + + + {/* Core Data Group */} + toggleSection('coreData')} + activeOpacity={0.7} + > + + Core Data + + + + + + + updatePreference('includeLibrary', v)} + theme={currentTheme} + /> + updatePreference('includeWatchProgress', v)} + theme={currentTheme} + /> + + + {/* Addons & Integrations Group */} + toggleSection('addonsIntegrations')} + activeOpacity={0.7} + > + + Addons & Integrations + + + + + + + updatePreference('includeAddons', v)} + theme={currentTheme} + /> + updatePreference('includeLocalScrapers', v)} + theme={currentTheme} + /> + updatePreference('includeTraktData', v)} + theme={currentTheme} + /> + + + {/* Settings & Preferences Group */} + toggleSection('settingsPreferences')} + activeOpacity={0.7} + > + + Settings & Preferences + + + + + + + updatePreference('includeSettings', v)} + theme={currentTheme} + /> + updatePreference('includeUserPreferences', v)} + theme={currentTheme} + /> + updatePreference('includeCatalogSettings', v)} + theme={currentTheme} + /> + updatePreference('includeApiKeys', v)} + theme={currentTheme} + /> + + + {/* Backup Actions */} @@ -297,7 +537,7 @@ const BackupScreen: React.FC = () => { About Backups - • Backups include all your data: library, watch progress, settings, addons, downloads, and plugins{'\n'} + • Customize what gets backed up using the toggles above{'\n'} • Backup files are stored locally on your device{'\n'} • Share your backup to transfer data between devices{'\n'} • Restoring will overwrite your current data @@ -309,6 +549,33 @@ const BackupScreen: React.FC = () => { ); }; +interface OptionToggleProps { + label: string; + description: string; + value: boolean; + onValueChange: (value: boolean) => void; + theme: any; +} + +const OptionToggle: React.FC = ({ label, description, value, onValueChange, theme }) => ( + + + + {label} + + + {description} + + + + +); + const styles = StyleSheet.create({ container: { flex: 1, @@ -356,6 +623,42 @@ const styles = StyleSheet.create({ fontWeight: '600', marginBottom: 16, }, + sectionDescription: { + fontSize: 14, + marginBottom: 16, + }, + groupLabel: { + fontSize: 14, + fontWeight: '600', + }, + sectionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: 16, + marginBottom: 8, + paddingVertical: 8, + }, + optionRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + borderBottomWidth: 1, + }, + optionLeft: { + flex: 1, + paddingRight: 16, + }, + optionLabel: { + fontSize: 15, + fontWeight: '500', + marginBottom: 4, + }, + optionDescription: { + fontSize: 13, + lineHeight: 16, + }, actionButton: { flexDirection: 'row', alignItems: 'center',