mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
feat: enhance onboarding and settings screens with NAS authentication and configuration
This commit is contained in:
1 parent
5158305086
commit
09a6d2a57b
2 files changed
+475
-230
No files matched your search
+183
-79
@@ -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 }) => {
|
||||
<Text style={[styles.description, { color: theme.textSecondary }]}>{steps[currentStep].description}</Text>
|
||||
</View>
|
||||
|
||||
{/* Step 0: Select services */}
|
||||
{/* ── Step 0: pick services ── */}
|
||||
{currentStep === 0 && (
|
||||
<View style={styles.cardsContainer}>
|
||||
<ServiceToggleCard theme={theme} icon="📷" name="Google Photos"
|
||||
@@ -101,40 +182,88 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
<ServiceToggleCard theme={theme} icon="💾" name="NAS Storage"
|
||||
description="Connect to your NAS photo server (REST API)"
|
||||
enabled={nasEnabled} onToggle={() => setNasEnabled(!nasEnabled)} />
|
||||
{nasEnabled && <NASSetupInstructions />}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Step 1: Configure each selected service via shared ServiceConnectCard */}
|
||||
{/* ── Step 1: connect each service ── */}
|
||||
{currentStep === 1 && (
|
||||
<View style={styles.cardsContainer}>
|
||||
|
||||
{googleEnabled && (
|
||||
<ServiceConnectCard
|
||||
serviceType="GOOGLE_PHOTOS"
|
||||
userId={userId}
|
||||
isLinked={googleConnected}
|
||||
onLinked={() => setGoogleConnected(true)}
|
||||
/>
|
||||
)}
|
||||
{iosEnabled && (
|
||||
<ServiceConnectCard
|
||||
serviceType="ICLOUD_PHOTOS"
|
||||
userId={userId}
|
||||
isLinked={iosConnected}
|
||||
onLinked={() => setIosConnected(true)}
|
||||
/>
|
||||
)}
|
||||
{nasEnabled && (
|
||||
<ServiceConnectCard
|
||||
serviceType="NAS"
|
||||
userId={userId}
|
||||
isLinked={nasConnected}
|
||||
onLinked={() => setNasConnected(true)}
|
||||
/>
|
||||
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
<View style={styles.configHeader}>
|
||||
<ServiceHealthIndicator status={googleHealth} size={10} />
|
||||
<Text style={styles.configIcon}>📷</Text>
|
||||
<Text style={[styles.configTitle, { color: theme.text }]}>Google Photos</Text>
|
||||
{googleConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
|
||||
</View>
|
||||
{!googleConnected && (
|
||||
<TouchableOpacity
|
||||
style={[styles.authButton, { backgroundColor: '#4285F4' }, loading && styles.buttonDisabled]}
|
||||
onPress={handleGoogleAuth} disabled={loading}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.authButtonText}>Sign in with Google</Text>}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Step 2: Auto-backup multi-checkbox */}
|
||||
{iosEnabled && (
|
||||
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
<View style={styles.configHeader}>
|
||||
<ServiceHealthIndicator status={iosHealth} size={10} />
|
||||
<Text style={styles.configIcon}>🍎</Text>
|
||||
<Text style={[styles.configTitle, { color: theme.text }]}>iCloud Photos</Text>
|
||||
{iosConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
|
||||
</View>
|
||||
{!iosConnected && (
|
||||
<TouchableOpacity
|
||||
style={[styles.authButton, { backgroundColor: '#333' }, loading && styles.buttonDisabled]}
|
||||
onPress={handleIosAuth} disabled={loading}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.authButtonText}>Connect iCloud</Text>}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{nasEnabled && (
|
||||
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
<View style={styles.configHeader}>
|
||||
<ServiceHealthIndicator status={nasHealth} size={10} />
|
||||
<Text style={styles.configIcon}>💾</Text>
|
||||
<Text style={[styles.configTitle, { color: theme.text }]}>NAS Storage</Text>
|
||||
{nasConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
|
||||
</View>
|
||||
{!nasConnected && (
|
||||
<>
|
||||
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Server Address</Text>
|
||||
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
placeholder="192.168.1.100" placeholderTextColor={theme.textTertiary}
|
||||
value={nasHost} onChangeText={setNasHost} editable={!loading} />
|
||||
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Port</Text>
|
||||
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
placeholder="9500" placeholderTextColor={theme.textTertiary}
|
||||
value={nasPort} onChangeText={setNasPort} editable={!loading} keyboardType="numeric" />
|
||||
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Username</Text>
|
||||
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
placeholder="Your username" placeholderTextColor={theme.textTertiary}
|
||||
value={nasUsername} onChangeText={setNasUsername} editable={!loading} autoCapitalize="none" />
|
||||
<TouchableOpacity
|
||||
style={[styles.authButton, { backgroundColor: '#0a7ea4' }, loading && styles.buttonDisabled]}
|
||||
onPress={handleNasAuth} disabled={loading}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.authButtonText}>Test & Connect</Text>}
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ── Step 2: auto-backup ── */}
|
||||
{currentStep === 2 && (
|
||||
<View style={styles.cardsContainer}>
|
||||
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
@@ -142,31 +271,22 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
Select which services new photos should automatically upload to. You can select multiple or none.
|
||||
</Text>
|
||||
{getConnectedServices().map((svc) => (
|
||||
<TouchableOpacity
|
||||
key={svc.type}
|
||||
<TouchableOpacity key={svc.type}
|
||||
style={[styles.checkRow, { borderBottomColor: theme.borderLight }]}
|
||||
onPress={() => toggleAutoBackup(svc.type)}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => toggleAutoBackup(svc.type)} activeOpacity={0.7}
|
||||
>
|
||||
<View style={[
|
||||
styles.checkbox, { borderColor: theme.primary },
|
||||
autoBackupServices.includes(svc.type) && { backgroundColor: theme.primary },
|
||||
]}>
|
||||
<View style={[styles.checkbox, { borderColor: theme.primary },
|
||||
autoBackupServices.includes(svc.type) && { backgroundColor: theme.primary }]}>
|
||||
{autoBackupServices.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>}
|
||||
</View>
|
||||
<Text style={styles.checkIcon}>{svc.icon}</Text>
|
||||
<Text style={[styles.checkLabel, { color: theme.text }]}>{svc.name}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
style={[styles.checkRow, { borderBottomWidth: 0 }]}
|
||||
onPress={() => setAutoBackupServices([])}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[
|
||||
styles.checkbox, { borderColor: theme.border },
|
||||
autoBackupServices.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary },
|
||||
]}>
|
||||
<TouchableOpacity style={[styles.checkRow, { borderBottomWidth: 0 }]}
|
||||
onPress={() => setAutoBackupServices([])} activeOpacity={0.7}>
|
||||
<View style={[styles.checkbox, { borderColor: theme.border },
|
||||
autoBackupServices.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary }]}>
|
||||
{autoBackupServices.length === 0 && <Text style={styles.checkmark}>✓</Text>}
|
||||
</View>
|
||||
<Text style={[styles.checkLabel, { color: theme.text }]}>Don't automatically back up</Text>
|
||||
@@ -183,17 +303,13 @@ const OnboardingScreen = ({ navigation, route }) => {
|
||||
onPress={() => setCurrentStep(currentStep - 1)} disabled={loading}>
|
||||
<Text style={[styles.backButtonText, { color: theme.textSecondary }]}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={{ flex: 1 }} />
|
||||
)}
|
||||
) : <View style={{ flex: 1 }} />}
|
||||
<TouchableOpacity
|
||||
style={[styles.nextButton, { backgroundColor: theme.primary }, loading && styles.buttonDisabled]}
|
||||
onPress={handleNext} disabled={loading}
|
||||
>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : (
|
||||
<Text style={styles.nextButtonText}>
|
||||
{currentStep === steps.length - 1 ? 'Get Started' : 'Next'}
|
||||
</Text>
|
||||
<Text style={styles.nextButtonText}>{currentStep === steps.length - 1 ? 'Get Started' : 'Next'}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -210,21 +326,15 @@ const ServiceToggleCard = ({ theme, icon, name, description, enabled, onToggle }
|
||||
<Text style={[styles.toggleDescription, { color: theme.textSecondary }]}>{description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={enabled}
|
||||
onValueChange={onToggle}
|
||||
<Switch value={enabled} onValueChange={onToggle}
|
||||
trackColor={{ false: theme.border, true: '#a8dab5' }}
|
||||
thumbColor={enabled ? '#34a853' : '#f4f3f4'}
|
||||
/>
|
||||
thumbColor={enabled ? '#34a853' : '#f4f3f4'} />
|
||||
</View>
|
||||
);
|
||||
|
||||
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' },
|
||||
|
||||
+292
-151
@@ -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 (
|
||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<View style={[styles.header, { backgroundColor: theme.background, borderBottomColor: theme.border }]}>
|
||||
@@ -151,12 +249,11 @@ const SettingsScreen = ({ navigation, route }) => {
|
||||
|
||||
<ScrollView style={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* ── Appearance ───────────────────────────────── */}
|
||||
{/* ── Appearance ── */}
|
||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>APPEARANCE</Text>
|
||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
{themeOptions.map((opt) => (
|
||||
<TouchableOpacity
|
||||
key={opt.key}
|
||||
<TouchableOpacity key={opt.key}
|
||||
style={[styles.themeRow, themeName === opt.key && { backgroundColor: theme.cardActive }, { borderBottomColor: theme.borderLight }]}
|
||||
onPress={() => setThemeName(opt.key)}
|
||||
>
|
||||
@@ -170,30 +267,26 @@ const SettingsScreen = ({ navigation, route }) => {
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* ── Services ─────────────────────────────────── */}
|
||||
{/* Always shows ALL services — connected ones get a manage strip, unconnected show ServiceConnectCard */}
|
||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>SERVICES</Text>
|
||||
<Text style={[styles.sectionHint, { color: theme.textTertiary }]}>
|
||||
Connect new services or manage existing ones at any time.
|
||||
</Text>
|
||||
|
||||
{ALL_SERVICES.map((svc) => {
|
||||
const acc = accounts.find((a) => a.service_type === svc.type);
|
||||
const health = serviceHealth[svc.type] || (acc?.is_enabled ? 'checking' : 'inactive');
|
||||
|
||||
if (acc) {
|
||||
// Already linked — show management card with enable/disable + remove + re-auth via ServiceConnectCard
|
||||
{/* ── Connected services ── */}
|
||||
{linkedAccounts.length > 0 && (
|
||||
<>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>CONNECTED SERVICES</Text>
|
||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
{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 (
|
||||
<View key={svc.type} style={[styles.managedCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
{/* Top row: health, icon, name, enable toggle */}
|
||||
<View style={styles.managedHeader}>
|
||||
<TouchableOpacity onPress={() => checkSingleHealth(acc).then((s) => setServiceHealth((p) => ({ ...p, [svc.type]: s })))}>
|
||||
<View key={acc.id}>
|
||||
{/* Main row */}
|
||||
<View style={[styles.serviceRow, !isLastRow && { borderBottomColor: theme.borderLight, borderBottomWidth: StyleSheet.hairlineWidth }]}>
|
||||
<TouchableOpacity onPress={() => recheckHealth(acc)} activeOpacity={0.6}>
|
||||
<ServiceHealthIndicator status={health} size={10} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.managedIcon}>{svc.icon}</Text>
|
||||
<View style={styles.managedInfo}>
|
||||
<Text style={[styles.managedName, { color: theme.text }]}>{svc.name}</Text>
|
||||
<Text style={[styles.managedStatus, { color: acc.is_enabled ? theme.accent : theme.textTertiary }]}>
|
||||
<Text style={styles.serviceIcon}>{getServiceIcon(acc.service_type)}</Text>
|
||||
<View style={styles.serviceInfo}>
|
||||
<Text style={[styles.serviceName, { color: theme.text }]}>{getServiceName(acc.service_type)}</Text>
|
||||
<Text style={[styles.serviceStatus, { color: acc.is_enabled ? theme.accent : theme.textTertiary }]}>
|
||||
{acc.is_enabled ? 'Enabled' : 'Disabled'}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -205,67 +298,135 @@ const SettingsScreen = ({ navigation, route }) => {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Re-auth / edit via shared ServiceConnectCard (starts collapsed since already linked) */}
|
||||
<ServiceConnectCard
|
||||
key={`${svc.type}-${reloadKey}`}
|
||||
serviceType={svc.type}
|
||||
userId={userId}
|
||||
existingData={acc.account_data}
|
||||
isLinked={true}
|
||||
showRemove={true}
|
||||
onLinked={() => { setReloadKey((k) => k + 1); loadData(); }}
|
||||
onRemoved={() => deleteAccount(acc)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
{/* Action buttons */}
|
||||
<View style={[styles.serviceActions, { borderBottomColor: theme.borderLight }]}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, { borderColor: theme.primary }]}
|
||||
onPress={() => {
|
||||
if (acc.service_type === 'NAS') {
|
||||
setExpandedEdit(isEditOpen ? null : acc.service_type);
|
||||
} else {
|
||||
saveReauth(acc.service_type);
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={[styles.actionBtnText, { color: theme.primary }]}>
|
||||
{acc.service_type === 'NAS' ? (isEditOpen ? 'Cancel' : 'Edit') : (loading ? '…' : 'Re-auth')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, { borderColor: theme.danger }]}
|
||||
onPress={() => deleteAccount(acc)}
|
||||
>
|
||||
<Text style={[styles.actionBtnText, { color: theme.danger }]}>Remove</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
// Not yet linked — show ServiceConnectCard directly so user can add it
|
||||
return (
|
||||
<View key={svc.type}>
|
||||
<View style={[styles.notLinkedHeader, { borderColor: theme.border }]}>
|
||||
<Text style={styles.managedIcon}>{svc.icon}</Text>
|
||||
<View style={styles.managedInfo}>
|
||||
<Text style={[styles.managedName, { color: theme.text }]}>{svc.name}</Text>
|
||||
<Text style={[styles.managedStatus, { color: theme.textTertiary }]}>Not connected</Text>
|
||||
{/* Inline NAS edit form */}
|
||||
{isEditOpen && acc.service_type === 'NAS' && (
|
||||
<View style={[styles.inlineForm, { borderTopColor: theme.borderLight }]}>
|
||||
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Server Address</Text>
|
||||
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
value={nasHost} onChangeText={setNasHost} placeholder="192.168.1.100" placeholderTextColor={theme.textTertiary} />
|
||||
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Port</Text>
|
||||
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
value={nasPort} onChangeText={setNasPort} keyboardType="numeric" placeholder="9500" placeholderTextColor={theme.textTertiary} />
|
||||
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Username</Text>
|
||||
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
value={nasUsername} onChangeText={setNasUsername} autoCapitalize="none" placeholder="Your username" placeholderTextColor={theme.textTertiary} />
|
||||
<TouchableOpacity style={[styles.saveButton, { backgroundColor: theme.primary }]}
|
||||
onPress={() => saveReauth('NAS')} disabled={loading}>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.saveButtonText}>Save Changes</Text>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<ServiceConnectCard
|
||||
key={`${svc.type}-${reloadKey}`}
|
||||
serviceType={svc.type}
|
||||
userId={userId}
|
||||
isLinked={false}
|
||||
onLinked={() => { setReloadKey((k) => k + 1); loadData(); }}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── NAS Offloading ────────────────────────────── */}
|
||||
{/* ── Add services (only shows services not yet connected) ── */}
|
||||
{unlinkedServices.length > 0 && (
|
||||
<>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>ADD SERVICES</Text>
|
||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
{unlinkedServices.map((svc, idx) => {
|
||||
const isLast = idx === unlinkedServices.length - 1;
|
||||
const isOpen = expandedAdd === svc.type;
|
||||
return (
|
||||
<View key={svc.type}>
|
||||
{/* Service row with Add button */}
|
||||
<View style={[styles.serviceRow, !isLast && !isOpen && { borderBottomColor: theme.borderLight, borderBottomWidth: StyleSheet.hairlineWidth }]}>
|
||||
<Text style={styles.serviceIcon}>{svc.icon}</Text>
|
||||
<View style={styles.serviceInfo}>
|
||||
<Text style={[styles.serviceName, { color: theme.text }]}>{svc.name}</Text>
|
||||
<Text style={[styles.serviceStatus, { color: theme.textTertiary }]}>Not connected</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.addBtn, { backgroundColor: theme.primary }]}
|
||||
onPress={() => {
|
||||
if (svc.type === 'NAS') {
|
||||
setNasHost(''); setNasPort('9500'); setNasUsername('');
|
||||
setExpandedAdd(isOpen ? null : svc.type);
|
||||
} else {
|
||||
saveNewService(svc.type);
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.addBtnText}>{loading && expandedAdd === svc.type ? '…' : 'Add'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Inline NAS add form */}
|
||||
{isOpen && svc.type === 'NAS' && (
|
||||
<View style={[styles.inlineForm, { borderTopColor: theme.borderLight }]}>
|
||||
<NASSetupInstructions />
|
||||
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Server Address</Text>
|
||||
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
value={nasHost} onChangeText={setNasHost} placeholder="192.168.1.100" placeholderTextColor={theme.textTertiary} />
|
||||
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Port</Text>
|
||||
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
value={nasPort} onChangeText={setNasPort} keyboardType="numeric" placeholder="9500" placeholderTextColor={theme.textTertiary} />
|
||||
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Username</Text>
|
||||
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
value={nasUsername} onChangeText={setNasUsername} autoCapitalize="none" placeholder="Your username" placeholderTextColor={theme.textTertiary} />
|
||||
<TouchableOpacity style={[styles.saveButton, { backgroundColor: '#0a7ea4' }]}
|
||||
onPress={() => saveNewService('NAS')} disabled={loading}>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.saveButtonText}>Test & Connect</Text>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── NAS Offloading ── */}
|
||||
{hasNasConnected && (
|
||||
<>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>NAS OFFLOADING</Text>
|
||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
<View style={styles.serviceRow}>
|
||||
<Text style={styles.managedIcon}>📷</Text>
|
||||
<View style={styles.managedInfo}>
|
||||
<Text style={[styles.managedName, { color: theme.text }]}>Offload Google Photos to NAS</Text>
|
||||
<Text style={[styles.managedStatus, { color: theme.textSecondary }]}>
|
||||
Fetch Google Photos data through NAS server
|
||||
</Text>
|
||||
<Text style={styles.serviceIcon}>📷</Text>
|
||||
<View style={styles.serviceInfo}>
|
||||
<Text style={[styles.serviceName, { color: theme.text }]}>Offload Google Photos to NAS</Text>
|
||||
<Text style={[styles.serviceStatus, { color: theme.textSecondary }]}>Fetch Google Photos data through NAS server</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={nasOffload}
|
||||
onValueChange={setNasOffload}
|
||||
<Switch value={nasOffload} onValueChange={setNasOffload}
|
||||
trackColor={{ false: theme.border, true: '#a8dab5' }}
|
||||
thumbColor={nasOffload ? theme.accent : '#f4f3f4'}
|
||||
/>
|
||||
thumbColor={nasOffload ? theme.accent : '#f4f3f4'} />
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Auto Backup ───────────────────────────────── */}
|
||||
{/* ── Auto Backup ── */}
|
||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>AUTOMATIC BACKUP</Text>
|
||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||
<Text style={[styles.cardSubtext, { color: theme.textSecondary }]}>
|
||||
@@ -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 (
|
||||
<TouchableOpacity
|
||||
key={svc.type}
|
||||
<TouchableOpacity key={svc.type}
|
||||
style={[styles.checkRow, { borderBottomColor: theme.borderLight }]}
|
||||
onPress={() => isConnected && toggleAutoBackup(svc.type)}
|
||||
disabled={!isConnected}
|
||||
activeOpacity={0.7}
|
||||
disabled={!isConnected} activeOpacity={0.7}
|
||||
>
|
||||
<View style={[
|
||||
styles.checkbox, { borderColor: isConnected ? theme.primary : theme.border },
|
||||
autoBackup.includes(svc.type) && { backgroundColor: theme.primary, borderColor: theme.primary },
|
||||
]}>
|
||||
<View style={[styles.checkbox, { borderColor: isConnected ? theme.primary : theme.border },
|
||||
autoBackup.includes(svc.type) && { backgroundColor: theme.primary, borderColor: theme.primary }]}>
|
||||
{autoBackup.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>}
|
||||
</View>
|
||||
<Text style={styles.checkIcon}>{svc.icon}</Text>
|
||||
<Text style={[styles.checkLabel, { color: isConnected ? theme.text : theme.textTertiary }]}>
|
||||
{svc.name}
|
||||
</Text>
|
||||
<Text style={[styles.checkLabel, { color: isConnected ? theme.text : theme.textTertiary }]}>{svc.name}</Text>
|
||||
{!isConnected && <Text style={[styles.notConnected, { color: theme.textTertiary }]}>Not connected</Text>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
<TouchableOpacity
|
||||
style={[styles.checkRow, { borderBottomWidth: 0 }]}
|
||||
<TouchableOpacity style={[styles.checkRow, { borderBottomWidth: 0 }]}
|
||||
onPress={() => { setAutoBackup([]); DatabaseService.setAutoBackupServices(userId, []); }}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[
|
||||
styles.checkbox, { borderColor: theme.border },
|
||||
autoBackup.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary },
|
||||
]}>
|
||||
<View style={[styles.checkbox, { borderColor: theme.border },
|
||||
autoBackup.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary }]}>
|
||||
{autoBackup.length === 0 && <Text style={styles.checkmark}>✓</Text>}
|
||||
</View>
|
||||
<Text style={[styles.checkLabel, { color: theme.text, marginLeft: 0 }]}>
|
||||
Don't automatically back up
|
||||
</Text>
|
||||
<Text style={[styles.checkLabel, { color: theme.text, marginLeft: 0 }]}>Don't automatically back up</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* ── Sign Out ──────────────────────────────────── */}
|
||||
{/* ── Sign Out ── */}
|
||||
<TouchableOpacity style={[styles.logoutButton, { borderColor: theme.danger }]} onPress={handleLogout}>
|
||||
<Text style={[styles.logoutText, { color: theme.danger }]}>Sign Out</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user