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 React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
StyleSheet, View, Text, ScrollView, TouchableOpacity, Alert,
|
StyleSheet, View, Text, ScrollView, TouchableOpacity, Alert,
|
||||||
ActivityIndicator, Switch,
|
ActivityIndicator, Switch, TextInput,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useTheme } from '../services/ThemeContext';
|
import { useTheme } from '../services/ThemeContext';
|
||||||
import DatabaseService from '../services/DatabaseService';
|
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 OnboardingScreen = ({ navigation, route }) => {
|
||||||
const { userId } = route.params;
|
const { userId } = route.params;
|
||||||
@@ -18,11 +21,21 @@ const OnboardingScreen = ({ navigation, route }) => {
|
|||||||
const [iosEnabled, setIosEnabled] = useState(false);
|
const [iosEnabled, setIosEnabled] = useState(false);
|
||||||
const [nasEnabled, setNasEnabled] = 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 [googleConnected, setGoogleConnected] = useState(false);
|
||||||
const [iosConnected, setIosConnected] = useState(false);
|
const [iosConnected, setIosConnected] = useState(false);
|
||||||
const [nasConnected, setNasConnected] = 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
|
// Step 2 — auto-backup checklist
|
||||||
const [autoBackupServices, setAutoBackupServices] = useState([]);
|
const [autoBackupServices, setAutoBackupServices] = useState([]);
|
||||||
|
|
||||||
@@ -40,6 +53,74 @@ const OnboardingScreen = ({ navigation, route }) => {
|
|||||||
return services;
|
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) => {
|
const toggleAutoBackup = (serviceType) => {
|
||||||
setAutoBackupServices((prev) =>
|
setAutoBackupServices((prev) =>
|
||||||
prev.includes(serviceType) ? prev.filter((s) => s !== serviceType) : [...prev, serviceType]
|
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>
|
<Text style={[styles.description, { color: theme.textSecondary }]}>{steps[currentStep].description}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Step 0: Select services */}
|
{/* ── Step 0: pick services ── */}
|
||||||
{currentStep === 0 && (
|
{currentStep === 0 && (
|
||||||
<View style={styles.cardsContainer}>
|
<View style={styles.cardsContainer}>
|
||||||
<ServiceToggleCard theme={theme} icon="📷" name="Google Photos"
|
<ServiceToggleCard theme={theme} icon="📷" name="Google Photos"
|
||||||
@@ -101,40 +182,88 @@ const OnboardingScreen = ({ navigation, route }) => {
|
|||||||
<ServiceToggleCard theme={theme} icon="💾" name="NAS Storage"
|
<ServiceToggleCard theme={theme} icon="💾" name="NAS Storage"
|
||||||
description="Connect to your NAS photo server (REST API)"
|
description="Connect to your NAS photo server (REST API)"
|
||||||
enabled={nasEnabled} onToggle={() => setNasEnabled(!nasEnabled)} />
|
enabled={nasEnabled} onToggle={() => setNasEnabled(!nasEnabled)} />
|
||||||
|
{nasEnabled && <NASSetupInstructions />}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 1: Configure each selected service via shared ServiceConnectCard */}
|
{/* ── Step 1: connect each service ── */}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<View style={styles.cardsContainer}>
|
<View style={styles.cardsContainer}>
|
||||||
|
|
||||||
{googleEnabled && (
|
{googleEnabled && (
|
||||||
<ServiceConnectCard
|
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||||
serviceType="GOOGLE_PHOTOS"
|
<View style={styles.configHeader}>
|
||||||
userId={userId}
|
<ServiceHealthIndicator status={googleHealth} size={10} />
|
||||||
isLinked={googleConnected}
|
<Text style={styles.configIcon}>📷</Text>
|
||||||
onLinked={() => setGoogleConnected(true)}
|
<Text style={[styles.configTitle, { color: theme.text }]}>Google Photos</Text>
|
||||||
/>
|
{googleConnected && <Text style={[styles.connectedBadge, { color: theme.accent }]}>✓ Connected</Text>}
|
||||||
)}
|
</View>
|
||||||
{iosEnabled && (
|
{!googleConnected && (
|
||||||
<ServiceConnectCard
|
<TouchableOpacity
|
||||||
serviceType="ICLOUD_PHOTOS"
|
style={[styles.authButton, { backgroundColor: '#4285F4' }, loading && styles.buttonDisabled]}
|
||||||
userId={userId}
|
onPress={handleGoogleAuth} disabled={loading}
|
||||||
isLinked={iosConnected}
|
>
|
||||||
onLinked={() => setIosConnected(true)}
|
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.authButtonText}>Sign in with Google</Text>}
|
||||||
/>
|
</TouchableOpacity>
|
||||||
)}
|
|
||||||
{nasEnabled && (
|
|
||||||
<ServiceConnectCard
|
|
||||||
serviceType="NAS"
|
|
||||||
userId={userId}
|
|
||||||
isLinked={nasConnected}
|
|
||||||
onLinked={() => setNasConnected(true)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</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 && (
|
{currentStep === 2 && (
|
||||||
<View style={styles.cardsContainer}>
|
<View style={styles.cardsContainer}>
|
||||||
<View style={[styles.configCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
<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.
|
Select which services new photos should automatically upload to. You can select multiple or none.
|
||||||
</Text>
|
</Text>
|
||||||
{getConnectedServices().map((svc) => (
|
{getConnectedServices().map((svc) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity key={svc.type}
|
||||||
key={svc.type}
|
|
||||||
style={[styles.checkRow, { borderBottomColor: theme.borderLight }]}
|
style={[styles.checkRow, { borderBottomColor: theme.borderLight }]}
|
||||||
onPress={() => toggleAutoBackup(svc.type)}
|
onPress={() => toggleAutoBackup(svc.type)} activeOpacity={0.7}
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[styles.checkbox, { borderColor: theme.primary },
|
||||||
styles.checkbox, { borderColor: theme.primary },
|
autoBackupServices.includes(svc.type) && { backgroundColor: theme.primary }]}>
|
||||||
autoBackupServices.includes(svc.type) && { backgroundColor: theme.primary },
|
|
||||||
]}>
|
|
||||||
{autoBackupServices.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>}
|
{autoBackupServices.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>}
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.checkIcon}>{svc.icon}</Text>
|
<Text style={styles.checkIcon}>{svc.icon}</Text>
|
||||||
<Text style={[styles.checkLabel, { color: theme.text }]}>{svc.name}</Text>
|
<Text style={[styles.checkLabel, { color: theme.text }]}>{svc.name}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
<TouchableOpacity
|
<TouchableOpacity style={[styles.checkRow, { borderBottomWidth: 0 }]}
|
||||||
style={[styles.checkRow, { borderBottomWidth: 0 }]}
|
onPress={() => setAutoBackupServices([])} activeOpacity={0.7}>
|
||||||
onPress={() => setAutoBackupServices([])}
|
<View style={[styles.checkbox, { borderColor: theme.border },
|
||||||
activeOpacity={0.7}
|
autoBackupServices.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary }]}>
|
||||||
>
|
|
||||||
<View style={[
|
|
||||||
styles.checkbox, { borderColor: theme.border },
|
|
||||||
autoBackupServices.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary },
|
|
||||||
]}>
|
|
||||||
{autoBackupServices.length === 0 && <Text style={styles.checkmark}>✓</Text>}
|
{autoBackupServices.length === 0 && <Text style={styles.checkmark}>✓</Text>}
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.checkLabel, { color: theme.text }]}>Don't automatically back up</Text>
|
<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}>
|
onPress={() => setCurrentStep(currentStep - 1)} disabled={loading}>
|
||||||
<Text style={[styles.backButtonText, { color: theme.textSecondary }]}>Back</Text>
|
<Text style={[styles.backButtonText, { color: theme.textSecondary }]}>Back</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : <View style={{ flex: 1 }} />}
|
||||||
<View style={{ flex: 1 }} />
|
|
||||||
)}
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.nextButton, { backgroundColor: theme.primary }, loading && styles.buttonDisabled]}
|
style={[styles.nextButton, { backgroundColor: theme.primary }, loading && styles.buttonDisabled]}
|
||||||
onPress={handleNext} disabled={loading}
|
onPress={handleNext} disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? <ActivityIndicator color="#fff" /> : (
|
{loading ? <ActivityIndicator color="#fff" /> : (
|
||||||
<Text style={styles.nextButtonText}>
|
<Text style={styles.nextButtonText}>{currentStep === steps.length - 1 ? 'Get Started' : 'Next'}</Text>
|
||||||
{currentStep === steps.length - 1 ? 'Get Started' : 'Next'}
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -210,21 +326,15 @@ const ServiceToggleCard = ({ theme, icon, name, description, enabled, onToggle }
|
|||||||
<Text style={[styles.toggleDescription, { color: theme.textSecondary }]}>{description}</Text>
|
<Text style={[styles.toggleDescription, { color: theme.textSecondary }]}>{description}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch value={enabled} onValueChange={onToggle}
|
||||||
value={enabled}
|
|
||||||
onValueChange={onToggle}
|
|
||||||
trackColor={{ false: theme.border, true: '#a8dab5' }}
|
trackColor={{ false: theme.border, true: '#a8dab5' }}
|
||||||
thumbColor={enabled ? '#34a853' : '#f4f3f4'}
|
thumbColor={enabled ? '#34a853' : '#f4f3f4'} />
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1 },
|
container: { flex: 1 },
|
||||||
progressBar: {
|
progressBar: { flexDirection: 'row', justifyContent: 'center', gap: 8, paddingTop: 60, paddingBottom: 8 },
|
||||||
flexDirection: 'row', justifyContent: 'center', gap: 8,
|
|
||||||
paddingTop: 60, paddingBottom: 8,
|
|
||||||
},
|
|
||||||
progressDot: { width: 32, height: 4, borderRadius: 2 },
|
progressDot: { width: 32, height: 4, borderRadius: 2 },
|
||||||
content: { flex: 1, paddingHorizontal: 20 },
|
content: { flex: 1, paddingHorizontal: 20 },
|
||||||
header: { marginBottom: 24 },
|
header: { marginBottom: 24 },
|
||||||
@@ -232,35 +342,29 @@ const styles = StyleSheet.create({
|
|||||||
title: { fontSize: 26, fontWeight: '700', marginBottom: 6 },
|
title: { fontSize: 26, fontWeight: '700', marginBottom: 6 },
|
||||||
description: { fontSize: 14 },
|
description: { fontSize: 14 },
|
||||||
cardsContainer: { gap: 12 },
|
cardsContainer: { gap: 12 },
|
||||||
toggleCard: {
|
toggleCard: { borderRadius: 12, padding: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderWidth: 1.5 },
|
||||||
borderRadius: 12, padding: 16, flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between', alignItems: 'center', borderWidth: 1.5,
|
|
||||||
},
|
|
||||||
toggleCardContent: { flex: 1, flexDirection: 'row', alignItems: 'center' },
|
toggleCardContent: { flex: 1, flexDirection: 'row', alignItems: 'center' },
|
||||||
toggleIcon: { fontSize: 28, marginRight: 14 },
|
toggleIcon: { fontSize: 28, marginRight: 14 },
|
||||||
toggleInfo: { flex: 1 },
|
toggleInfo: { flex: 1 },
|
||||||
toggleName: { fontSize: 15, fontWeight: '600', marginBottom: 2 },
|
toggleName: { fontSize: 15, fontWeight: '600', marginBottom: 2 },
|
||||||
toggleDescription: { fontSize: 12 },
|
toggleDescription: { fontSize: 12 },
|
||||||
configCard: { borderRadius: 12, padding: 16, borderWidth: 1 },
|
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 },
|
checklistInfo: { fontSize: 13, marginBottom: 8 },
|
||||||
checkRow: {
|
checkRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth },
|
||||||
flexDirection: 'row', alignItems: 'center', paddingVertical: 14,
|
checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, justifyContent: 'center', alignItems: 'center', marginRight: 12 },
|
||||||
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' },
|
checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' },
|
||||||
checkIcon: { fontSize: 18, marginRight: 8 },
|
checkIcon: { fontSize: 18, marginRight: 8 },
|
||||||
checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 },
|
checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 },
|
||||||
footer: {
|
footer: { flexDirection: 'row', paddingHorizontal: 20, paddingVertical: 14, borderTopWidth: 1, gap: 12 },
|
||||||
flexDirection: 'row', paddingHorizontal: 20, paddingVertical: 14,
|
backButton: { flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center', borderWidth: 1 },
|
||||||
borderTopWidth: 1, gap: 12,
|
|
||||||
},
|
|
||||||
backButton: {
|
|
||||||
flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center', borderWidth: 1,
|
|
||||||
},
|
|
||||||
backButtonText: { fontSize: 15, fontWeight: '600' },
|
backButtonText: { fontSize: 15, fontWeight: '600' },
|
||||||
nextButton: { flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center' },
|
nextButton: { flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center' },
|
||||||
nextButtonText: { fontSize: 15, fontWeight: '600', color: '#fff' },
|
nextButtonText: { fontSize: 15, fontWeight: '600', color: '#fff' },
|
||||||
|
|||||||
+292
-151
@@ -1,32 +1,42 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
StyleSheet, View, Text, ScrollView, TouchableOpacity, Switch, Alert, ActivityIndicator,
|
StyleSheet, View, Text, ScrollView, TouchableOpacity, Switch, TextInput,
|
||||||
|
Alert, ActivityIndicator,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import { useTheme } from '../services/ThemeContext';
|
import { useTheme } from '../services/ThemeContext';
|
||||||
import DatabaseService from '../services/DatabaseService';
|
import DatabaseService from '../services/DatabaseService';
|
||||||
import ServiceConnectCard from '../components/ServiceConnectCard';
|
import NASSetupInstructions from '../components/NASSetupInstructions';
|
||||||
import ServiceHealthIndicator from '../components/ServiceHealthIndicator';
|
import ServiceHealthIndicator from '../components/ServiceHealthIndicator';
|
||||||
import NASAUTH from '../AUTH/NASAUTH';
|
import NASAUTH from '../AUTH/NASAUTH';
|
||||||
import GoogleAUTH from '../AUTH/GoogleAUTH';
|
import GoogleAUTH from '../AUTH/GoogleAUTH';
|
||||||
import { getServiceIcon, getServiceName } from '../UI/ServiceHelpers';
|
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 = [
|
const ALL_SERVICES = [
|
||||||
{ type: 'GOOGLE_PHOTOS', icon: '📷', name: 'Google Photos', desc: 'Access your Google Photos library' },
|
{ type: 'GOOGLE_PHOTOS', icon: '📷', name: 'Google Photos' },
|
||||||
{ type: 'ICLOUD_PHOTOS', icon: '🍎', name: 'iCloud Photos', desc: 'Access your iCloud photo library' },
|
{ type: 'ICLOUD_PHOTOS', icon: '🍎', name: 'iCloud Photos' },
|
||||||
{ type: 'NAS', icon: '💾', name: 'NAS Storage', desc: 'Connect to your NAS photo server' },
|
{ type: 'NAS', icon: '💾', name: 'NAS Storage' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SettingsScreen = ({ navigation, route }) => {
|
const SettingsScreen = ({ navigation, route }) => {
|
||||||
const userId = route?.params?.userId;
|
const userId = route?.params?.userId;
|
||||||
const { theme, themeName, setThemeName } = useTheme();
|
const { theme, themeName, setThemeName } = useTheme();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [accounts, setAccounts] = useState([]);
|
const [accounts, setAccounts] = useState([]);
|
||||||
const [autoBackup, setAutoBackup] = useState([]);
|
const [autoBackup, setAutoBackup] = useState([]);
|
||||||
const [nasOffload, setNasOffload] = useState(false);
|
const [nasOffload, setNasOffload] = useState(false);
|
||||||
const [serviceHealth, setServiceHealth] = useState({});
|
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(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -40,64 +50,72 @@ const SettingsScreen = ({ navigation, route }) => {
|
|||||||
setAccounts(allAccounts);
|
setAccounts(allAccounts);
|
||||||
const backup = await DatabaseService.getAutoBackupServices(userId);
|
const backup = await DatabaseService.getAutoBackupServices(userId);
|
||||||
setAutoBackup(backup);
|
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);
|
checkAllServiceHealth(allAccounts);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading settings:', err);
|
console.error('Error loading settings:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Health checks ───────────────────────────────────────
|
// ─── Health ──────────────────────────────────────────────
|
||||||
const checkAllServiceHealth = async (accs) => {
|
const checkAllServiceHealth = async (accs) => {
|
||||||
for (const acc of accs) {
|
for (const acc of accs) {
|
||||||
if (!acc.is_enabled) {
|
if (!acc.is_enabled) {
|
||||||
setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'inactive' }));
|
setServiceHealth((p) => ({ ...p, [acc.service_type]: 'inactive' }));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'checking' }));
|
setServiceHealth((p) => ({ ...p, [acc.service_type]: 'checking' }));
|
||||||
const status = await checkSingleHealth(acc);
|
const s = await checkSingleHealth(acc);
|
||||||
setServiceHealth((prev) => ({ ...prev, [acc.service_type]: status }));
|
setServiceHealth((p) => ({ ...p, [acc.service_type]: s }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkSingleHealth = async (acc) => {
|
const checkSingleHealth = async (acc) => {
|
||||||
try {
|
try {
|
||||||
if (acc.service_type === 'GOOGLE_PHOTOS') {
|
if (acc.service_type === 'GOOGLE_PHOTOS') {
|
||||||
const gAuth = new GoogleAUTH();
|
const g = new GoogleAUTH();
|
||||||
gAuth.accessToken = acc.account_data?.accessToken;
|
g.accessToken = acc.account_data?.accessToken;
|
||||||
gAuth.refreshToken = acc.account_data?.refreshToken;
|
g.refreshToken = acc.account_data?.refreshToken;
|
||||||
gAuth.expiresAt = acc.account_data?.expiresAt || 0;
|
g.expiresAt = acc.account_data?.expiresAt || 0;
|
||||||
const info = await gAuth.getUserInfo();
|
const info = await g.getUserInfo();
|
||||||
return info?.email ? 'healthy' : 'error';
|
return info?.email ? 'healthy' : 'error';
|
||||||
}
|
}
|
||||||
if (acc.service_type === 'NAS') {
|
if (acc.service_type === 'NAS') {
|
||||||
const nasAuth = new NASAUTH();
|
const n = new NASAUTH();
|
||||||
const d = acc.account_data || {};
|
const d = acc.account_data || {};
|
||||||
await nasAuth.authenticate(d.host, d.port, d.username);
|
await n.authenticate(d.host, d.port, d.username);
|
||||||
const healthy = await nasAuth.healthCheck();
|
const ok = await n.healthCheck();
|
||||||
await nasAuth.revokeAccess();
|
await n.revokeAccess();
|
||||||
return healthy ? 'healthy' : 'error';
|
return ok ? 'healthy' : 'error';
|
||||||
}
|
}
|
||||||
if (acc.service_type === 'ICLOUD_PHOTOS') return 'healthy';
|
if (acc.service_type === 'ICLOUD_PHOTOS') return 'healthy';
|
||||||
return 'inactive';
|
return 'inactive';
|
||||||
} catch {
|
} catch { return 'error'; }
|
||||||
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) => {
|
const toggleService = async (accountId, currentEnabled) => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled);
|
await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled);
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch {
|
} catch { Alert.alert('Error', 'Failed to update service'); }
|
||||||
Alert.alert('Error', 'Failed to update service');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteAccount = (acc) => {
|
const deleteAccount = (acc) => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Remove Service',
|
'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' },
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
{
|
{
|
||||||
@@ -105,23 +123,99 @@ const SettingsScreen = ({ navigation, route }) => {
|
|||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
try {
|
try {
|
||||||
await DatabaseService.removeLinkedAccount(acc.id);
|
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();
|
await loadData();
|
||||||
} catch {
|
} catch { Alert.alert('Error', 'Failed to remove service'); }
|
||||||
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 toggleAutoBackup = async (serviceType) => {
|
||||||
const updated = autoBackup.includes(serviceType)
|
const updated = autoBackup.includes(serviceType)
|
||||||
? autoBackup.filter((s) => s !== serviceType)
|
? autoBackup.filter((s) => s !== serviceType)
|
||||||
: [...autoBackup, serviceType];
|
: [...autoBackup, serviceType];
|
||||||
setAutoBackup(updated);
|
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 = () => {
|
const handleLogout = () => {
|
||||||
@@ -139,6 +233,10 @@ const SettingsScreen = ({ navigation, route }) => {
|
|||||||
|
|
||||||
const hasNasConnected = accounts.some((a) => a.service_type === 'NAS' && a.is_enabled);
|
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 (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={[styles.header, { backgroundColor: theme.background, borderBottomColor: theme.border }]}>
|
<View style={[styles.header, { backgroundColor: theme.background, borderBottomColor: theme.border }]}>
|
||||||
@@ -151,12 +249,11 @@ const SettingsScreen = ({ navigation, route }) => {
|
|||||||
|
|
||||||
<ScrollView style={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||||
|
|
||||||
{/* ── Appearance ───────────────────────────────── */}
|
{/* ── Appearance ── */}
|
||||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>APPEARANCE</Text>
|
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>APPEARANCE</Text>
|
||||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||||
{themeOptions.map((opt) => (
|
{themeOptions.map((opt) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity key={opt.key}
|
||||||
key={opt.key}
|
|
||||||
style={[styles.themeRow, themeName === opt.key && { backgroundColor: theme.cardActive }, { borderBottomColor: theme.borderLight }]}
|
style={[styles.themeRow, themeName === opt.key && { backgroundColor: theme.cardActive }, { borderBottomColor: theme.borderLight }]}
|
||||||
onPress={() => setThemeName(opt.key)}
|
onPress={() => setThemeName(opt.key)}
|
||||||
>
|
>
|
||||||
@@ -170,30 +267,26 @@ const SettingsScreen = ({ navigation, route }) => {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ── Services ─────────────────────────────────── */}
|
{/* ── Connected services ── */}
|
||||||
{/* Always shows ALL services — connected ones get a manage strip, unconnected show ServiceConnectCard */}
|
{linkedAccounts.length > 0 && (
|
||||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>SERVICES</Text>
|
<>
|
||||||
<Text style={[styles.sectionHint, { color: theme.textTertiary }]}>
|
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>CONNECTED SERVICES</Text>
|
||||||
Connect new services or manage existing ones at any time.
|
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||||
</Text>
|
{linkedAccounts.map((acc, idx) => {
|
||||||
|
const health = serviceHealth[acc.service_type] || (acc.is_enabled ? 'checking' : 'inactive');
|
||||||
{ALL_SERVICES.map((svc) => {
|
const isLastRow = idx === linkedAccounts.length - 1 && expandedEdit !== acc.service_type;
|
||||||
const acc = accounts.find((a) => a.service_type === svc.type);
|
const isEditOpen = expandedEdit === acc.service_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
|
|
||||||
return (
|
return (
|
||||||
<View key={svc.type} style={[styles.managedCard, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
<View key={acc.id}>
|
||||||
{/* Top row: health, icon, name, enable toggle */}
|
{/* Main row */}
|
||||||
<View style={styles.managedHeader}>
|
<View style={[styles.serviceRow, !isLastRow && { borderBottomColor: theme.borderLight, borderBottomWidth: StyleSheet.hairlineWidth }]}>
|
||||||
<TouchableOpacity onPress={() => checkSingleHealth(acc).then((s) => setServiceHealth((p) => ({ ...p, [svc.type]: s })))}>
|
<TouchableOpacity onPress={() => recheckHealth(acc)} activeOpacity={0.6}>
|
||||||
<ServiceHealthIndicator status={health} size={10} />
|
<ServiceHealthIndicator status={health} size={10} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.managedIcon}>{svc.icon}</Text>
|
<Text style={styles.serviceIcon}>{getServiceIcon(acc.service_type)}</Text>
|
||||||
<View style={styles.managedInfo}>
|
<View style={styles.serviceInfo}>
|
||||||
<Text style={[styles.managedName, { color: theme.text }]}>{svc.name}</Text>
|
<Text style={[styles.serviceName, { color: theme.text }]}>{getServiceName(acc.service_type)}</Text>
|
||||||
<Text style={[styles.managedStatus, { color: acc.is_enabled ? theme.accent : theme.textTertiary }]}>
|
<Text style={[styles.serviceStatus, { color: acc.is_enabled ? theme.accent : theme.textTertiary }]}>
|
||||||
{acc.is_enabled ? 'Enabled' : 'Disabled'}
|
{acc.is_enabled ? 'Enabled' : 'Disabled'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -205,67 +298,135 @@ const SettingsScreen = ({ navigation, route }) => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Re-auth / edit via shared ServiceConnectCard (starts collapsed since already linked) */}
|
{/* Action buttons */}
|
||||||
<ServiceConnectCard
|
<View style={[styles.serviceActions, { borderBottomColor: theme.borderLight }]}>
|
||||||
key={`${svc.type}-${reloadKey}`}
|
<TouchableOpacity
|
||||||
serviceType={svc.type}
|
style={[styles.actionBtn, { borderColor: theme.primary }]}
|
||||||
userId={userId}
|
onPress={() => {
|
||||||
existingData={acc.account_data}
|
if (acc.service_type === 'NAS') {
|
||||||
isLinked={true}
|
setExpandedEdit(isEditOpen ? null : acc.service_type);
|
||||||
showRemove={true}
|
} else {
|
||||||
onLinked={() => { setReloadKey((k) => k + 1); loadData(); }}
|
saveReauth(acc.service_type);
|
||||||
onRemoved={() => deleteAccount(acc)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
|
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
|
{/* Inline NAS edit form */}
|
||||||
return (
|
{isEditOpen && acc.service_type === 'NAS' && (
|
||||||
<View key={svc.type}>
|
<View style={[styles.inlineForm, { borderTopColor: theme.borderLight }]}>
|
||||||
<View style={[styles.notLinkedHeader, { borderColor: theme.border }]}>
|
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Server Address</Text>
|
||||||
<Text style={styles.managedIcon}>{svc.icon}</Text>
|
<TextInput style={[styles.fieldInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||||
<View style={styles.managedInfo}>
|
value={nasHost} onChangeText={setNasHost} placeholder="192.168.1.100" placeholderTextColor={theme.textTertiary} />
|
||||||
<Text style={[styles.managedName, { color: theme.text }]}>{svc.name}</Text>
|
<Text style={[styles.fieldLabel, { color: theme.textSecondary }]}>Port</Text>
|
||||||
<Text style={[styles.managedStatus, { color: theme.textTertiary }]}>Not connected</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>
|
||||||
</View>
|
)}
|
||||||
<ServiceConnectCard
|
|
||||||
key={`${svc.type}-${reloadKey}`}
|
|
||||||
serviceType={svc.type}
|
|
||||||
userId={userId}
|
|
||||||
isLinked={false}
|
|
||||||
onLinked={() => { setReloadKey((k) => k + 1); loadData(); }}
|
|
||||||
/>
|
|
||||||
</View>
|
</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 && (
|
{hasNasConnected && (
|
||||||
<>
|
<>
|
||||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>NAS OFFLOADING</Text>
|
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>NAS OFFLOADING</Text>
|
||||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||||
<View style={styles.serviceRow}>
|
<View style={styles.serviceRow}>
|
||||||
<Text style={styles.managedIcon}>📷</Text>
|
<Text style={styles.serviceIcon}>📷</Text>
|
||||||
<View style={styles.managedInfo}>
|
<View style={styles.serviceInfo}>
|
||||||
<Text style={[styles.managedName, { color: theme.text }]}>Offload Google Photos to NAS</Text>
|
<Text style={[styles.serviceName, { color: theme.text }]}>Offload Google Photos to NAS</Text>
|
||||||
<Text style={[styles.managedStatus, { color: theme.textSecondary }]}>
|
<Text style={[styles.serviceStatus, { color: theme.textSecondary }]}>Fetch Google Photos data through NAS server</Text>
|
||||||
Fetch Google Photos data through NAS server
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch value={nasOffload} onValueChange={setNasOffload}
|
||||||
value={nasOffload}
|
|
||||||
onValueChange={setNasOffload}
|
|
||||||
trackColor={{ false: theme.border, true: '#a8dab5' }}
|
trackColor={{ false: theme.border, true: '#a8dab5' }}
|
||||||
thumbColor={nasOffload ? theme.accent : '#f4f3f4'}
|
thumbColor={nasOffload ? theme.accent : '#f4f3f4'} />
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Auto Backup ───────────────────────────────── */}
|
{/* ── Auto Backup ── */}
|
||||||
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>AUTOMATIC BACKUP</Text>
|
<Text style={[styles.sectionTitle, { color: theme.textSecondary }]}>AUTOMATIC BACKUP</Text>
|
||||||
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||||
<Text style={[styles.cardSubtext, { color: theme.textSecondary }]}>
|
<Text style={[styles.cardSubtext, { color: theme.textSecondary }]}>
|
||||||
@@ -274,45 +435,34 @@ const SettingsScreen = ({ navigation, route }) => {
|
|||||||
{ALL_SERVICES.map((svc) => {
|
{ALL_SERVICES.map((svc) => {
|
||||||
const isConnected = accounts.some((a) => a.service_type === svc.type && a.is_enabled);
|
const isConnected = accounts.some((a) => a.service_type === svc.type && a.is_enabled);
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity key={svc.type}
|
||||||
key={svc.type}
|
|
||||||
style={[styles.checkRow, { borderBottomColor: theme.borderLight }]}
|
style={[styles.checkRow, { borderBottomColor: theme.borderLight }]}
|
||||||
onPress={() => isConnected && toggleAutoBackup(svc.type)}
|
onPress={() => isConnected && toggleAutoBackup(svc.type)}
|
||||||
disabled={!isConnected}
|
disabled={!isConnected} activeOpacity={0.7}
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[styles.checkbox, { borderColor: isConnected ? theme.primary : theme.border },
|
||||||
styles.checkbox, { borderColor: isConnected ? theme.primary : theme.border },
|
autoBackup.includes(svc.type) && { backgroundColor: theme.primary, borderColor: theme.primary }]}>
|
||||||
autoBackup.includes(svc.type) && { backgroundColor: theme.primary, borderColor: theme.primary },
|
|
||||||
]}>
|
|
||||||
{autoBackup.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>}
|
{autoBackup.includes(svc.type) && <Text style={styles.checkmark}>✓</Text>}
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.checkIcon}>{svc.icon}</Text>
|
<Text style={styles.checkIcon}>{svc.icon}</Text>
|
||||||
<Text style={[styles.checkLabel, { color: isConnected ? theme.text : theme.textTertiary }]}>
|
<Text style={[styles.checkLabel, { color: isConnected ? theme.text : theme.textTertiary }]}>{svc.name}</Text>
|
||||||
{svc.name}
|
|
||||||
</Text>
|
|
||||||
{!isConnected && <Text style={[styles.notConnected, { color: theme.textTertiary }]}>Not connected</Text>}
|
{!isConnected && <Text style={[styles.notConnected, { color: theme.textTertiary }]}>Not connected</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<TouchableOpacity
|
<TouchableOpacity style={[styles.checkRow, { borderBottomWidth: 0 }]}
|
||||||
style={[styles.checkRow, { borderBottomWidth: 0 }]}
|
|
||||||
onPress={() => { setAutoBackup([]); DatabaseService.setAutoBackupServices(userId, []); }}
|
onPress={() => { setAutoBackup([]); DatabaseService.setAutoBackupServices(userId, []); }}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[styles.checkbox, { borderColor: theme.border },
|
||||||
styles.checkbox, { borderColor: theme.border },
|
autoBackup.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary }]}>
|
||||||
autoBackup.length === 0 && { backgroundColor: theme.primary, borderColor: theme.primary },
|
|
||||||
]}>
|
|
||||||
{autoBackup.length === 0 && <Text style={styles.checkmark}>✓</Text>}
|
{autoBackup.length === 0 && <Text style={styles.checkmark}>✓</Text>}
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.checkLabel, { color: theme.text, marginLeft: 0 }]}>
|
<Text style={[styles.checkLabel, { color: theme.text, marginLeft: 0 }]}>Don't automatically back up</Text>
|
||||||
Don't automatically back up
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ── Sign Out ──────────────────────────────────── */}
|
{/* ── Sign Out ── */}
|
||||||
<TouchableOpacity style={[styles.logoutButton, { borderColor: theme.danger }]} onPress={handleLogout}>
|
<TouchableOpacity style={[styles.logoutButton, { borderColor: theme.danger }]} onPress={handleLogout}>
|
||||||
<Text style={[styles.logoutText, { color: theme.danger }]}>Sign Out</Text>
|
<Text style={[styles.logoutText, { color: theme.danger }]}>Sign Out</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -331,11 +481,9 @@ const styles = StyleSheet.create({
|
|||||||
backArrow: { fontSize: 24 },
|
backArrow: { fontSize: 24 },
|
||||||
headerTitle: { fontSize: 18, fontWeight: '700' },
|
headerTitle: { fontSize: 18, fontWeight: '700' },
|
||||||
scrollContent: { flex: 1, paddingHorizontal: 16 },
|
scrollContent: { flex: 1, paddingHorizontal: 16 },
|
||||||
sectionTitle: { fontSize: 12, fontWeight: '700', marginTop: 24, marginBottom: 4, letterSpacing: 0.5 },
|
sectionTitle: { fontSize: 12, fontWeight: '700', marginTop: 24, marginBottom: 8, letterSpacing: 0.5 },
|
||||||
sectionHint: { fontSize: 12, marginBottom: 10 },
|
|
||||||
card: { borderRadius: 12, borderWidth: 1, overflow: 'hidden', marginBottom: 4 },
|
card: { borderRadius: 12, borderWidth: 1, overflow: 'hidden', marginBottom: 4 },
|
||||||
cardSubtext: { fontSize: 12, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 4 },
|
cardSubtext: { fontSize: 12, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 4 },
|
||||||
// Theme rows
|
|
||||||
themeRow: {
|
themeRow: {
|
||||||
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14,
|
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
@@ -345,40 +493,33 @@ const styles = StyleSheet.create({
|
|||||||
themeName: { fontSize: 14, fontWeight: '600', marginBottom: 2 },
|
themeName: { fontSize: 14, fontWeight: '600', marginBottom: 2 },
|
||||||
themeDesc: { fontSize: 12 },
|
themeDesc: { fontSize: 12 },
|
||||||
radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: '#dadce0' },
|
radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: '#dadce0' },
|
||||||
// Managed (already linked) service cards
|
serviceRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12 },
|
||||||
managedCard: {
|
serviceIcon: { fontSize: 22, marginRight: 8 },
|
||||||
borderRadius: 12, borderWidth: 1, marginBottom: 10, overflow: 'hidden',
|
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: {
|
actionBtn: { borderWidth: 1, borderRadius: 6, paddingHorizontal: 14, paddingVertical: 6 },
|
||||||
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12,
|
actionBtnText: { fontSize: 12, fontWeight: '600' },
|
||||||
gap: 8,
|
addBtn: { borderRadius: 6, paddingHorizontal: 14, paddingVertical: 6 },
|
||||||
},
|
addBtnText: { color: '#fff', fontSize: 12, fontWeight: '600' },
|
||||||
managedIcon: { fontSize: 22 },
|
inlineForm: { paddingHorizontal: 14, paddingBottom: 14, borderTopWidth: StyleSheet.hairlineWidth },
|
||||||
managedInfo: { flex: 1 },
|
fieldLabel: { fontSize: 12, fontWeight: '600', marginTop: 10, marginBottom: 4 },
|
||||||
managedName: { fontSize: 14, fontWeight: '600', marginBottom: 2 },
|
fieldInput: { borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14 },
|
||||||
managedStatus: { fontSize: 12 },
|
saveButton: { borderRadius: 8, paddingVertical: 12, alignItems: 'center', marginTop: 14 },
|
||||||
// Not-yet-linked header strip
|
saveButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
|
||||||
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
|
|
||||||
checkRow: {
|
checkRow: {
|
||||||
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12,
|
flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
},
|
||||||
checkbox: {
|
checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, justifyContent: 'center', alignItems: 'center', marginRight: 12 },
|
||||||
width: 22, height: 22, borderRadius: 4, borderWidth: 2,
|
|
||||||
justifyContent: 'center', alignItems: 'center', marginRight: 12,
|
|
||||||
},
|
|
||||||
checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' },
|
checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' },
|
||||||
checkIcon: { fontSize: 18, marginRight: 8 },
|
checkIcon: { fontSize: 18, marginRight: 8 },
|
||||||
checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 },
|
checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 },
|
||||||
notConnected: { fontSize: 11 },
|
notConnected: { fontSize: 11 },
|
||||||
// Logout
|
|
||||||
logoutButton: { borderWidth: 1.5, borderRadius: 8, paddingVertical: 14, alignItems: 'center', marginTop: 24 },
|
logoutButton: { borderWidth: 1.5, borderRadius: 8, paddingVertical: 14, alignItems: 'center', marginTop: 24 },
|
||||||
logoutText: { fontSize: 15, fontWeight: '600' },
|
logoutText: { fontSize: 15, fontWeight: '600' },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user