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' },
});