From 09a6d2a57b60260acc9eeeca33eecbdacd86d788 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Wed, 4 Mar 2026 23:10:19 +0000 Subject: [PATCH] feat: enhance onboarding and settings screens with NAS authentication and configuration --- src/screens/OnboardingScreen.js | 254 ++++++++++++----- src/screens/SettingsScreen.js | 475 +++++++++++++++++++++----------- 2 files changed, 487 insertions(+), 242 deletions(-) diff --git a/src/screens/OnboardingScreen.js b/src/screens/OnboardingScreen.js index c94505e..f592e6e 100644 --- a/src/screens/OnboardingScreen.js +++ b/src/screens/OnboardingScreen.js @@ -1,11 +1,14 @@ import React, { useState } from 'react'; import { StyleSheet, View, Text, ScrollView, TouchableOpacity, Alert, - ActivityIndicator, Switch, + ActivityIndicator, Switch, TextInput, } from 'react-native'; import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; -import ServiceConnectCard from '../components/ServiceConnectCard'; +import GoogleAUTH from '../AUTH/GoogleAUTH'; +import NASAUTH from '../AUTH/NASAUTH'; +import NASSetupInstructions from '../components/NASSetupInstructions'; +import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; const OnboardingScreen = ({ navigation, route }) => { const { userId } = route.params; @@ -18,11 +21,21 @@ const OnboardingScreen = ({ navigation, route }) => { const [iosEnabled, setIosEnabled] = useState(false); const [nasEnabled, setNasEnabled] = useState(false); - // Step 1 — track which services completed connection via ServiceConnectCard callbacks + // Step 1 — connection status const [googleConnected, setGoogleConnected] = useState(false); const [iosConnected, setIosConnected] = useState(false); const [nasConnected, setNasConnected] = useState(false); + // Step 1 — health indicators + const [googleHealth, setGoogleHealth] = useState('inactive'); + const [iosHealth, setIosHealth] = useState('inactive'); + const [nasHealth, setNasHealth] = useState('inactive'); + + // NAS fields + const [nasHost, setNasHost] = useState(''); + const [nasPort, setNasPort] = useState('9500'); + const [nasUsername, setNasUsername] = useState(''); + // Step 2 — auto-backup checklist const [autoBackupServices, setAutoBackupServices] = useState([]); @@ -40,6 +53,74 @@ const OnboardingScreen = ({ navigation, route }) => { return services; }; + const handleGoogleAuth = async () => { + setLoading(true); + setGoogleHealth('checking'); + try { + const googleAuth = new GoogleAUTH(); + const result = await googleAuth.authenticate(); + await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', { + accessToken: result.accessToken, + refreshToken: result.refreshToken || null, + expiresAt: googleAuth.expiresAt || Date.now() + 3600000, + email: result.email || 'google-user', + }); + setGoogleConnected(true); + setGoogleHealth('healthy'); + Alert.alert('Success', 'Google Photos connected!'); + } catch (error) { + setGoogleHealth('error'); + Alert.alert('Error', 'Failed to connect Google Photos: ' + error.message); + } finally { + setLoading(false); + } + }; + + const handleIosAuth = async () => { + setLoading(true); + setIosHealth('checking'); + try { + await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', { + status: 'placeholder', + note: 'iCloud integration pending Apple API access', + }); + setIosConnected(true); + setIosHealth('healthy'); + Alert.alert('Info', 'iCloud Photos registered. Full integration coming soon.'); + } catch (error) { + setIosHealth('error'); + Alert.alert('Error', 'Failed to set up iCloud: ' + error.message); + } finally { + setLoading(false); + } + }; + + const handleNasAuth = async () => { + if (!nasHost.trim() || !nasUsername.trim()) { + Alert.alert('Error', 'Please fill in the NAS host and username'); + return; + } + setLoading(true); + setNasHealth('checking'); + try { + const nasAuth = new NASAUTH(); + await nasAuth.authenticate(nasHost.trim(), parseInt(nasPort) || 9500, nasUsername.trim()); + await DatabaseService.linkAccount(userId, 'NAS', { + host: nasHost.trim(), + port: parseInt(nasPort) || 9500, + username: nasUsername.trim(), + }); + setNasConnected(true); + setNasHealth('healthy'); + Alert.alert('Success', 'NAS connected!'); + } catch (error) { + setNasHealth('error'); + Alert.alert('Error', 'Failed to connect NAS: ' + error.message); + } finally { + setLoading(false); + } + }; + const toggleAutoBackup = (serviceType) => { setAutoBackupServices((prev) => prev.includes(serviceType) ? prev.filter((s) => s !== serviceType) : [...prev, serviceType] @@ -89,7 +170,7 @@ const OnboardingScreen = ({ navigation, route }) => { {steps[currentStep].description} - {/* Step 0: Select services */} + {/* ── Step 0: pick services ── */} {currentStep === 0 && ( { setNasEnabled(!nasEnabled)} /> + {nasEnabled && } )} - {/* Step 1: Configure each selected service via shared ServiceConnectCard */} + {/* ── Step 1: connect each service ── */} {currentStep === 1 && ( + {googleEnabled && ( - setGoogleConnected(true)} - /> + + + + 📷 + Google Photos + {googleConnected && ✓ Connected} + + {!googleConnected && ( + + {loading ? : Sign in with Google} + + )} + )} + {iosEnabled && ( - setIosConnected(true)} - /> + + + + 🍎 + iCloud Photos + {iosConnected && ✓ Connected} + + {!iosConnected && ( + + {loading ? : Connect iCloud} + + )} + )} + {nasEnabled && ( - setNasConnected(true)} - /> + + + + 💾 + NAS Storage + {nasConnected && ✓ Connected} + + {!nasConnected && ( + <> + Server Address + + Port + + Username + + + {loading ? : Test & Connect} + + + )} + )} )} - {/* Step 2: Auto-backup multi-checkbox */} + {/* ── Step 2: auto-backup ── */} {currentStep === 2 && ( @@ -142,31 +271,22 @@ const OnboardingScreen = ({ navigation, route }) => { Select which services new photos should automatically upload to. You can select multiple or none. {getConnectedServices().map((svc) => ( - toggleAutoBackup(svc.type)} - activeOpacity={0.7} + onPress={() => toggleAutoBackup(svc.type)} activeOpacity={0.7} > - + {autoBackupServices.includes(svc.type) && ✓} {svc.icon} {svc.name} ))} - setAutoBackupServices([])} - activeOpacity={0.7} - > - + setAutoBackupServices([])} activeOpacity={0.7}> + {autoBackupServices.length === 0 && ✓} Don't automatically back up @@ -183,17 +303,13 @@ const OnboardingScreen = ({ navigation, route }) => { onPress={() => setCurrentStep(currentStep - 1)} disabled={loading}> Back - ) : ( - - )} + ) : } {loading ? : ( - - {currentStep === steps.length - 1 ? 'Get Started' : 'Next'} - + {currentStep === steps.length - 1 ? 'Get Started' : 'Next'} )} @@ -210,21 +326,15 @@ const ServiceToggleCard = ({ theme, icon, name, description, enabled, onToggle } {description} - + thumbColor={enabled ? '#34a853' : '#f4f3f4'} /> ); const styles = StyleSheet.create({ container: { flex: 1 }, - progressBar: { - flexDirection: 'row', justifyContent: 'center', gap: 8, - paddingTop: 60, paddingBottom: 8, - }, + progressBar: { flexDirection: 'row', justifyContent: 'center', gap: 8, paddingTop: 60, paddingBottom: 8 }, progressDot: { width: 32, height: 4, borderRadius: 2 }, content: { flex: 1, paddingHorizontal: 20 }, header: { marginBottom: 24 }, @@ -232,35 +342,29 @@ const styles = StyleSheet.create({ title: { fontSize: 26, fontWeight: '700', marginBottom: 6 }, description: { fontSize: 14 }, cardsContainer: { gap: 12 }, - toggleCard: { - borderRadius: 12, padding: 16, flexDirection: 'row', - justifyContent: 'space-between', alignItems: 'center', borderWidth: 1.5, - }, + toggleCard: { borderRadius: 12, padding: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderWidth: 1.5 }, toggleCardContent: { flex: 1, flexDirection: 'row', alignItems: 'center' }, toggleIcon: { fontSize: 28, marginRight: 14 }, toggleInfo: { flex: 1 }, toggleName: { fontSize: 15, fontWeight: '600', marginBottom: 2 }, toggleDescription: { fontSize: 12 }, configCard: { borderRadius: 12, padding: 16, borderWidth: 1 }, + configHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, + configIcon: { fontSize: 24, marginRight: 10 }, + configTitle: { fontSize: 16, fontWeight: '600', flex: 1 }, + connectedBadge: { fontSize: 13, fontWeight: '600' }, + authButton: { borderRadius: 8, paddingVertical: 12, alignItems: 'center', marginTop: 4 }, + authButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' }, + fieldLabel: { fontSize: 12, fontWeight: '600', marginTop: 10, marginBottom: 4 }, + fieldInput: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14 }, checklistInfo: { fontSize: 13, marginBottom: 8 }, - checkRow: { - flexDirection: 'row', alignItems: 'center', paddingVertical: 14, - borderBottomWidth: StyleSheet.hairlineWidth, - }, - checkbox: { - width: 22, height: 22, borderRadius: 4, borderWidth: 2, - justifyContent: 'center', alignItems: 'center', marginRight: 12, - }, + checkRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth }, + checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, justifyContent: 'center', alignItems: 'center', marginRight: 12 }, checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' }, checkIcon: { fontSize: 18, marginRight: 8 }, checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 }, - footer: { - flexDirection: 'row', paddingHorizontal: 20, paddingVertical: 14, - borderTopWidth: 1, gap: 12, - }, - backButton: { - flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center', borderWidth: 1, - }, + footer: { flexDirection: 'row', paddingHorizontal: 20, paddingVertical: 14, borderTopWidth: 1, gap: 12 }, + backButton: { flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center', borderWidth: 1 }, backButtonText: { fontSize: 15, fontWeight: '600' }, nextButton: { flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center' }, nextButtonText: { fontSize: 15, fontWeight: '600', color: '#fff' }, diff --git a/src/screens/SettingsScreen.js b/src/screens/SettingsScreen.js index e4139fe..23343f0 100644 --- a/src/screens/SettingsScreen.js +++ b/src/screens/SettingsScreen.js @@ -1,32 +1,42 @@ import React, { useState, useCallback } from 'react'; import { - StyleSheet, View, Text, ScrollView, TouchableOpacity, Switch, Alert, ActivityIndicator, + StyleSheet, View, Text, ScrollView, TouchableOpacity, Switch, TextInput, + Alert, ActivityIndicator, } from 'react-native'; import { useFocusEffect } from '@react-navigation/native'; import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; -import ServiceConnectCard from '../components/ServiceConnectCard'; +import NASSetupInstructions from '../components/NASSetupInstructions'; import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; import NASAUTH from '../AUTH/NASAUTH'; import GoogleAUTH from '../AUTH/GoogleAUTH'; import { getServiceIcon, getServiceName } from '../UI/ServiceHelpers'; -// All services the app supports — add new ones here and they appear everywhere automatically +// Master list — add a new service here and it appears in both sections automatically const ALL_SERVICES = [ - { type: 'GOOGLE_PHOTOS', icon: '📷', name: 'Google Photos', desc: 'Access your Google Photos library' }, - { type: 'ICLOUD_PHOTOS', icon: '🍎', name: 'iCloud Photos', desc: 'Access your iCloud photo library' }, - { type: 'NAS', icon: '💾', name: 'NAS Storage', desc: 'Connect to your NAS photo server' }, + { type: 'GOOGLE_PHOTOS', icon: '📷', name: 'Google Photos' }, + { type: 'ICLOUD_PHOTOS', icon: '🍎', name: 'iCloud Photos' }, + { type: 'NAS', icon: '💾', name: 'NAS Storage' }, ]; const SettingsScreen = ({ navigation, route }) => { const userId = route?.params?.userId; const { theme, themeName, setThemeName } = useTheme(); + const [loading, setLoading] = useState(false); const [accounts, setAccounts] = useState([]); const [autoBackup, setAutoBackup] = useState([]); const [nasOffload, setNasOffload] = useState(false); const [serviceHealth, setServiceHealth] = useState({}); - // Track a reload token so ServiceConnectCards re-read existingData after linking - const [reloadKey, setReloadKey] = useState(0); + + // Which unlinked-service "add" form is expanded + const [expandedAdd, setExpandedAdd] = useState(null); + // Which linked-service edit form is expanded + const [expandedEdit, setExpandedEdit] = useState(null); + + // NAS form fields (shared for both add and edit) + const [nasHost, setNasHost] = useState(''); + const [nasPort, setNasPort] = useState('9500'); + const [nasUsername, setNasUsername] = useState(''); useFocusEffect( useCallback(() => { @@ -40,64 +50,72 @@ const SettingsScreen = ({ navigation, route }) => { setAccounts(allAccounts); const backup = await DatabaseService.getAutoBackupServices(userId); setAutoBackup(backup); + const nasAcc = allAccounts.find((a) => a.service_type === 'NAS'); + if (nasAcc?.account_data) { + setNasHost(nasAcc.account_data.host || ''); + setNasPort(String(nasAcc.account_data.port || 9500)); + setNasUsername(nasAcc.account_data.username || ''); + } checkAllServiceHealth(allAccounts); } catch (err) { console.error('Error loading settings:', err); } }; - // ─── Health checks ─────────────────────────────────────── + // ─── Health ────────────────────────────────────────────── const checkAllServiceHealth = async (accs) => { for (const acc of accs) { if (!acc.is_enabled) { - setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'inactive' })); + setServiceHealth((p) => ({ ...p, [acc.service_type]: 'inactive' })); continue; } - setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'checking' })); - const status = await checkSingleHealth(acc); - setServiceHealth((prev) => ({ ...prev, [acc.service_type]: status })); + setServiceHealth((p) => ({ ...p, [acc.service_type]: 'checking' })); + const s = await checkSingleHealth(acc); + setServiceHealth((p) => ({ ...p, [acc.service_type]: s })); } }; const checkSingleHealth = async (acc) => { try { if (acc.service_type === 'GOOGLE_PHOTOS') { - const gAuth = new GoogleAUTH(); - gAuth.accessToken = acc.account_data?.accessToken; - gAuth.refreshToken = acc.account_data?.refreshToken; - gAuth.expiresAt = acc.account_data?.expiresAt || 0; - const info = await gAuth.getUserInfo(); + const g = new GoogleAUTH(); + g.accessToken = acc.account_data?.accessToken; + g.refreshToken = acc.account_data?.refreshToken; + g.expiresAt = acc.account_data?.expiresAt || 0; + const info = await g.getUserInfo(); return info?.email ? 'healthy' : 'error'; } if (acc.service_type === 'NAS') { - const nasAuth = new NASAUTH(); + const n = new NASAUTH(); const d = acc.account_data || {}; - await nasAuth.authenticate(d.host, d.port, d.username); - const healthy = await nasAuth.healthCheck(); - await nasAuth.revokeAccess(); - return healthy ? 'healthy' : 'error'; + await n.authenticate(d.host, d.port, d.username); + const ok = await n.healthCheck(); + await n.revokeAccess(); + return ok ? 'healthy' : 'error'; } if (acc.service_type === 'ICLOUD_PHOTOS') return 'healthy'; return 'inactive'; - } catch { - return 'error'; - } + } catch { return 'error'; } }; - // ─── Service actions ───────────────────────────────────── + const recheckHealth = async (acc) => { + setServiceHealth((p) => ({ ...p, [acc.service_type]: 'checking' })); + const s = await checkSingleHealth(acc); + setServiceHealth((p) => ({ ...p, [acc.service_type]: s })); + }; + + // ─── Actions on existing accounts ──────────────────────── const toggleService = async (accountId, currentEnabled) => { try { await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled); await loadData(); - } catch { - Alert.alert('Error', 'Failed to update service'); - } + } catch { Alert.alert('Error', 'Failed to update service'); } }; const deleteAccount = (acc) => { Alert.alert( 'Remove Service', - `Remove ${getServiceName(acc.service_type)} from your account?`, + `Remove ${getServiceName(acc.service_type)} from your account? This cannot be undone.`, [ { text: 'Cancel', style: 'cancel' }, { @@ -105,23 +123,99 @@ const SettingsScreen = ({ navigation, route }) => { onPress: async () => { try { await DatabaseService.removeLinkedAccount(acc.id); - setServiceHealth((prev) => { const n = { ...prev }; delete n[acc.service_type]; return n; }); + setServiceHealth((p) => { const n = { ...p }; delete n[acc.service_type]; return n; }); + setExpandedEdit(null); await loadData(); - } catch { - Alert.alert('Error', 'Failed to remove service'); - } + } catch { Alert.alert('Error', 'Failed to remove service'); } }, }, ] ); }; + // ─── Re-authenticate / edit existing ───────────────────── + const saveReauth = async (serviceType) => { + setLoading(true); + try { + if (serviceType === 'GOOGLE_PHOTOS') { + const g = new GoogleAUTH(); + const result = await g.authenticate(); + await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', { + accessToken: result.accessToken, + refreshToken: result.refreshToken || null, + expiresAt: g.expiresAt || Date.now() + 3600000, + email: result.email || 'google-user', + }); + Alert.alert('Success', 'Google Photos re-authenticated!'); + } else if (serviceType === 'ICLOUD_PHOTOS') { + await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', { + status: 'placeholder', note: 'iCloud integration pending Apple API access', + }); + Alert.alert('Info', 'iCloud Photos re-registered.'); + } else if (serviceType === 'NAS') { + if (!nasHost.trim() || !nasUsername.trim()) { + Alert.alert('Error', 'Please fill in the NAS host and username'); + setLoading(false); return; + } + const n = new NASAUTH(); + await n.authenticate(nasHost.trim(), parseInt(nasPort) || 9500, nasUsername.trim()); + await DatabaseService.linkAccount(userId, 'NAS', { + host: nasHost.trim(), port: parseInt(nasPort) || 9500, username: nasUsername.trim(), + }); + Alert.alert('Success', 'NAS configuration saved!'); + } + setExpandedEdit(null); + await loadData(); + } catch (err) { + Alert.alert('Error', 'Failed: ' + err.message); + } finally { setLoading(false); } + }; + + // ─── Add new service ────────────────────────────────────── + const saveNewService = async (serviceType) => { + setLoading(true); + try { + if (serviceType === 'GOOGLE_PHOTOS') { + const g = new GoogleAUTH(); + const result = await g.authenticate(); + await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', { + accessToken: result.accessToken, + refreshToken: result.refreshToken || null, + expiresAt: g.expiresAt || Date.now() + 3600000, + email: result.email || 'google-user', + }); + Alert.alert('Success', 'Google Photos connected!'); + } else if (serviceType === 'ICLOUD_PHOTOS') { + await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', { + status: 'placeholder', note: 'iCloud integration pending Apple API access', + }); + Alert.alert('Info', 'iCloud Photos registered.'); + } else if (serviceType === 'NAS') { + if (!nasHost.trim() || !nasUsername.trim()) { + Alert.alert('Error', 'Please fill in the NAS host and username'); + setLoading(false); return; + } + const n = new NASAUTH(); + await n.authenticate(nasHost.trim(), parseInt(nasPort) || 9500, nasUsername.trim()); + await DatabaseService.linkAccount(userId, 'NAS', { + host: nasHost.trim(), port: parseInt(nasPort) || 9500, username: nasUsername.trim(), + }); + Alert.alert('Success', 'NAS connected!'); + } + setExpandedAdd(null); + setNasHost(''); setNasPort('9500'); setNasUsername(''); + await loadData(); + } catch (err) { + Alert.alert('Error', 'Failed to connect: ' + err.message); + } finally { setLoading(false); } + }; + const toggleAutoBackup = async (serviceType) => { const updated = autoBackup.includes(serviceType) ? autoBackup.filter((s) => s !== serviceType) : [...autoBackup, serviceType]; setAutoBackup(updated); - try { await DatabaseService.setAutoBackupServices(userId, updated); } catch (err) { console.error(err); } + try { await DatabaseService.setAutoBackupServices(userId, updated); } catch (e) { console.error(e); } }; const handleLogout = () => { @@ -139,6 +233,10 @@ const SettingsScreen = ({ navigation, route }) => { const hasNasConnected = accounts.some((a) => a.service_type === 'NAS' && a.is_enabled); + // Split into linked and unlinked + const linkedAccounts = accounts.filter((a) => ALL_SERVICES.some((s) => s.type === a.service_type)); + const unlinkedServices = ALL_SERVICES.filter((s) => !accounts.some((a) => a.service_type === s.type)); + return ( @@ -151,12 +249,11 @@ const SettingsScreen = ({ navigation, route }) => { - {/* ── Appearance ───────────────────────────────── */} + {/* ── Appearance ── */} APPEARANCE {themeOptions.map((opt) => ( - setThemeName(opt.key)} > @@ -170,102 +267,166 @@ const SettingsScreen = ({ navigation, route }) => { ))} - {/* ── Services ─────────────────────────────────── */} - {/* Always shows ALL services — connected ones get a manage strip, unconnected show ServiceConnectCard */} - SERVICES - - Connect new services or manage existing ones at any time. - + {/* ── Connected services ── */} + {linkedAccounts.length > 0 && ( + <> + CONNECTED SERVICES + + {linkedAccounts.map((acc, idx) => { + const health = serviceHealth[acc.service_type] || (acc.is_enabled ? 'checking' : 'inactive'); + const isLastRow = idx === linkedAccounts.length - 1 && expandedEdit !== acc.service_type; + const isEditOpen = expandedEdit === acc.service_type; + return ( + + {/* Main row */} + + recheckHealth(acc)} activeOpacity={0.6}> + + + {getServiceIcon(acc.service_type)} + + {getServiceName(acc.service_type)} + + {acc.is_enabled ? 'Enabled' : 'Disabled'} + + + toggleService(acc.id, acc.is_enabled)} + trackColor={{ false: theme.border, true: '#a8dab5' }} + thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'} + /> + - {ALL_SERVICES.map((svc) => { - const acc = accounts.find((a) => a.service_type === svc.type); - const health = serviceHealth[svc.type] || (acc?.is_enabled ? 'checking' : 'inactive'); + {/* Action buttons */} + + { + if (acc.service_type === 'NAS') { + setExpandedEdit(isEditOpen ? null : acc.service_type); + } else { + saveReauth(acc.service_type); + } + }} + disabled={loading} + > + + {acc.service_type === 'NAS' ? (isEditOpen ? 'Cancel' : 'Edit') : (loading ? '…' : 'Re-auth')} + + + deleteAccount(acc)} + > + Remove + + - if (acc) { - // Already linked — show management card with enable/disable + remove + re-auth via ServiceConnectCard - return ( - - {/* Top row: health, icon, name, enable toggle */} - - checkSingleHealth(acc).then((s) => setServiceHealth((p) => ({ ...p, [svc.type]: s })))}> - - - {svc.icon} - - {svc.name} - - {acc.is_enabled ? 'Enabled' : 'Disabled'} - + {/* Inline NAS edit form */} + {isEditOpen && acc.service_type === 'NAS' && ( + + Server Address + + Port + + Username + + saveReauth('NAS')} disabled={loading}> + {loading ? : Save Changes} + + + )} - toggleService(acc.id, acc.is_enabled)} - trackColor={{ false: theme.border, true: '#a8dab5' }} - thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'} - /> - - - {/* Re-auth / edit via shared ServiceConnectCard (starts collapsed since already linked) */} - { setReloadKey((k) => k + 1); loadData(); }} - onRemoved={() => deleteAccount(acc)} - /> - - ); - } - - // Not yet linked — show ServiceConnectCard directly so user can add it - return ( - - - {svc.icon} - - {svc.name} - Not connected - - - { setReloadKey((k) => k + 1); loadData(); }} - /> + ); + })} - ); - })} + + )} - {/* ── NAS Offloading ────────────────────────────── */} + {/* ── Add services (only shows services not yet connected) ── */} + {unlinkedServices.length > 0 && ( + <> + ADD SERVICES + + {unlinkedServices.map((svc, idx) => { + const isLast = idx === unlinkedServices.length - 1; + const isOpen = expandedAdd === svc.type; + return ( + + {/* Service row with Add button */} + + {svc.icon} + + {svc.name} + Not connected + + { + if (svc.type === 'NAS') { + setNasHost(''); setNasPort('9500'); setNasUsername(''); + setExpandedAdd(isOpen ? null : svc.type); + } else { + saveNewService(svc.type); + } + }} + disabled={loading} + > + {loading && expandedAdd === svc.type ? '…' : 'Add'} + + + + {/* Inline NAS add form */} + {isOpen && svc.type === 'NAS' && ( + + + Server Address + + Port + + Username + + saveNewService('NAS')} disabled={loading}> + {loading ? : Test & Connect} + + + )} + + ); + })} + + + )} + + {/* ── NAS Offloading ── */} {hasNasConnected && ( <> NAS OFFLOADING - 📷 - - Offload Google Photos to NAS - - Fetch Google Photos data through NAS server - + 📷 + + Offload Google Photos to NAS + Fetch Google Photos data through NAS server - + thumbColor={nasOffload ? theme.accent : '#f4f3f4'} /> )} - {/* ── Auto Backup ───────────────────────────────── */} + {/* ── Auto Backup ── */} AUTOMATIC BACKUP @@ -274,45 +435,34 @@ const SettingsScreen = ({ navigation, route }) => { {ALL_SERVICES.map((svc) => { const isConnected = accounts.some((a) => a.service_type === svc.type && a.is_enabled); return ( - isConnected && toggleAutoBackup(svc.type)} - disabled={!isConnected} - activeOpacity={0.7} + disabled={!isConnected} activeOpacity={0.7} > - + {autoBackup.includes(svc.type) && ✓} {svc.icon} - - {svc.name} - + {svc.name} {!isConnected && Not connected} ); })} - { setAutoBackup([]); DatabaseService.setAutoBackupServices(userId, []); }} activeOpacity={0.7} > - + {autoBackup.length === 0 && ✓} - - Don't automatically back up - + Don't automatically back up - {/* ── Sign Out ──────────────────────────────────── */} + {/* ── Sign Out ── */} Sign Out @@ -331,11 +481,9 @@ const styles = StyleSheet.create({ backArrow: { fontSize: 24 }, headerTitle: { fontSize: 18, fontWeight: '700' }, scrollContent: { flex: 1, paddingHorizontal: 16 }, - sectionTitle: { fontSize: 12, fontWeight: '700', marginTop: 24, marginBottom: 4, letterSpacing: 0.5 }, - sectionHint: { fontSize: 12, marginBottom: 10 }, + sectionTitle: { fontSize: 12, fontWeight: '700', marginTop: 24, marginBottom: 8, letterSpacing: 0.5 }, card: { borderRadius: 12, borderWidth: 1, overflow: 'hidden', marginBottom: 4 }, cardSubtext: { fontSize: 12, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 4 }, - // Theme rows themeRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth, @@ -345,40 +493,33 @@ const styles = StyleSheet.create({ themeName: { fontSize: 14, fontWeight: '600', marginBottom: 2 }, themeDesc: { fontSize: 12 }, radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: '#dadce0' }, - // Managed (already linked) service cards - managedCard: { - borderRadius: 12, borderWidth: 1, marginBottom: 10, overflow: 'hidden', + serviceRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12 }, + serviceIcon: { fontSize: 22, marginRight: 8 }, + serviceInfo: { flex: 1 }, + serviceName: { fontSize: 14, fontWeight: '600', marginBottom: 2 }, + serviceStatus: { fontSize: 12 }, + serviceActions: { + flexDirection: 'row', paddingHorizontal: 14, paddingVertical: 8, gap: 8, + borderBottomWidth: StyleSheet.hairlineWidth, }, - managedHeader: { - flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12, - gap: 8, - }, - managedIcon: { fontSize: 22 }, - managedInfo: { flex: 1 }, - managedName: { fontSize: 14, fontWeight: '600', marginBottom: 2 }, - managedStatus: { fontSize: 12 }, - // Not-yet-linked header strip - notLinkedHeader: { - flexDirection: 'row', alignItems: 'center', paddingHorizontal: 4, paddingBottom: 2, gap: 8, - }, - // Service row (offload section) - serviceRow: { - flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12, - }, - // Auto backup + actionBtn: { borderWidth: 1, borderRadius: 6, paddingHorizontal: 14, paddingVertical: 6 }, + actionBtnText: { fontSize: 12, fontWeight: '600' }, + addBtn: { borderRadius: 6, paddingHorizontal: 14, paddingVertical: 6 }, + addBtnText: { color: '#fff', fontSize: 12, fontWeight: '600' }, + inlineForm: { paddingHorizontal: 14, paddingBottom: 14, borderTopWidth: StyleSheet.hairlineWidth }, + fieldLabel: { fontSize: 12, fontWeight: '600', marginTop: 10, marginBottom: 4 }, + fieldInput: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14 }, + saveButton: { borderRadius: 8, paddingVertical: 12, alignItems: 'center', marginTop: 14 }, + saveButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' }, checkRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12, borderBottomWidth: StyleSheet.hairlineWidth, }, - checkbox: { - width: 22, height: 22, borderRadius: 4, borderWidth: 2, - justifyContent: 'center', alignItems: 'center', marginRight: 12, - }, + checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, justifyContent: 'center', alignItems: 'center', marginRight: 12 }, checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' }, checkIcon: { fontSize: 18, marginRight: 8 }, checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 }, notConnected: { fontSize: 11 }, - // Logout logoutButton: { borderWidth: 1.5, borderRadius: 8, paddingVertical: 14, alignItems: 'center', marginTop: 24 }, logoutText: { fontSize: 15, fontWeight: '600' }, });