From 51583050860bf7c501e2c70cc65568b4d28bb239 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Wed, 4 Mar 2026 22:57:03 +0000 Subject: [PATCH] feat: implement NAS image lazy-loading and enhance service connection UI --- ServerFiles/server.js | 23 +- src/API/NASAPI.js | 13 +- src/components/MediaViewer.js | 50 +++- src/components/PhotoGrid.js | 6 +- src/components/ServiceConnectCard.js | 296 ++++++++++++++++++++ src/screens/HomeScreen.js | 61 +++- src/screens/OnboardingScreen.js | 214 ++------------ src/screens/SettingsScreen.js | 402 +++++++++------------------ 8 files changed, 573 insertions(+), 492 deletions(-) create mode 100644 src/components/ServiceConnectCard.js diff --git a/ServerFiles/server.js b/ServerFiles/server.js index bd91602..4267c9c 100644 --- a/ServerFiles/server.js +++ b/ServerFiles/server.js @@ -11,7 +11,12 @@ app.use(cors()); app.use(express.json({ limit: '100mb' })); app.use((req, res, next) => { - console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`); + const start = Date.now(); + res.on('finish', () => { + const ms = Date.now() - start; + const user = req.body?.username ? ` user=${req.body.username}` : ''; + console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} → ${res.statusCode} (${ms}ms)${user}`); + }); next(); }); @@ -52,6 +57,7 @@ app.post('/api/register', (req, res) => { } res.json({ success: true, userId, username: sanitized }); } catch (err) { + console.error('[/api/register] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -79,6 +85,7 @@ app.post('/api/index', (req, res) => { orphansRemoved: removed, }); } catch (err) { + console.error('[/api/index] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -107,6 +114,7 @@ app.post('/api/photos', (req, res) => { })), }); } catch (err) { + console.error('[/api/photos] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -149,6 +157,7 @@ app.post('/api/photo/fetch', (req, res) => { }, }); } catch (err) { + console.error('[/api/photo/fetch] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -187,6 +196,7 @@ app.post('/api/photo/upload', (req, res) => { res.json({ success: true, filename: safeFilename, indexed: result.indexed }); } catch (err) { + console.error('[/api/photo/upload] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -197,10 +207,10 @@ app.post('/api/photo/delete', (req, res) => { try { const { photoId } = req.body; if (!photoId) return res.status(400).json({ error: 'photoId is required' }); - const result = db.deletePhoto(photoId); res.json(result); } catch (err) { + console.error('[/api/photo/delete] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -234,6 +244,7 @@ app.post('/api/refresh', (req, res) => { })), }); } catch (err) { + console.error('[/api/refresh] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -248,6 +259,7 @@ app.post('/api/albums', (req, res) => { const albums = db.getAlbums(sanitized); res.json({ success: true, albums }); } catch (err) { + console.error('[/api/albums] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -260,6 +272,7 @@ app.post('/api/album/create', (req, res) => { const result = db.createAlbum(sanitized, name.trim()); res.json(result); } catch (err) { + console.error('[/api/album/create] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -281,6 +294,7 @@ app.post('/api/album/photos', (req, res) => { })), }); } catch (err) { + console.error('[/api/album/photos] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -292,6 +306,7 @@ app.post('/api/album/add-photo', (req, res) => { const result = db.addPhotoToAlbum(albumId, photoId); res.json(result); } catch (err) { + console.error('[/api/album/add-photo] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -303,6 +318,7 @@ app.post('/api/album/remove-photo', (req, res) => { const result = db.removePhotoFromAlbum(albumId, photoId); res.json(result); } catch (err) { + console.error('[/api/album/remove-photo] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -314,6 +330,7 @@ app.post('/api/album/delete', (req, res) => { const result = db.deleteAlbum(albumId); res.json(result); } catch (err) { + console.error('[/api/album/delete] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -325,6 +342,7 @@ app.post('/api/album/rename', (req, res) => { const result = db.renameAlbum(albumId, name.trim()); res.json(result); } catch (err) { + console.error('[/api/album/rename] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); @@ -336,6 +354,7 @@ app.get('/api/users', (req, res) => { const users = db.getAllUsers(); res.json({ success: true, users }); } catch (err) { + console.error('[/api/users] Error:', err.message, err.stack); res.status(500).json({ error: err.message }); } }); diff --git a/src/API/NASAPI.js b/src/API/NASAPI.js index ebdf5d8..cd9d2b6 100644 --- a/src/API/NASAPI.js +++ b/src/API/NASAPI.js @@ -17,16 +17,12 @@ class NASAPI { async connect(host, port, username) { if (!host || !username) throw new Error('Host and username are required'); - this.configure(host, port, username); - - // Register user on NAS — NAS creates user folder and DB entry const response = await axios.post( `${this.baseUrl}/api/register`, { username: this.username }, { timeout: 10000 }, ); - if (response.data?.success) { this.isConnected = true; return { @@ -49,7 +45,6 @@ class NASAPI { } } - // Full refresh cycle: POST to re-index + fetch updated photo list async refreshPhotos() { if (!this.username) throw new Error('Not connected'); const response = await axios.post( @@ -60,7 +55,6 @@ class NASAPI { return response.data; } - // Fetch photo index only (no re-index) async getPhotos() { if (!this.username) throw new Error('Not connected'); const response = await axios.post( @@ -71,7 +65,6 @@ class NASAPI { return response.data; } - // Fetch a single photo as base64 with hash verification async fetchPhoto(photoId) { const response = await axios.post( `${this.baseUrl}/api/photo/fetch`, @@ -84,7 +77,6 @@ class NASAPI { return response.data.photo; } - // Upload a photo from device to NAS as base64 async uploadPhoto(filename, base64Data) { if (!this.username) throw new Error('Not connected'); const response = await axios.post( @@ -95,7 +87,6 @@ class NASAPI { return response.data; } - // Delete a photo from NAS async deletePhoto(photoId) { const response = await axios.post( `${this.baseUrl}/api/photo/delete`, @@ -105,7 +96,6 @@ class NASAPI { return response.data; } - // Re-index only async reIndex() { if (!this.username) throw new Error('Not connected'); const response = await axios.post( @@ -116,8 +106,6 @@ class NASAPI { return response.data; } - // ─── Album operations ─── - async getAlbums() { if (!this.username) throw new Error('Not connected'); const response = await axios.post( @@ -197,3 +185,4 @@ class NASAPI { } export default NASAPI; + diff --git a/src/components/MediaViewer.js b/src/components/MediaViewer.js index b081873..cbdf5c2 100644 --- a/src/components/MediaViewer.js +++ b/src/components/MediaViewer.js @@ -1,7 +1,7 @@ import React, { useRef, useState, useEffect } from 'react'; import { Modal, View, Text, TouchableOpacity, Image, StyleSheet, - Dimensions, FlatList, + Dimensions, FlatList, ActivityIndicator, } from 'react-native'; import { useTheme } from '../services/ThemeContext'; import { getServiceIcon, getServiceName } from '../UI/ServiceHelpers'; @@ -32,7 +32,7 @@ const MediaViewer = ({ visible, media, allMedia, onClose, onDelete, onAddToAlbum const mediaList = allMedia && allMedia.length > 0 ? allMedia : (media ? [media] : []); const hasCarousel = mediaList.length > 1; - // When the modal opens or media prop changes, scroll to the correct item + // When the modal opens, media prop changes, or allMedia updates — sync currentMedia useEffect(() => { if (!visible || !media || mediaList.length === 0) return; @@ -41,9 +41,9 @@ const MediaViewer = ({ visible, media, allMedia, onClose, onDelete, onAddToAlbum ); const newIndex = idx >= 0 ? idx : 0; setActiveIndex(newIndex); - setCurrentMedia(mediaList[newIndex]); + // Always use the freshest version from mediaList (which gets updated when base64 loads) + setCurrentMedia(mediaList[newIndex] || media); - // Delay scroll so FlatList has rendered setTimeout(() => { if (mainListRef.current && newIndex >= 0) { mainListRef.current.scrollToOffset({ offset: newIndex * SCREEN_WIDTH, animated: false }); @@ -54,7 +54,7 @@ const MediaViewer = ({ visible, media, allMedia, onClose, onDelete, onAddToAlbum } catch (e) { /* index might be out of range during initial render */ } } }, 100); - }, [visible, media]); + }, [visible, media, allMedia]); const activeIndexRef = useRef(0); @@ -83,10 +83,23 @@ const MediaViewer = ({ visible, media, allMedia, onClose, onDelete, onAddToAlbum if (!media) return null; + // True while a NAS photo has no local_path yet (still loading base64) + const isNasLoading = (item) => + item?.source_service === 'NAS' && item?.nas_photo_id && !item?.local_path; + const renderMainItem = ({ item }) => ( {item.local_path ? ( - + + ) : isNasLoading(item) ? ( + + + Loading from NAS… + ) : ( 📷 @@ -106,6 +119,10 @@ const MediaViewer = ({ visible, media, allMedia, onClose, onDelete, onAddToAlbum > {item.local_path ? ( + ) : isNasLoading(item) ? ( + + + ) : ( 📷 @@ -151,15 +168,17 @@ const MediaViewer = ({ visible, media, allMedia, onClose, onDelete, onAddToAlbum })} style={styles.mainList} /> + ) : displayMedia.local_path ? ( + + ) : isNasLoading(displayMedia) ? ( + + + Loading from NAS… + ) : ( - // Single image mode (backward compat) - displayMedia.local_path ? ( - - ) : ( - - 📷 - - ) + + 📷 + )} {/* Thumbnail carousel */} @@ -272,7 +291,8 @@ const styles = StyleSheet.create({ counterText: { color: '#fff', fontSize: 14, fontWeight: '600' }, mainList: { flex: 1 }, fullImage: { flex: 1, width: '100%' }, - fullPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + fullPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 }, + loadingText: { color: '#ccc', fontSize: 14, marginTop: 8 }, // Thumbnail carousel thumbCarousel: { position: 'absolute', bottom: 110, left: 0, right: 0, zIndex: 15, diff --git a/src/components/PhotoGrid.js b/src/components/PhotoGrid.js index 4f535ee..92b1f12 100644 --- a/src/components/PhotoGrid.js +++ b/src/components/PhotoGrid.js @@ -1,6 +1,6 @@ import React from 'react'; import { - View, Text, FlatList, TouchableOpacity, Image, Dimensions, StyleSheet, + View, Text, FlatList, TouchableOpacity, Image, Dimensions, StyleSheet, ActivityIndicator, } from 'react-native'; import { useTheme } from '../services/ThemeContext'; import { getServiceIcon } from '../UI/ServiceHelpers'; @@ -61,6 +61,10 @@ const PhotoGrid = ({ resizeMode="cover" onError={(e) => console.warn(`Image load failed [${item.source_service}]: ${item.local_path?.substring(0, 80)}`, e.nativeEvent?.error)} /> + ) : item.source_service === 'NAS' && item.nas_photo_id ? ( + + + ) : ( 📷 diff --git a/src/components/ServiceConnectCard.js b/src/components/ServiceConnectCard.js new file mode 100644 index 0000000..233b6aa --- /dev/null +++ b/src/components/ServiceConnectCard.js @@ -0,0 +1,296 @@ +import React, { useState } from 'react'; +import { + View, Text, TouchableOpacity, TextInput, StyleSheet, ActivityIndicator, +} from 'react-native'; +import { useTheme } from '../services/ThemeContext'; +import ServiceHealthIndicator from './ServiceHealthIndicator'; +import NASSetupInstructions from './NASSetupInstructions'; +import DatabaseService from '../services/DatabaseService'; +import GoogleAUTH from '../AUTH/GoogleAUTH'; +import NASAUTH from '../AUTH/NASAUTH'; + +/** + * ServiceConnectCard + * + * A self-contained card for connecting / managing a single service. + * Used identically in OnboardingScreen (step 1) and SettingsScreen. + * + * Props: + * serviceType – 'GOOGLE_PHOTOS' | 'ICLOUD_PHOTOS' | 'NAS' + * userId – current user id + * existingData – account_data from DB if already linked (or null) + * isLinked – whether the service is already linked + * onLinked – callback() fired after a successful new connection / re-auth + * onRemoved – callback() fired after the account is deleted (Settings only) + * showRemove – whether to show a Remove button (Settings only) + */ +const ServiceConnectCard = ({ + serviceType, + userId, + existingData = null, + isLinked = false, + onLinked, + onRemoved, + showRemove = false, +}) => { + const { theme } = useTheme(); + const [loading, setLoading] = useState(false); + const [health, setHealth] = useState(isLinked ? 'healthy' : 'inactive'); + const [connected, setConnected] = useState(isLinked); + const [showForm, setShowForm] = useState(!isLinked); // show form if not yet linked + + // NAS-specific fields — pre-fill from existingData if editing + const [nasHost, setNasHost] = useState(existingData?.host || ''); + const [nasPort, setNasPort] = useState(String(existingData?.port || 9500)); + const [nasUsername, setNasUsername] = useState(existingData?.username || ''); + + // ─── Service metadata ──────────────────────────────────── + const META = { + GOOGLE_PHOTOS: { icon: '📷', name: 'Google Photos', desc: 'Access your Google Photos library', color: '#4285F4' }, + ICLOUD_PHOTOS: { icon: '🍎', name: 'iCloud Photos', desc: 'Access your iCloud photo library', color: '#555' }, + NAS: { icon: '💾', name: 'NAS Storage', desc: 'Connect to your NAS photo server', color: '#0a7ea4' }, + }; + const meta = META[serviceType] || { icon: '🔌', name: serviceType, desc: '', color: '#888' }; + + // ─── Connect handlers ──────────────────────────────────── + const connectGoogle = async () => { + setLoading(true); + setHealth('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', + }); + setConnected(true); + setHealth('healthy'); + setShowForm(false); + onLinked?.(); + } catch (err) { + setHealth('error'); + console.error('Google auth error:', err.message); + } finally { + setLoading(false); + } + }; + + const connectIcloud = async () => { + setLoading(true); + setHealth('checking'); + try { + await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', { + status: 'placeholder', + note: 'iCloud integration pending Apple API access', + }); + setConnected(true); + setHealth('healthy'); + setShowForm(false); + onLinked?.(); + } catch (err) { + setHealth('error'); + console.error('iCloud auth error:', err.message); + } finally { + setLoading(false); + } + }; + + const connectNas = async () => { + if (!nasHost.trim() || !nasUsername.trim()) { + // surface validation inline rather than an Alert so it fits the card style + setHealth('error'); + return; + } + setLoading(true); + setHealth('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(), + }); + setConnected(true); + setHealth('healthy'); + setShowForm(false); + onLinked?.(); + } catch (err) { + setHealth('error'); + console.error('NAS auth error:', err.message); + } finally { + setLoading(false); + } + }; + + const handleConnect = () => { + if (serviceType === 'GOOGLE_PHOTOS') return connectGoogle(); + if (serviceType === 'ICLOUD_PHOTOS') return connectIcloud(); + if (serviceType === 'NAS') return connectNas(); + }; + + // ─── Render ────────────────────────────────────────────── + return ( + + {/* Header row */} + + + {meta.icon} + + {meta.name} + {meta.desc} + + {connected && ( + + ✓ Connected + + )} + + + {/* NAS setup instructions (collapsible, shown when form is visible) */} + {serviceType === 'NAS' && showForm && } + + {/* Connection form */} + {showForm && ( + + {serviceType === 'NAS' && ( + <> + Server Address + + Port + + Username + + + )} + + {serviceType === 'ICLOUD_PHOTOS' && ( + + Full iCloud integration is coming soon. Tap below to register as a placeholder. + + )} + + + {loading + ? + : + {serviceType === 'NAS' ? 'Test & Connect' : `Sign in with ${meta.name}`} + + } + + + {health === 'error' && ( + + {serviceType === 'NAS' && (!nasHost.trim() || !nasUsername.trim()) + ? 'Please fill in all required fields.' + : 'Connection failed. Check your details and try again.'} + + )} + + )} + + {/* Action buttons when already connected */} + {connected && !showForm && ( + + setShowForm(true)} + disabled={loading} + > + + {serviceType === 'NAS' ? 'Edit' : 'Re-authenticate'} + + + + {showRemove && ( + + Remove + + )} + + )} + + ); +}; + +const styles = StyleSheet.create({ + card: { + borderRadius: 12, borderWidth: 1, marginBottom: 10, overflow: 'hidden', + }, + header: { + flexDirection: 'row', alignItems: 'center', padding: 14, gap: 8, + }, + icon: { fontSize: 22 }, + headerInfo: { flex: 1 }, + name: { fontSize: 14, fontWeight: '600', marginBottom: 2 }, + desc: { fontSize: 12 }, + badge: { + borderRadius: 6, paddingHorizontal: 8, paddingVertical: 3, + }, + badgeText: { fontSize: 11, fontWeight: '600' }, + form: { + paddingHorizontal: 14, paddingBottom: 14, + }, + label: { + fontSize: 12, fontWeight: '600', marginBottom: 5, marginTop: 10, + }, + input: { + borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, + fontSize: 14, + }, + infoText: { + fontSize: 13, lineHeight: 18, marginTop: 8, marginBottom: 4, + }, + connectBtn: { + borderRadius: 8, paddingVertical: 12, alignItems: 'center', marginTop: 14, + }, + connectBtnText: { color: '#fff', fontSize: 14, fontWeight: '600' }, + btnDisabled: { opacity: 0.6 }, + errorText: { + color: '#e53935', fontSize: 12, marginTop: 6, textAlign: 'center', + }, + actions: { + flexDirection: 'row', paddingHorizontal: 14, paddingBottom: 12, gap: 8, + }, + actionBtn: { + borderWidth: 1, borderRadius: 6, paddingHorizontal: 14, paddingVertical: 6, + }, + actionBtnText: { fontSize: 13, fontWeight: '600' }, +}); + +export default ServiceConnectCard; + diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index 8d80951..d88c555 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { StyleSheet, View, Text, ActivityIndicator, RefreshControl, Alert, } from 'react-native'; @@ -90,6 +90,9 @@ const HomeScreen = ({ navigation, route }) => { const [actionSheetVisible, setActionSheetVisible] = useState(false); const [actionSheetTarget, setActionSheetTarget] = useState(null); + // Cache of nas_photo_id → data: URI so we only fetch each image once + const nasImageCache = useRef({}); + useFocusEffect( useCallback(() => { if (userId) loadUserData(); @@ -314,6 +317,58 @@ const HomeScreen = ({ navigation, route }) => { } }; + // ─── NAS image lazy-loader ─── + const fetchNasImage = async (item) => { + if (!item.nas_photo_id) return item; + + // Return cached version immediately + if (nasImageCache.current[item.nas_photo_id]) { + return { ...item, local_path: nasImageCache.current[item.nas_photo_id] }; + } + + const nasAccount = linkedAccounts.find((a) => a.service_type === 'NAS'); + if (!nasAccount) return item; + + try { + const nasData = nasAccount.account_data; + const nasAuth = new NASAUTH(); + await nasAuth.authenticate(nasData.host, nasData.port, nasData.username); + const photo = await nasAuth.fetchPhoto(item.nas_photo_id); + await nasAuth.revokeAccess(); + + if (photo?.base64) { + const mimeType = photo.mimeType || item.mimeType || 'image/jpeg'; + const dataUri = `data:${mimeType};base64,${photo.base64}`; + nasImageCache.current[item.nas_photo_id] = dataUri; + + // Update the item in allMedia state so the grid thumbnail also updates + setAllMedia((prev) => + prev.map((m) => + m.nas_photo_id === item.nas_photo_id ? { ...m, local_path: dataUri } : m + ) + ); + + return { ...item, local_path: dataUri }; + } + } catch (err) { + console.error('NAS image fetch error:', err.message); + } + return item; + }; + + const handleMediaPress = async (item) => { + if (item.nas_photo_id && !item.local_path) { + // Show viewer immediately with placeholder, then update when loaded + setSelectedMedia(item); + setViewerVisible(true); + const loaded = await fetchNasImage(item); + setSelectedMedia(loaded); + } else { + setSelectedMedia(item); + setViewerVisible(true); + } + }; + // ─── Photo actions ─── const handleDeletePhoto = (item) => { @@ -378,7 +433,7 @@ const HomeScreen = ({ navigation, route }) => { const getActionSheetActions = () => { if (!actionSheetTarget) return []; const actions = [ - { label: 'View Details', icon: 'ℹ️', onPress: () => { setSelectedMedia(actionSheetTarget); setViewerVisible(true); } }, + { label: 'View Details', icon: 'ℹ️', onPress: () => { handleMediaPress(actionSheetTarget); } }, ]; if (actionSheetTarget.nas_photo_id || actionSheetTarget.source_service === 'NAS') { actions.push({ label: 'Delete from NAS', icon: '🗑️', destructive: true, onPress: () => handleDeletePhoto(actionSheetTarget) }); @@ -455,7 +510,7 @@ const HomeScreen = ({ navigation, route }) => { ) : ( { setSelectedMedia(item); setViewerVisible(true); }} + onItemPress={(item) => handleMediaPress(item)} onItemLongPress={handleLongPress} onEndReached={loadMorePhotos} refreshControl={ diff --git a/src/screens/OnboardingScreen.js b/src/screens/OnboardingScreen.js index 747fd4e..c94505e 100644 --- a/src/screens/OnboardingScreen.js +++ b/src/screens/OnboardingScreen.js @@ -1,14 +1,11 @@ import React, { useState } from 'react'; import { StyleSheet, View, Text, ScrollView, TouchableOpacity, Alert, - ActivityIndicator, Switch, TextInput, + ActivityIndicator, Switch, } from 'react-native'; import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; -import GoogleAUTH from '../AUTH/GoogleAUTH'; -import NASAUTH from '../AUTH/NASAUTH'; -import NASSetupInstructions from '../components/NASSetupInstructions'; -import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; +import ServiceConnectCard from '../components/ServiceConnectCard'; const OnboardingScreen = ({ navigation, route }) => { const { userId } = route.params; @@ -16,27 +13,17 @@ const OnboardingScreen = ({ navigation, route }) => { const [currentStep, setCurrentStep] = useState(0); const [loading, setLoading] = useState(false); - // Service toggles + // Step 0 — service toggles const [googleEnabled, setGoogleEnabled] = useState(false); const [iosEnabled, setIosEnabled] = useState(false); const [nasEnabled, setNasEnabled] = useState(false); - // Service connection status + // Step 1 — track which services completed connection via ServiceConnectCard callbacks const [googleConnected, setGoogleConnected] = useState(false); const [iosConnected, setIosConnected] = useState(false); const [nasConnected, setNasConnected] = useState(false); - // Service health: 'inactive' | 'healthy' | 'error' | 'checking' - const [googleHealth, setGoogleHealth] = useState('inactive'); - const [iosHealth, setIosHealth] = useState('inactive'); - const [nasHealth, setNasHealth] = useState('inactive'); - - // NAS config - const [nasHost, setNasHost] = useState(''); - const [nasPort, setNasPort] = useState('9500'); - const [nasUsername, setNasUsername] = useState(''); - - // Auto-backup checklist (multi-select) + // Step 2 — auto-backup checklist const [autoBackupServices, setAutoBackupServices] = useState([]); const steps = [ @@ -53,82 +40,6 @@ const OnboardingScreen = ({ navigation, route }) => { return services; }; - const handleGoogleAuth = async () => { - try { - setLoading(true); - setGoogleHealth('checking'); - 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) { - console.error('Google auth error:', error); - setGoogleHealth('error'); - Alert.alert('Error', 'Failed to connect Google Photos: ' + error.message); - } finally { - setLoading(false); - } - }; - - const handleIosAuth = async () => { - try { - setLoading(true); - setIosHealth('checking'); - 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) { - console.error('iCloud auth error:', 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; - } - try { - setLoading(true); - setNasHealth('checking'); - 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) { - console.error('NAS auth error:', 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] @@ -137,8 +48,10 @@ const OnboardingScreen = ({ navigation, route }) => { const handleNext = async () => { if (currentStep === 0) { - const any = googleEnabled || iosEnabled || nasEnabled; - if (!any) { Alert.alert('Error', 'Please select at least one service'); return; } + if (!googleEnabled && !iosEnabled && !nasEnabled) { + Alert.alert('Error', 'Please select at least one service'); + return; + } setCurrentStep(1); } else if (currentStep === 1) { const anyConnected = @@ -188,84 +101,35 @@ const OnboardingScreen = ({ navigation, route }) => { setNasEnabled(!nasEnabled)} /> - - {/* NAS setup instructions dropdown */} - {nasEnabled && } )} - {/* Step 1: Configure each service */} + {/* Step 1: Configure each selected service via shared ServiceConnectCard */} {currentStep === 1 && ( {googleEnabled && ( - - - - 📷 - Google Photos - {googleConnected && ✓ Connected} - - {!googleConnected && ( - - {loading ? : Sign in with Google} - - )} - + setGoogleConnected(true)} + /> )} - {iosEnabled && ( - - - - 🍎 - iCloud Photos - {iosConnected && ✓ Connected} - - {!iosConnected && ( - - {loading ? : Connect iCloud} - - )} - + setIosConnected(true)} + /> )} - {nasEnabled && ( - - - - 💾 - NAS Storage - {nasConnected && ✓ Connected} - - {!nasConnected && ( - <> - Server Address - - - Port - - - Username - - - - {loading ? : Test & Connect} - - - )} - + setNasConnected(true)} + /> )} )} @@ -277,7 +141,6 @@ const OnboardingScreen = ({ navigation, route }) => { Select which services new photos should automatically upload to. You can select multiple or none. - {getConnectedServices().map((svc) => ( { activeOpacity={0.7} > {autoBackupServices.includes(svc.type) && ✓} @@ -296,15 +158,13 @@ const OnboardingScreen = ({ navigation, route }) => { {svc.name} ))} - setAutoBackupServices([])} activeOpacity={0.7} > {autoBackupServices.length === 0 && ✓} @@ -372,7 +232,6 @@ const styles = StyleSheet.create({ title: { fontSize: 26, fontWeight: '700', marginBottom: 6 }, description: { fontSize: 14 }, cardsContainer: { gap: 12 }, - // Toggle cards (Step 0) toggleCard: { borderRadius: 12, padding: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderWidth: 1.5, @@ -382,19 +241,7 @@ const styles = StyleSheet.create({ toggleInfo: { flex: 1 }, toggleName: { fontSize: 15, fontWeight: '600', marginBottom: 2 }, toggleDescription: { fontSize: 12 }, - // Config cards (Step 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: 8 }, - 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, - }, - // Auto-backup (Step 2) checklistInfo: { fontSize: 13, marginBottom: 8 }, checkRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14, @@ -407,7 +254,6 @@ const styles = StyleSheet.create({ checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' }, checkIcon: { fontSize: 18, marginRight: 8 }, checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 }, - // Footer footer: { flexDirection: 'row', paddingHorizontal: 20, paddingVertical: 14, borderTopWidth: 1, gap: 12, diff --git a/src/screens/SettingsScreen.js b/src/screens/SettingsScreen.js index 7c7ca86..e4139fe 100644 --- a/src/screens/SettingsScreen.js +++ b/src/screens/SettingsScreen.js @@ -1,33 +1,32 @@ import React, { useState, useCallback } from 'react'; import { - StyleSheet, View, Text, ScrollView, TouchableOpacity, Switch, TextInput, - Alert, ActivityIndicator, + StyleSheet, View, Text, ScrollView, TouchableOpacity, Switch, Alert, ActivityIndicator, } from 'react-native'; import { useFocusEffect } from '@react-navigation/native'; import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; -import NASSetupInstructions from '../components/NASSetupInstructions'; +import ServiceConnectCard from '../components/ServiceConnectCard'; 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 +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' }, +]; + 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); - - // Per-service health: { GOOGLE_PHOTOS: 'healthy'|'error'|'inactive', ... } const [serviceHealth, setServiceHealth] = useState({}); - - // NAS edit fields - const [editingNas, setEditingNas] = useState(false); - const [nasHost, setNasHost] = useState(''); - const [nasPort, setNasPort] = useState('9500'); - const [nasUsername, setNasUsername] = useState(''); + // Track a reload token so ServiceConnectCards re-read existingData after linking + const [reloadKey, setReloadKey] = useState(0); useFocusEffect( useCallback(() => { @@ -41,41 +40,28 @@ 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 || ''); - } - - // Check health for each enabled account checkAllServiceHealth(allAccounts); } catch (err) { console.error('Error loading settings:', err); } }; - // ─── Health checks ───────────────────────────────────────── + // ─── Health checks ─────────────────────────────────────── const checkAllServiceHealth = async (accs) => { - const health = {}; for (const acc of accs) { if (!acc.is_enabled) { - health[acc.service_type] = 'inactive'; + setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'inactive' })); continue; } - health[acc.service_type] = 'checking'; setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'checking' })); - health[acc.service_type] = await checkSingleHealth(acc); + const status = await checkSingleHealth(acc); + setServiceHealth((prev) => ({ ...prev, [acc.service_type]: status })); } - setServiceHealth(health); }; const checkSingleHealth = async (acc) => { try { if (acc.service_type === 'GOOGLE_PHOTOS') { - // The Photos Library REST API is deprecated; health-check the token - // by calling the userinfo endpoint instead. const gAuth = new GoogleAUTH(); gAuth.accessToken = acc.account_data?.accessToken; gAuth.refreshToken = acc.account_data?.refreshToken; @@ -91,23 +77,14 @@ const SettingsScreen = ({ navigation, route }) => { await nasAuth.revokeAccess(); return healthy ? 'healthy' : 'error'; } - if (acc.service_type === 'ICLOUD_PHOTOS') { - // Placeholder – always "healthy" since it's a stub - return 'healthy'; - } + if (acc.service_type === 'ICLOUD_PHOTOS') return 'healthy'; return 'inactive'; } catch { return 'error'; } }; - const recheckHealth = async (acc) => { - setServiceHealth((prev) => ({ ...prev, [acc.service_type]: 'checking' })); - const status = await checkSingleHealth(acc); - setServiceHealth((prev) => ({ ...prev, [acc.service_type]: status })); - }; - - // ─── Service actions ─────────────────────────────────────── + // ─── Service actions ───────────────────────────────────── const toggleService = async (accountId, currentEnabled) => { try { await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled); @@ -119,23 +96,17 @@ const SettingsScreen = ({ navigation, route }) => { const deleteAccount = (acc) => { Alert.alert( - 'Delete Service', - `Remove ${getServiceName(acc.service_type)} from your account? This cannot be undone.`, + 'Remove Service', + `Remove ${getServiceName(acc.service_type)} from your account?`, [ { text: 'Cancel', style: 'cancel' }, { - text: 'Delete', - style: 'destructive', + text: 'Remove', style: 'destructive', onPress: async () => { try { await DatabaseService.removeLinkedAccount(acc.id); - setServiceHealth((prev) => { - const next = { ...prev }; - delete next[acc.service_type]; - return next; - }); + setServiceHealth((prev) => { const n = { ...prev }; delete n[acc.service_type]; return n; }); await loadData(); - Alert.alert('Deleted', `${getServiceName(acc.service_type)} has been removed.`); } catch { Alert.alert('Error', 'Failed to remove service'); } @@ -145,75 +116,12 @@ const SettingsScreen = ({ navigation, route }) => { ); }; - const reauthenticateService = async (acc) => { - setLoading(true); - try { - if (acc.service_type === 'GOOGLE_PHOTOS') { - const gAuth = new GoogleAUTH(); - const result = await gAuth.authenticate(); - await DatabaseService.linkAccount(userId, 'GOOGLE_PHOTOS', { - accessToken: result.accessToken, - refreshToken: result.refreshToken || null, - expiresAt: gAuth.expiresAt || Date.now() + 3600000, - email: result.email || 'google-user', - }); - Alert.alert('Success', 'Google Photos re-authenticated!'); - } else if (acc.service_type === 'ICLOUD_PHOTOS') { - await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', { - status: 'placeholder', - note: 'iCloud integration pending Apple API access', - }); - Alert.alert('Info', 'iCloud Photos re-registered (placeholder).'); - } else if (acc.service_type === 'NAS') { - setEditingNas(true); - setLoading(false); - return; // user edits inline - } - await loadData(); - } catch (error) { - Alert.alert('Error', 'Re-authentication failed: ' + error.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('Error saving auto backup:', err); - } - }; - - const saveNasConfig = async () => { - if (!nasHost.trim() || !nasUsername.trim()) { - Alert.alert('Error', 'Please fill in the NAS host and username'); - return; - } - setLoading(true); - 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(), - }); - setEditingNas(false); - await loadData(); - Alert.alert('Success', 'NAS configuration updated'); - } catch (err) { - Alert.alert('Error', 'Failed to update NAS: ' + err.message); - } finally { - setLoading(false); - } + try { await DatabaseService.setAutoBackupServices(userId, updated); } catch (err) { console.error(err); } }; const handleLogout = () => { @@ -224,8 +132,8 @@ const SettingsScreen = ({ navigation, route }) => { }; const themeOptions = [ - { key: 'light', label: 'Light', icon: '☀️', desc: 'Standard light mode' }, - { key: 'dark', label: 'Dark', icon: '🌙', desc: 'Dark mode for low light' }, + { key: 'light', label: 'Light', icon: '☀️', desc: 'Standard light mode' }, + { key: 'dark', label: 'Dark', icon: '🌙', desc: 'Dark mode for low light' }, { key: 'liquidGlass', label: 'Liquid Glass', icon: '💎', desc: 'iOS-style frosted glass' }, ]; @@ -242,17 +150,14 @@ const SettingsScreen = ({ navigation, route }) => { - {/* ── Theme ──────────────────────────────────────── */} + + {/* ── Appearance ───────────────────────────────── */} APPEARANCE {themeOptions.map((opt) => ( setThemeName(opt.key)} > {opt.icon} @@ -265,105 +170,87 @@ const SettingsScreen = ({ navigation, route }) => { ))} - {/* ── Services ──────────────────────────────────── */} + {/* ── Services ─────────────────────────────────── */} + {/* Always shows ALL services — connected ones get a manage strip, unconnected show ServiceConnectCard */} SERVICES - - {accounts.length === 0 ? ( - - - No services connected. Go to onboarding to add services. - - - ) : ( - accounts.map((acc) => { - const health = serviceHealth[acc.service_type] || (acc.is_enabled ? 'checking' : 'inactive'); - return ( - - {/* Main row */} - - recheckHealth(acc)} activeOpacity={0.6}> - - - {getServiceIcon(acc.service_type)} - - - {getServiceName(acc.service_type)} - - - {acc.is_enabled ? 'Enabled' : 'Disabled'} - - - toggleService(acc.id, acc.is_enabled)} - trackColor={{ false: theme.border, true: '#a8dab5' }} - thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'} - /> - - {/* Action buttons row */} - - {acc.service_type === 'NAS' && acc.is_enabled && ( - setEditingNas(!editingNas)} - > - Edit - - )} - reauthenticateService(acc)} - disabled={loading} - > - - {loading ? '...' : 'Re-auth'} - - - deleteAccount(acc)} - > - Delete - + + Connect new services or manage existing ones at any time. + + + {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 + return ( + + {/* Top row: health, icon, name, enable toggle */} + + checkSingleHealth(acc).then((s) => setServiceHealth((p) => ({ ...p, [svc.type]: s })))}> + + + {svc.icon} + + {svc.name} + + {acc.is_enabled ? 'Enabled' : 'Disabled'} + + toggleService(acc.id, acc.is_enabled)} + trackColor={{ false: theme.border, true: '#a8dab5' }} + thumbColor={acc.is_enabled ? theme.accent : '#f4f3f4'} + /> - ); - }) - )} - - {/* NAS Edit Config */} - {editingNas && ( - - Edit NAS Configuration - Server Address - - Port - - Username - - - {loading ? : Save Configuration} - - - )} + {/* Re-auth / edit via shared ServiceConnectCard (starts collapsed since already linked) */} + { setReloadKey((k) => k + 1); loadData(); }} + onRemoved={() => deleteAccount(acc)} + /> + + ); + } - {/* NAS Setup Instructions */} - + // Not yet linked — show ServiceConnectCard directly so user can add it + return ( + + + {svc.icon} + + {svc.name} + Not connected + + + { setReloadKey((k) => k + 1); loadData(); }} + /> + + ); + })} - {/* NAS Google Photos Offloading */} + {/* ── NAS Offloading ────────────────────────────── */} {hasNasConnected && ( <> NAS OFFLOADING - 📷 - - Offload Google Photos to NAS - + 📷 + + Offload Google Photos to NAS + Fetch Google Photos data through NAS server @@ -384,11 +271,7 @@ const SettingsScreen = ({ navigation, route }) => { New photos from your device will be automatically uploaded to the selected services. - {[ - { type: 'GOOGLE_PHOTOS', name: 'Google Photos', icon: '📷' }, - { type: 'ICLOUD_PHOTOS', name: 'iCloud Photos', icon: '🍎' }, - { type: 'NAS', name: 'NAS Storage', icon: '💾' }, - ].map((svc) => { + {ALL_SERVICES.map((svc) => { const isConnected = accounts.some((a) => a.service_type === svc.type && a.is_enabled); return ( { activeOpacity={0.7} > {autoBackup.includes(svc.type) && ✓} {svc.icon} - + {svc.name} - {!isConnected && ( - Not connected - )} + {!isConnected && Not connected} ); })} { - setAutoBackup([]); - DatabaseService.setAutoBackupServices(userId, []); - }} + onPress={() => { setAutoBackup([]); DatabaseService.setAutoBackupServices(userId, []); }} activeOpacity={0.7} > {autoBackup.length === 0 && ✓} @@ -440,13 +313,9 @@ const SettingsScreen = ({ navigation, route }) => { {/* ── Sign Out ──────────────────────────────────── */} - + Sign Out - @@ -457,17 +326,14 @@ const styles = StyleSheet.create({ container: { flex: 1 }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: 16, paddingTop: 52, paddingBottom: 12, - borderBottomWidth: 1, + paddingHorizontal: 16, paddingTop: 52, paddingBottom: 12, borderBottomWidth: 1, }, backArrow: { fontSize: 24 }, headerTitle: { fontSize: 18, fontWeight: '700' }, scrollContent: { flex: 1, paddingHorizontal: 16 }, - sectionTitle: { fontSize: 12, fontWeight: '700', marginTop: 24, marginBottom: 8, letterSpacing: 0.5 }, - card: { - borderRadius: 12, borderWidth: 1, overflow: 'hidden', marginBottom: 4, - }, - cardTitle: { fontSize: 15, fontWeight: '600', padding: 14, paddingBottom: 4 }, + sectionTitle: { fontSize: 12, fontWeight: '700', marginTop: 24, marginBottom: 4, letterSpacing: 0.5 }, + sectionHint: { fontSize: 12, marginBottom: 10 }, + card: { borderRadius: 12, borderWidth: 1, overflow: 'hidden', marginBottom: 4 }, cardSubtext: { fontSize: 12, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 4 }, // Theme rows themeRow: { @@ -478,39 +344,28 @@ const styles = StyleSheet.create({ themeInfo: { flex: 1 }, themeName: { fontSize: 14, fontWeight: '600', marginBottom: 2 }, 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 + managedCard: { + borderRadius: 12, borderWidth: 1, marginBottom: 10, overflow: 'hidden', }, - // Service rows + 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, - borderBottomWidth: StyleSheet.hairlineWidth, }, - 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, - }, - actionBtn: { - borderWidth: 1, borderRadius: 6, paddingHorizontal: 12, paddingVertical: 5, - }, - actionBtnText: { fontSize: 12, fontWeight: '600' }, - emptyRow: { padding: 16 }, - emptyText: { fontSize: 13, textAlign: 'center' }, - // NAS fields - fieldLabel: { fontSize: 12, fontWeight: '600', marginTop: 10, marginBottom: 4, paddingHorizontal: 14 }, - fieldInput: { - borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, - fontSize: 14, marginHorizontal: 14, - }, - saveButton: { - borderRadius: 8, paddingVertical: 12, alignItems: 'center', margin: 14, - }, - saveButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' }, - // Auto backup checkbox rows + // Auto backup checkRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12, borderBottomWidth: StyleSheet.hairlineWidth, @@ -524,10 +379,7 @@ const styles = StyleSheet.create({ checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 }, 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' }, });