From 6f4ec66e2e704104830215cb690c2cd457fcfc98 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Sat, 21 Feb 2026 15:04:58 +0000 Subject: [PATCH] refactored code into components (major cleanup for reused code) added email for local acccount creation and changes to how nas and google photo connections work and added themes --- App.js | 83 +++-- ignorefiles/INSTALL_COMMANDS.md | 27 ++ package.json | 1 + src/AUTH/GoogleAUTH.js | 226 +++++++++++--- src/components/MediaViewer.js | 74 +++++ src/components/NASSetupInstructions.js | 101 +++++++ src/components/PhotoGrid.js | 83 +++++ src/components/TopBar.js | 65 ++++ src/screens/AlbumsScreen.js | 277 +++++++++-------- src/screens/HomeScreen.js | 307 +++++-------------- src/screens/LoginScreen.js | 211 +++++-------- src/screens/OnboardingScreen.js | 331 +++++++++----------- src/screens/RegisterScreen.js | 244 +++++---------- src/screens/SearchScreen.js | 130 ++------ src/screens/SettingsScreen.js | 401 +++++++++++++++++++++++++ src/services/DatabaseService.js | 67 +++++ src/services/ThemeContext.js | 151 ++++++++++ 17 files changed, 1761 insertions(+), 1018 deletions(-) create mode 100644 ignorefiles/INSTALL_COMMANDS.md create mode 100644 src/components/MediaViewer.js create mode 100644 src/components/NASSetupInstructions.js create mode 100644 src/components/PhotoGrid.js create mode 100644 src/components/TopBar.js create mode 100644 src/screens/SettingsScreen.js create mode 100644 src/services/ThemeContext.js diff --git a/App.js b/App.js index e83c4f8..36df805 100644 --- a/App.js +++ b/App.js @@ -4,53 +4,51 @@ import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { ActivityIndicator, View, Text, TouchableOpacity } from 'react-native'; +import { ThemeProvider, useTheme } from './src/services/ThemeContext'; import DatabaseService from './src/services/DatabaseService'; import LoginScreen from './src/screens/LoginScreen'; import RegisterScreen from './src/screens/RegisterScreen'; import OnboardingScreen from './src/screens/OnboardingScreen'; import HomeScreen from './src/screens/HomeScreen'; import SearchScreen from './src/screens/SearchScreen'; -import AlbumsScreen from './src/screens/AlbumsScreen'; +import CollectionsScreen from './src/screens/AlbumsScreen'; +import SettingsScreen from './src/screens/SettingsScreen'; const Stack = createNativeStackNavigator(); const Tab = createBottomTabNavigator(); /** - * MainTabs - Bottom tab navigator for logged-in users - * Tabs: Home (Library), Search, Albums + * MainTabs – Bottom tab navigator for logged-in users + * Tabs: Library, Search, Collections */ function MainTabs({ route }) { const { userId } = route.params || {}; + const { theme } = useTheme(); return ( ( - 🖼️ - ), + tabBarLabel: 'Library', + tabBarIcon: ({ color }) => 🖼️, }} /> ( - 🔍 - ), + tabBarIcon: ({ color }) => 🔍, }} /> ( - 📁 - ), + tabBarLabel: 'Collections', + tabBarIcon: ({ color }) => 📁, }} /> @@ -80,31 +74,26 @@ function MainTabs({ route }) { } /** - * AppNavigator - Root stack navigator - * Handles auth flow (Login/Register/Onboarding) then MainTabs + * AppNavigator – Root stack navigator */ function AppNavigator() { return ( - + + ); } -export default function App() { +function AppInner() { + const { theme } = useTheme(); const [isReady, setIsReady] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - initializeApp(); - }, []); + useEffect(() => { initializeApp(); }, []); const initializeApp = async () => { try { @@ -120,21 +109,19 @@ export default function App() { if (!isReady && !error) { return ( - - + + ); } if (error) { return ( - - - {error} - + + {error} Retry @@ -147,7 +134,15 @@ export default function App() { - + ); } + +export default function App() { + return ( + + + + ); +} diff --git a/ignorefiles/INSTALL_COMMANDS.md b/ignorefiles/INSTALL_COMMANDS.md new file mode 100644 index 0000000..04d7b74 --- /dev/null +++ b/ignorefiles/INSTALL_COMMANDS.md @@ -0,0 +1,27 @@ +# Install Commands + +## Client App (root folder) +```bash +npm install expo expo-sqlite expo-crypto expo-secure-store expo-auth-session expo-web-browser expo-linking expo-blur expo-file-system expo-image-picker expo-media-library expo-status-bar react react-native @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs react-native-safe-area-context react-native-screens axios @babel/core +``` + +## NAS Backend Server (ServerFiles folder) +```bash +cd ServerFiles && npm install express cors dotenv jsonwebtoken express-fileupload body-parser node-fetch@2 nodemon +``` + +## Quick Setup (both at once) +```bash +npm install && cd ServerFiles && npm install && cd .. +``` + +## Version Fix (if React version mismatch) +```bash +npx expo install --fix +``` + +## Clear Cache + Rebuild +```bash +npx expo start --clear +``` + diff --git a/package.json b/package.json index d1aeceb..eaaa7dd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "axios": "^1.7.9", "expo": "~54.0.33", "expo-auth-session": "~7.0.10", + "expo-blur": "~15.0.8", "expo-crypto": "~15.0.8", "expo-file-system": "~19.0.21", "expo-image-picker": "~17.0.10", diff --git a/src/AUTH/GoogleAUTH.js b/src/AUTH/GoogleAUTH.js index c198183..495c00e 100644 --- a/src/AUTH/GoogleAUTH.js +++ b/src/AUTH/GoogleAUTH.js @@ -1,18 +1,26 @@ +import * as AuthSession from 'expo-auth-session'; +import * as WebBrowser from 'expo-web-browser'; import { AuthServiceBase } from './AuthServiceBase'; -/** - * Google Photos Authentication Service - * - * Uses placeholder tokens for now. - * To enable real Google OAuth, set up a Google Cloud project with Photos Library API, - * create OAuth 2.0 credentials, and replace GOOGLE_CLIENT_ID below. - * - * Real OAuth flow uses expo-auth-session + expo-web-browser. - */ +WebBrowser.maybeCompleteAuthSession(); // TODO: Replace with your real Google Cloud OAuth Client ID const GOOGLE_CLIENT_ID = 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com'; +const GOOGLE_DISCOVERY = { + authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth', + tokenEndpoint: 'https://oauth2.googleapis.com/token', + revocationEndpoint: 'https://oauth2.googleapis.com/revoke', +}; + +// Google Photos Library API scopes - read and write +const SCOPES = [ + 'https://www.googleapis.com/auth/photoslibrary', + 'https://www.googleapis.com/auth/photoslibrary.appendonly', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +]; + class GoogleAUTH extends AuthServiceBase { constructor() { super(); @@ -20,21 +28,21 @@ class GoogleAUTH extends AuthServiceBase { this.refreshToken = null; this.expiresAt = null; this.email = null; + this.idToken = null; } /** - * Authenticate with Google - * Currently uses placeholder tokens. Replace with real OAuth when client ID is configured. + * Authenticate with Google using expo-auth-session + * Falls back to placeholder tokens when no real client ID is set */ async authenticate() { try { if (GOOGLE_CLIENT_ID === 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com') { - // Placeholder mode - no real client ID configured console.log('Google Auth: Using placeholder mode (no client ID configured)'); this.accessToken = 'placeholder_google_token_' + Date.now(); this.refreshToken = 'placeholder_refresh_' + Date.now(); this.expiresAt = Date.now() + 3600000; - this.email = 'google-user'; + this.email = 'google-user@placeholder.com'; return { success: true, @@ -44,26 +52,92 @@ class GoogleAUTH extends AuthServiceBase { }; } - // Real OAuth flow would go here using expo-auth-session - // const discovery = { authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth', tokenEndpoint: 'https://oauth2.googleapis.com/token' }; - // Use AuthSession.makeRedirectUri() and AuthSession.useAuthRequest() - throw new Error('Google OAuth client ID configured but real OAuth flow not yet implemented'); + // Real OAuth flow + const redirectUri = AuthSession.makeRedirectUri({ preferLocalhost: true }); + + const authRequest = new AuthSession.AuthRequest({ + clientId: GOOGLE_CLIENT_ID, + scopes: SCOPES, + redirectUri, + responseType: AuthSession.ResponseType.Code, + usePKCE: true, + extraParams: { access_type: 'offline', prompt: 'consent' }, + }); + + const result = await authRequest.promptAsync(GOOGLE_DISCOVERY); + + if (result.type !== 'success') { + throw new Error('Google authentication was cancelled or failed'); + } + + const tokenResponse = await AuthSession.exchangeCodeAsync( + { + clientId: GOOGLE_CLIENT_ID, + code: result.params.code, + redirectUri, + extraParams: { code_verifier: authRequest.codeVerifier }, + }, + GOOGLE_DISCOVERY + ); + + this.accessToken = tokenResponse.accessToken; + this.refreshToken = tokenResponse.refreshToken; + this.idToken = tokenResponse.idToken; + this.expiresAt = tokenResponse.issuedAt + ? (tokenResponse.issuedAt + (tokenResponse.expiresIn || 3600)) * 1000 + : Date.now() + 3600000; + + const userInfo = await this.fetchUserInfo(); + this.email = userInfo?.email || 'google-user'; + + return { + success: true, + accessToken: this.accessToken, + refreshToken: this.refreshToken, + email: this.email, + }; } catch (error) { console.error('Google authentication error:', error); throw error; } } + async fetchUserInfo() { + try { + const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { + headers: { Authorization: `Bearer ${this.accessToken}` }, + }); + if (!response.ok) return null; + return await response.json(); + } catch { + return null; + } + } + async getAccessToken() { if (this.accessToken && this.expiresAt > Date.now()) return this.accessToken; + if (this.refreshToken) return await this.refreshAccessToken(); throw new Error('No valid Google access token'); } async refreshAccessToken() { try { - console.log('Refreshing Google access token...'); - this.accessToken = 'refreshed_google_token_' + Date.now(); - this.expiresAt = Date.now() + 3600000; + if (GOOGLE_CLIENT_ID === 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com') { + this.accessToken = 'refreshed_placeholder_' + Date.now(); + this.expiresAt = Date.now() + 3600000; + return this.accessToken; + } + + const tokenResponse = await AuthSession.refreshAsync( + { clientId: GOOGLE_CLIENT_ID, refreshToken: this.refreshToken }, + GOOGLE_DISCOVERY + ); + + this.accessToken = tokenResponse.accessToken; + this.expiresAt = tokenResponse.issuedAt + ? (tokenResponse.issuedAt + (tokenResponse.expiresIn || 3600)) * 1000 + : Date.now() + 3600000; + return this.accessToken; } catch (error) { console.error('Token refresh error:', error); @@ -72,6 +146,13 @@ class GoogleAUTH extends AuthServiceBase { } async revokeAccess() { + try { + if (this.accessToken && GOOGLE_CLIENT_ID !== 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com') { + await AuthSession.revokeAsync({ token: this.accessToken }, GOOGLE_DISCOVERY); + } + } catch (err) { + console.error('Revoke error:', err); + } this.accessToken = null; this.refreshToken = null; this.expiresAt = null; @@ -80,31 +161,108 @@ class GoogleAUTH extends AuthServiceBase { } async getUserInfo() { - return { - id: 'google_user', - email: this.email || 'unknown', - name: 'Google User', - }; + const info = await this.fetchUserInfo(); + return info || { id: 'google_user', email: this.email || 'unknown', name: 'Google User' }; } - async getPhotos(pageSize = 50) { + /** + * Fetch media items from Google Photos Library API + */ + async getPhotos(pageSize = 50, pageToken = null) { try { - await this.getAccessToken(); - // In production: call Google Photos API - // https://photoslibrary.googleapis.com/v1/mediaItems - return { mediaItems: [], nextPageToken: null }; + const token = await this.getAccessToken(); + const params = new URLSearchParams({ pageSize: String(pageSize) }); + if (pageToken) params.append('pageToken', pageToken); + + const response = await fetch( + `https://photoslibrary.googleapis.com/v1/mediaItems?${params.toString()}`, + { headers: { Authorization: `Bearer ${token}` } } + ); + + if (!response.ok) { + console.error('Google Photos API error:', response.status); + return { mediaItems: [], nextPageToken: null }; + } + + const data = await response.json(); + return { + mediaItems: (data.mediaItems || []).map((item) => ({ + source_service: 'GOOGLE_PHOTOS', + source_id: item.id, + title: item.filename || 'Untitled', + description: item.description || '', + local_path: item.baseUrl ? `${item.baseUrl}=w400-h400` : null, + remote_path: item.productUrl || '', + media_type: item.mimeType?.startsWith('video') ? 'video' : 'photo', + created_date: item.mediaMetadata?.creationTime || null, + })), + nextPageToken: data.nextPageToken || null, + }; } catch (error) { console.error('Get photos error:', error); - throw error; + return { mediaItems: [], nextPageToken: null }; } } - async getAlbums() { + /** + * Fetch albums from Google Photos Library API + */ + async getAlbums(pageSize = 50, pageToken = null) { try { - await this.getAccessToken(); - return { albums: [], nextPageToken: null }; + const token = await this.getAccessToken(); + const params = new URLSearchParams({ pageSize: String(pageSize) }); + if (pageToken) params.append('pageToken', pageToken); + + const response = await fetch( + `https://photoslibrary.googleapis.com/v1/albums?${params.toString()}`, + { headers: { Authorization: `Bearer ${token}` } } + ); + + if (!response.ok) return { albums: [], nextPageToken: null }; + const data = await response.json(); + return { albums: data.albums || [], nextPageToken: data.nextPageToken || null }; } catch (error) { console.error('Get albums error:', error); + return { albums: [], nextPageToken: null }; + } + } + + /** + * Upload media to Google Photos Library + */ + async uploadPhoto(filePath, filename, mimeType = 'image/jpeg') { + try { + const token = await this.getAccessToken(); + + const uploadResponse = await fetch('https://photoslibrary.googleapis.com/v1/uploads', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/octet-stream', + 'X-Goog-Upload-File-Name': filename, + 'X-Goog-Upload-Protocol': 'raw', + }, + body: await fetch(filePath).then((r) => r.blob()), + }); + + if (!uploadResponse.ok) throw new Error('Upload bytes failed'); + const uploadToken = await uploadResponse.text(); + + const createResponse = await fetch( + 'https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate', + { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + newMediaItems: [{ simpleMediaItem: { uploadToken, fileName: filename } }], + }), + } + ); + + if (!createResponse.ok) throw new Error('Create media item failed'); + return await createResponse.json(); + } catch (error) { + console.error('Upload photo error:', error); throw error; } } diff --git a/src/components/MediaViewer.js b/src/components/MediaViewer.js new file mode 100644 index 0000000..a0971aa --- /dev/null +++ b/src/components/MediaViewer.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { + Modal, View, Text, TouchableOpacity, Image, StyleSheet, +} from 'react-native'; +import { useTheme } from '../services/ThemeContext'; + +const getServiceIcon = (type) => + ({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱'); +const getServiceName = (type) => + ({ GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' }[type] || type); + +const MediaViewer = ({ visible, media, onClose }) => { + const { theme } = useTheme(); + + if (!media) return null; + + return ( + + + + ✕ + + {media.local_path ? ( + + ) : ( + + 📷 + + )} + + {media.title || 'Photo'} + + Source + + {getServiceIcon(media.source_service)} {getServiceName(media.source_service)} + + + {media.created_date && ( + + Date + + {new Date(media.created_date).toLocaleDateString()} + + + )} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1 }, + closeButton: { + position: 'absolute', top: 44, right: 16, zIndex: 20, + width: 36, height: 36, borderRadius: 18, + backgroundColor: 'rgba(255,255,255,0.2)', + justifyContent: 'center', alignItems: 'center', + }, + closeText: { color: '#fff', fontSize: 20, fontWeight: '600' }, + fullImage: { flex: 1, width: '100%' }, + fullPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + infoPanel: { + padding: 16, paddingBottom: 32, + borderTopLeftRadius: 16, borderTopRightRadius: 16, + }, + title: { fontSize: 16, fontWeight: '600', color: '#fff', marginBottom: 8 }, + row: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 }, + label: { fontSize: 12, color: '#9aa0a6' }, + value: { fontSize: 12, color: '#e8eaed', fontWeight: '500' }, +}); + +export default MediaViewer; + diff --git a/src/components/NASSetupInstructions.js b/src/components/NASSetupInstructions.js new file mode 100644 index 0000000..f962779 --- /dev/null +++ b/src/components/NASSetupInstructions.js @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { useTheme } from '../services/ThemeContext'; + +const NASSetupInstructions = () => { + const { theme } = useTheme(); + const [expanded, setExpanded] = useState(false); + + return ( + + setExpanded(!expanded)} + activeOpacity={0.7} + > + + 📋 How to set up a NAS for photo storage + + + {expanded ? '▲' : '▼'} + + + + {expanded && ( + + Prerequisites + + • An Ubuntu server (20.04 LTS or later) with SSH access enabled{'\n'} + • A user account with sudo access on the server{'\n'} + • The server's IP address and SSH port (default: 22) + + + Step 1: Enable SSH + + If SSH is not already enabled on your Ubuntu server, open a terminal on the server and run:{'\n\n'} + sudo apt update && sudo apt install openssh-server{'\n'} + sudo systemctl enable ssh && sudo systemctl start ssh + + + Step 2: Create a Photos Directory + + Create a dedicated folder for your photo storage:{'\n\n'} + mkdir -p ~/photos{'\n'} + chmod 755 ~/photos + + + Step 3: Configure Firewall (if needed) + + If your server uses UFW, allow SSH connections:{'\n\n'} + sudo ufw allow ssh{'\n'} + sudo ufw enable + + + Step 4: Find Your Server IP + + Run this on your server to find its local IP:{'\n\n'} + hostname -I{'\n\n'} + Use that IP address in the "Server Address" field above. + + + Step 5: Connect from the App + + Enter the server IP, SSH port (22), your username, password, and the photos folder path (e.g. /home/youruser/photos).{'\n\n'} + The app's backend server will handle the SFTP connection securely. + + + Step 6: Run the Backend Server + + The NAS connection uses a backend server. Navigate to the ServerFiles folder and run:{'\n\n'} + npm install{'\n'} + node server.js{'\n\n'} + Ensure the backend URL in the app settings matches the server address (e.g. http://192.168.1.100:3001). + + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + borderRadius: 12, + borderWidth: 1, + overflow: 'hidden', + marginTop: 8, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 14, + }, + headerText: { fontSize: 13, fontWeight: '600', flex: 1, marginRight: 8 }, + arrow: { fontSize: 12 }, + body: { paddingHorizontal: 14, paddingBottom: 14 }, + stepTitle: { fontSize: 13, fontWeight: '700', marginTop: 12, marginBottom: 4 }, + stepText: { fontSize: 12, lineHeight: 18 }, +}); + +export default NASSetupInstructions; + diff --git a/src/components/PhotoGrid.js b/src/components/PhotoGrid.js new file mode 100644 index 0000000..0e2e4f5 --- /dev/null +++ b/src/components/PhotoGrid.js @@ -0,0 +1,83 @@ +import React from 'react'; +import { + View, Text, FlatList, TouchableOpacity, Image, Dimensions, StyleSheet, +} from 'react-native'; +import { useTheme } from '../services/ThemeContext'; + +const { width } = Dimensions.get('window'); +const GRID_GAP = 2; + +const PhotoGrid = ({ + data, + onItemPress, + columns = 3, + refreshControl, + ListHeaderComponent, + ListEmptyComponent, +}) => { + const { theme } = useTheme(); + const itemSize = (width - GRID_GAP * (columns + 1)) / columns; + + const getServiceIcon = (type) => + ({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱'); + + const renderItem = ({ item }) => ( + onItemPress?.(item)} + activeOpacity={0.8} + > + {item.local_path ? ( + + ) : ( + + 📷 + + )} + {item.media_type === 'video' && ( + + ▶ + + )} + + {getServiceIcon(item.source_service)} + + + ); + + return ( + item.id?.toString() || item.source_id || i.toString()} + numColumns={columns} + contentContainerStyle={styles.gridContent} + refreshControl={refreshControl} + ListHeaderComponent={ListHeaderComponent} + ListEmptyComponent={ListEmptyComponent} + /> + ); +}; + +const styles = StyleSheet.create({ + gridContent: { paddingBottom: 20 }, + gridItem: { + margin: GRID_GAP / 2, + position: 'relative', + }, + gridImage: { width: '100%', height: '100%' }, + placeholder: { + width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', + }, + videoOverlay: { + position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, + backgroundColor: 'rgba(0,0,0,0.25)', justifyContent: 'center', alignItems: 'center', + }, + sourceBadge: { + position: 'absolute', bottom: 4, right: 4, backgroundColor: 'rgba(0,0,0,0.5)', + borderRadius: 8, paddingHorizontal: 4, paddingVertical: 2, + }, +}); + +export default PhotoGrid; + diff --git a/src/components/TopBar.js b/src/components/TopBar.js new file mode 100644 index 0000000..d6582e3 --- /dev/null +++ b/src/components/TopBar.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { useTheme } from '../services/ThemeContext'; + +const TopBar = ({ currentUser, onSearchPress, onProfilePress, onMenuPress }) => { + const { theme } = useTheme(); + + return ( + + + 🔍 + Search your photos + + + + {currentUser?.username?.charAt(0)?.toUpperCase() || '?'} + + + + ⋮ + + + ); +}; + +const styles = StyleSheet.create({ + topBar: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12, + paddingTop: 48, + paddingBottom: 8, + gap: 8, + }, + searchPill: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 24, + paddingHorizontal: 14, + paddingVertical: 10, + gap: 8, + }, + searchIcon: { fontSize: 16 }, + searchText: { fontSize: 14 }, + profileButton: { + width: 32, + height: 32, + borderRadius: 16, + justifyContent: 'center', + alignItems: 'center', + }, + profileInitial: { color: '#fff', fontSize: 14, fontWeight: '700' }, + menuButton: { width: 32, height: 32, justifyContent: 'center', alignItems: 'center' }, + menuDots: { fontSize: 22, fontWeight: '700' }, +}); + +export default TopBar; + diff --git a/src/screens/AlbumsScreen.js b/src/screens/AlbumsScreen.js index 2f4f0a3..0b57496 100644 --- a/src/screens/AlbumsScreen.js +++ b/src/screens/AlbumsScreen.js @@ -1,210 +1,247 @@ import React, { useState, useCallback } from 'react'; import { - StyleSheet, View, Text, FlatList, TouchableOpacity, + StyleSheet, View, Text, SectionList, FlatList, TouchableOpacity, Dimensions, Image, ActivityIndicator, RefreshControl, } from 'react-native'; import { useFocusEffect } from '@react-navigation/native'; +import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; +import MediaViewer from '../components/MediaViewer'; const { width } = Dimensions.get('window'); const ALBUM_COLUMNS = 2; const ALBUM_GAP = 12; const albumSize = (width - ALBUM_GAP * (ALBUM_COLUMNS + 1)) / ALBUM_COLUMNS; -const AlbumsScreen = ({ route }) => { +const CollectionsScreen = ({ route, navigation }) => { const userId = route?.params?.userId; - const [albums, setAlbums] = useState([]); + const { theme } = useTheme(); + const [sections, setSections] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [selectedAlbum, setSelectedAlbum] = useState(null); const [albumMedia, setAlbumMedia] = useState([]); + const [selectedMedia, setSelectedMedia] = useState(null); + const [viewerVisible, setViewerVisible] = useState(false); useFocusEffect( useCallback(() => { - if (userId) loadAlbums(); + if (userId) loadCollections(); }, [userId]) ); - const loadAlbums = async () => { + const getServiceIcon = (type) => ({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱'); + const getServiceName = (type) => ({ GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud Photos', NAS: 'NAS Storage' }[type] || type); + + const loadCollections = async () => { try { const accounts = await DatabaseService.getLinkedAccounts(userId); - const albumList = []; + const allMedia = await DatabaseService.getMediaByUser(userId); + const sectionList = []; + // ─── Utility albums (top) ─── + const utilityAlbums = [ + { name: 'All Photos', icon: '📸', count: allMedia.length, media: allMedia, type: 'utility' }, + { name: 'Hidden', icon: '🙈', count: 0, media: [], type: 'utility' }, + { name: 'Duplicates', icon: '📋', count: 0, media: [], type: 'utility' }, + { name: 'Recently Deleted', icon: '🗑️', count: 0, media: [], type: 'utility' }, + ]; + sectionList.push({ title: '', data: [{ albums: utilityAlbums }] }); + + // ─── Per-service categories ─── for (const acc of accounts) { const media = await DatabaseService.getMediaByService(userId, acc.service_type); - albumList.push({ - service_type: acc.service_type, - name: getServiceName(acc.service_type), + const serviceAlbums = [ + { + name: `All ${getServiceName(acc.service_type)}`, + icon: getServiceIcon(acc.service_type), + count: media.length, + coverImage: media.length > 0 ? media[0].local_path : null, + media, + type: acc.service_type, + }, + ]; + sectionList.push({ + title: getServiceName(acc.service_type), icon: getServiceIcon(acc.service_type), - count: media.length, - coverImage: media.length > 0 ? media[0].local_path : null, - media, + data: [{ albums: serviceAlbums }], }); } - // Also add an "All Photos" album - const allMedia = await DatabaseService.getMediaByUser(userId); - albumList.unshift({ - service_type: 'ALL', - name: 'All Photos', - icon: '📸', - count: allMedia.length, - coverImage: allMedia.length > 0 ? allMedia[0].local_path : null, - media: allMedia, - }); - - setAlbums(albumList); + setSections(sectionList); } catch (err) { - console.error('Error loading albums:', err); + console.error('Error loading collections:', err); } finally { setLoading(false); setRefreshing(false); } }; - const getServiceIcon = (type) => ({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱'); - const getServiceName = (type) => ({ GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' }[type] || type); - - const renderAlbumCard = ({ item }) => ( - { - setSelectedAlbum(item); - setAlbumMedia(item.media); - }} - activeOpacity={0.8} - > - - {item.coverImage ? ( - - ) : ( - - {item.icon} - - )} - - {item.name} - {item.count} item{item.count !== 1 ? 's' : ''} - - ); - - const renderMediaItem = ({ item }) => ( - - {item.local_path ? ( - - ) : ( - - 📷 - - )} - - ); - - if (loading) { - return ( - - - Loading albums... - - ); - } - // Album detail view if (selectedAlbum) { + const gridItemSize = (width - 6) / 3; return ( - - + + { setSelectedAlbum(null); setAlbumMedia([]); }}> - ← + ← - {selectedAlbum.name} - {selectedAlbum.count} items + {selectedAlbum.name} + + {selectedAlbum.count} item{selectedAlbum.count !== 1 ? 's' : ''} + {albumMedia.length === 0 ? ( 📭 - No photos in this album + No items in this collection ) : ( ( + { setSelectedMedia(item); setViewerVisible(true); }} + activeOpacity={0.8} + > + {item.local_path ? ( + + ) : ( + + 📷 + + )} + + )} keyExtractor={(item, i) => item.id?.toString() || i.toString()} numColumns={3} contentContainerStyle={styles.gridContent} /> )} + setViewerVisible(false)} /> ); } - // Albums grid view - return ( - - - Albums + if (loading) { + return ( + + + Loading collections... - {albums.length === 0 ? ( + ); + } + + const renderAlbumRow = ({ item }) => ( + + {item.albums.map((album, idx) => ( + { setSelectedAlbum(album); setAlbumMedia(album.media); }} + activeOpacity={0.8} + > + + {album.coverImage ? ( + + ) : ( + + {album.icon} + + )} + + {album.name} + + {album.count} item{album.count !== 1 ? 's' : ''} + + + ))} + + ); + + const renderSectionHeader = ({ section }) => { + if (!section.title) return null; + return ( + + {section.icon} + {section.title} + + ); + }; + + return ( + + + Collections + + + {sections.length === 0 || (sections.length === 1 && sections[0].data[0].albums[0].count === 0) ? ( 📁 - No Albums - Albums from your connected services will appear here + No Collections + + Collections from your connected services will appear here + ) : ( - item.service_type} - numColumns={ALBUM_COLUMNS} - columnWrapperStyle={styles.albumRow} - contentContainerStyle={styles.albumsContent} - refreshControl={ { setRefreshing(true); loadAlbums(); }} />} + i.toString()} + contentContainerStyle={styles.listContent} + stickySectionHeadersEnabled={false} + refreshControl={ + { setRefreshing(true); loadCollections(); }} /> + } /> )} ); }; -const gridItemSize = (width - 6) / 3; - const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#fff' }, + container: { flex: 1 }, centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 24 }, header: { paddingHorizontal: 16, paddingTop: 52, paddingBottom: 12 }, - headerTitle: { fontSize: 24, fontWeight: '700', color: '#202124' }, - albumsContent: { paddingHorizontal: ALBUM_GAP, paddingBottom: 20 }, - albumRow: { gap: ALBUM_GAP, marginBottom: ALBUM_GAP }, - albumCard: { width: albumSize }, - albumCover: { - width: albumSize, height: albumSize, borderRadius: 12, overflow: 'hidden', - backgroundColor: '#f1f3f4', marginBottom: 8, + headerTitle: { fontSize: 24, fontWeight: '700' }, + listContent: { paddingHorizontal: ALBUM_GAP, paddingBottom: 20 }, + // Section headers + sectionHeader: { + flexDirection: 'row', alignItems: 'center', paddingVertical: 12, gap: 8, }, + sectionIcon: { fontSize: 20 }, + sectionTitle: { fontSize: 16, fontWeight: '700' }, + // Album grid + albumRow: { flexDirection: 'row', gap: ALBUM_GAP, marginBottom: ALBUM_GAP }, + albumCard: {}, + albumCover: { borderRadius: 12, overflow: 'hidden', marginBottom: 8 }, albumCoverImage: { width: '100%', height: '100%' }, albumCoverPlaceholder: { - width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', backgroundColor: '#e8eaed', + width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', }, - albumName: { fontSize: 14, fontWeight: '600', color: '#202124', paddingHorizontal: 2 }, - albumCount: { fontSize: 12, color: '#5f6368', paddingHorizontal: 2, marginTop: 2 }, + albumName: { fontSize: 14, fontWeight: '600', paddingHorizontal: 2 }, + albumCount: { fontSize: 12, paddingHorizontal: 2, marginTop: 2 }, // Album detail albumHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, - paddingTop: 52, paddingBottom: 12, gap: 12, + paddingTop: 52, paddingBottom: 12, gap: 12, borderBottomWidth: 1, }, - backArrow: { fontSize: 24, color: '#202124' }, + backArrow: { fontSize: 24 }, albumHeaderInfo: { flex: 1 }, - albumHeaderTitle: { fontSize: 20, fontWeight: '700', color: '#202124' }, - albumHeaderCount: { fontSize: 12, color: '#5f6368' }, + albumHeaderTitle: { fontSize: 20, fontWeight: '700' }, + albumHeaderCount: { fontSize: 12 }, gridContent: { paddingBottom: 20 }, - gridItem: { width: gridItemSize, height: gridItemSize, margin: 1, backgroundColor: '#f1f3f4' }, + gridItem: { margin: 1 }, gridImage: { width: '100%', height: '100%' }, - gridPlaceholder: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', backgroundColor: '#e8eaed' }, - // Empty - loadingText: { marginTop: 12, fontSize: 14, color: '#5f6368' }, - emptyTitle: { fontSize: 18, fontWeight: '600', color: '#202124', marginBottom: 6 }, - emptyText: { fontSize: 13, color: '#5f6368', textAlign: 'center' }, + gridPlaceholder: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center' }, + // Empty / Loading + loadingText: { marginTop: 12, fontSize: 14 }, + emptyTitle: { fontSize: 18, fontWeight: '600', marginBottom: 6 }, + emptyText: { fontSize: 13, textAlign: 'center' }, }); -export default AlbumsScreen; - +export default CollectionsScreen; diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index d756aa2..0636a93 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -1,21 +1,20 @@ import React, { useState, useEffect, useCallback } from 'react'; import { - StyleSheet, View, Text, FlatList, ActivityIndicator, Modal, - TouchableOpacity, Dimensions, Image, Alert, ScrollView, RefreshControl, + StyleSheet, View, Text, ActivityIndicator, ScrollView, RefreshControl, } from 'react-native'; import { useFocusEffect } from '@react-navigation/native'; +import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; import GoogleAUTH from '../AUTH/GoogleAUTH'; -import IOSAUTH from '../AUTH/IOSAUTH'; import NASAUTH from '../AUTH/NASAUTH'; - -const { width } = Dimensions.get('window'); -const GRID_COLUMNS = 3; -const GRID_GAP = 2; -const imageSize = (width - GRID_GAP * (GRID_COLUMNS + 1)) / GRID_COLUMNS; +import TopBar from '../components/TopBar'; +import PhotoGrid from '../components/PhotoGrid'; +import MediaViewer from '../components/MediaViewer'; +import { TouchableOpacity } from 'react-native'; const HomeScreen = ({ navigation, route }) => { const userId = route?.params?.userId; + const { theme } = useTheme(); const [currentUser, setCurrentUser] = useState(null); const [linkedAccounts, setLinkedAccounts] = useState([]); const [allMedia, setAllMedia] = useState([]); @@ -24,8 +23,7 @@ const HomeScreen = ({ navigation, route }) => { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [selectedMedia, setSelectedMedia] = useState(null); - const [modalVisible, setModalVisible] = useState(false); - const [menuVisible, setMenuVisible] = useState(false); + const [viewerVisible, setViewerVisible] = useState(false); useFocusEffect( useCallback(() => { @@ -37,9 +35,7 @@ const HomeScreen = ({ navigation, route }) => { if (currentUser) loadAllMedia(); }, [currentUser]); - useEffect(() => { - filterMedia(); - }, [selectedFilter, allMedia]); + useEffect(() => { filterMedia(); }, [selectedFilter, allMedia]); const loadUserData = async () => { try { @@ -63,8 +59,11 @@ const HomeScreen = ({ navigation, route }) => { if (googleAccount) { try { const googleAuth = new GoogleAUTH(); - // Would fetch real photos with valid token - console.log('Google Photos: ready to fetch when API is configured'); + googleAuth.accessToken = googleAccount.account_data?.accessToken; + googleAuth.refreshToken = googleAccount.account_data?.refreshToken; + googleAuth.expiresAt = Date.now() + 3600000; + const photos = await googleAuth.getPhotos(50); + if (photos?.mediaItems?.length) newMedia.push(...photos.mediaItems); } catch (err) { console.error('Google Photos fetch error:', err); } @@ -74,7 +73,6 @@ const HomeScreen = ({ navigation, route }) => { const iosAccount = linkedAccounts.find((a) => a.service_type === 'ICLOUD_PHOTOS'); if (iosAccount) { try { - const iosAuth = new IOSAUTH(); console.log('iCloud: placeholder - would fetch photos here'); } catch (err) { console.error('iCloud fetch error:', err); @@ -116,259 +114,102 @@ const HomeScreen = ({ navigation, route }) => { setFilteredMedia(filtered); }; - const handleLogout = () => { - Alert.alert('Logout', 'Are you sure?', [ - { text: 'Cancel', style: 'cancel' }, - { text: 'Logout', style: 'destructive', onPress: () => navigation.replace('Login') }, - ]); - }; - const getServiceIcon = (type) => ({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱'); const getServiceName = (type) => ({ GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' }[type] || type); - const renderMediaItem = ({ item }) => ( - { setSelectedMedia(item); setModalVisible(true); }} - activeOpacity={0.8} - > - {item.local_path ? ( - - ) : ( - - 📷 - - )} - {item.media_type === 'video' && ( - - ▶ - - )} - - {getServiceIcon(item.source_service)} - - - ); - - // Top bar component - const TopBar = () => ( - - navigation.navigate('Search')}> - 🔍 - Search your photos - - setMenuVisible(true)}> - - {currentUser?.username?.charAt(0)?.toUpperCase() || '?'} - - - setMenuVisible(true)}> - ⋮ - - - ); - - // Dropdown menu - const DropdownMenu = () => ( - setMenuVisible(false)}> - setMenuVisible(false)} activeOpacity={1}> - - - - {currentUser?.username?.charAt(0)?.toUpperCase() || '?'} - - {currentUser?.username || 'User'} - - - { setMenuVisible(false); navigation.navigate('Onboarding', { userId }); }}> - Manage Services - - { setMenuVisible(false); handleLogout(); }}> - Sign Out - - - - - ); - if (loading && allMedia.length === 0) { return ( - - - Loading photos... + + + Loading photos... ); } - return ( - - - + const FilterChips = () => ( + + setSelectedFilter('all')} + > + All + + {linkedAccounts.map((acc) => ( + setSelectedFilter(acc.service_type)} + > + {getServiceIcon(acc.service_type)} + + {getServiceName(acc.service_type)} + + + ))} + + ); - {/* Filter chips */} - {linkedAccounts.length > 0 && ( - - setSelectedFilter('all')} - > - All - - {linkedAccounts.map((acc) => ( - setSelectedFilter(acc.service_type)} - > - {getServiceIcon(acc.service_type)} - - {getServiceName(acc.service_type)} - - - ))} - - )} + return ( + + navigation.navigate('Search')} + onProfilePress={() => navigation.navigate('Settings', { userId })} + onMenuPress={() => navigation.navigate('Settings', { userId })} + /> + + {linkedAccounts.length > 0 && } {filteredMedia.length === 0 ? ( 📸 - No Photos Yet - Photos from your connected services will appear here - { setRefreshing(true); loadAllMedia(); }}> + No Photos Yet + + Photos from your connected services will appear here + + { setRefreshing(true); loadAllMedia(); }} + > Refresh ) : ( - item.id?.toString() || item.source_id || i.toString()} - numColumns={GRID_COLUMNS} - contentContainerStyle={styles.gridContent} - refreshControl={ { setRefreshing(true); loadAllMedia(); }} />} + onItemPress={(item) => { setSelectedMedia(item); setViewerVisible(true); }} + refreshControl={ + { setRefreshing(true); loadAllMedia(); }} /> + } /> )} - {/* Full-screen photo viewer */} - setModalVisible(false)}> - - setModalVisible(false)}> - ✕ - - {selectedMedia?.local_path ? ( - - ) : ( - - 📷 - - )} - - {selectedMedia?.title || 'Photo'} - - Source - - {getServiceIcon(selectedMedia?.source_service)} {getServiceName(selectedMedia?.source_service)} - - - {selectedMedia?.created_date && ( - - Date - {new Date(selectedMedia.created_date).toLocaleDateString()} - - )} - - - + setViewerVisible(false)} + /> ); }; const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#fff' }, + container: { flex: 1 }, centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 24 }, - // Top bar - topBar: { - flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingTop: 48, paddingBottom: 8, - backgroundColor: '#fff', gap: 8, - }, - topBarSearch: { - flex: 1, flexDirection: 'row', alignItems: 'center', backgroundColor: '#f1f3f4', - borderRadius: 24, paddingHorizontal: 14, paddingVertical: 10, gap: 8, - }, - topBarSearchIcon: { fontSize: 16 }, - topBarSearchText: { fontSize: 14, color: '#5f6368' }, - profileButton: { - width: 32, height: 32, borderRadius: 16, backgroundColor: '#1a73e8', - justifyContent: 'center', alignItems: 'center', - }, - profileInitial: { color: '#fff', fontSize: 14, fontWeight: '700' }, - menuButton: { width: 32, height: 32, justifyContent: 'center', alignItems: 'center' }, - menuDots: { fontSize: 22, color: '#5f6368', fontWeight: '700' }, - // Dropdown - menuOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.3)' }, - menuDropdown: { - position: 'absolute', top: 90, right: 12, backgroundColor: '#fff', - borderRadius: 12, minWidth: 200, paddingVertical: 8, - shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 8, elevation: 8, - }, - menuProfile: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 10 }, - menuAvatar: { - width: 36, height: 36, borderRadius: 18, backgroundColor: '#1a73e8', - justifyContent: 'center', alignItems: 'center', marginRight: 10, - }, - menuAvatarText: { color: '#fff', fontSize: 16, fontWeight: '700' }, - menuUsername: { fontSize: 15, fontWeight: '600', color: '#202124' }, - menuDivider: { height: 1, backgroundColor: '#e8eaed', marginVertical: 4 }, - menuItem: { paddingHorizontal: 16, paddingVertical: 12 }, - menuItemText: { fontSize: 14, color: '#202124' }, + loadingText: { marginTop: 12, fontSize: 14 }, // Filter chips filterRow: { paddingHorizontal: 12, paddingVertical: 8, gap: 8 }, chip: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 7, - backgroundColor: '#f1f3f4', borderRadius: 20, gap: 6, + borderRadius: 20, gap: 6, }, - chipActive: { backgroundColor: '#d2e3fc' }, chipIcon: { fontSize: 13 }, chipText: { fontSize: 13, fontWeight: '500', color: '#3c4043' }, - chipTextActive: { color: '#1a73e8', fontWeight: '600' }, - // Grid - gridContent: { paddingBottom: 20 }, - gridItem: { - width: imageSize, height: imageSize, margin: GRID_GAP / 2, - backgroundColor: '#f1f3f4', position: 'relative', - }, - gridImage: { width: '100%', height: '100%' }, - placeholder: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', backgroundColor: '#e8eaed' }, - videoOverlay: { - position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, - backgroundColor: 'rgba(0,0,0,0.25)', justifyContent: 'center', alignItems: 'center', - }, - sourceBadge: { - position: 'absolute', bottom: 4, right: 4, backgroundColor: 'rgba(0,0,0,0.5)', - borderRadius: 8, paddingHorizontal: 4, paddingVertical: 2, - }, - // Empty / Loading - loadingText: { marginTop: 12, fontSize: 14, color: '#5f6368' }, - emptyTitle: { fontSize: 18, fontWeight: '600', color: '#202124', marginBottom: 6 }, - emptyText: { fontSize: 13, color: '#5f6368', textAlign: 'center', marginBottom: 20 }, - retryButton: { backgroundColor: '#1a73e8', paddingHorizontal: 24, paddingVertical: 10, borderRadius: 20 }, + // Empty + emptyTitle: { fontSize: 18, fontWeight: '600', marginBottom: 6 }, + emptyText: { fontSize: 13, textAlign: 'center', marginBottom: 20 }, + retryButton: { paddingHorizontal: 24, paddingVertical: 10, borderRadius: 20 }, retryButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' }, - // Modal (photo viewer) - modalContainer: { flex: 1, backgroundColor: '#000' }, - modalClose: { - position: 'absolute', top: 44, right: 16, zIndex: 20, width: 36, height: 36, - borderRadius: 18, backgroundColor: 'rgba(255,255,255,0.2)', justifyContent: 'center', alignItems: 'center', - }, - modalCloseText: { color: '#fff', fontSize: 20, fontWeight: '600' }, - fullImage: { flex: 1, width: '100%' }, - fullPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - mediaInfo: { - backgroundColor: '#1a1a1a', padding: 16, paddingBottom: 32, borderTopLeftRadius: 16, borderTopRightRadius: 16, - }, - mediaTitle: { fontSize: 16, fontWeight: '600', color: '#fff', marginBottom: 8 }, - mediaRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 }, - mediaLabel: { fontSize: 12, color: '#9aa0a6' }, - mediaValue: { fontSize: 12, color: '#e8eaed', fontWeight: '500' }, }); export default HomeScreen; diff --git a/src/screens/LoginScreen.js b/src/screens/LoginScreen.js index c83d2e5..35d3600 100644 --- a/src/screens/LoginScreen.js +++ b/src/screens/LoginScreen.js @@ -1,18 +1,19 @@ import React, { useState } from 'react'; import { - StyleSheet, - View, - Text, - TextInput, - TouchableOpacity, - Alert, - ActivityIndicator, - KeyboardAvoidingView, - Platform, + StyleSheet, View, Text, TextInput, TouchableOpacity, Alert, + ActivityIndicator, KeyboardAvoidingView, Platform, Image, ScrollView, } from 'react-native'; +import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; +// ────────────────────────────────────────────────────────── +// App icon placeholder – change the require path when you +// have a final image in the assets folder. +// ────────────────────────────────────────────────────────── +const APP_ICON = require('../../assets/icon.png'); + const LoginScreen = ({ navigation }) => { + const { theme } = useTheme(); const [identifier, setIdentifier] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); @@ -27,7 +28,6 @@ const LoginScreen = ({ navigation }) => { try { const user = await DatabaseService.loginUser(identifier.trim(), password); if (user) { - // Check if user has completed onboarding (has linked accounts) const accounts = await DatabaseService.getLinkedAccounts(user.id); if (accounts.length > 0) { navigation.replace('MainTabs', { userId: user.id }); @@ -48,145 +48,98 @@ const LoginScreen = ({ navigation }) => { return ( - - - 📸 - Photo Library - Sign in to your account - + + + + + Photo Library + Sign in to your account + - - Username or Email - + + Username or Email + - Password - - + Password + - - {loading ? ( - - ) : ( - Sign In - )} - + + Forgot Password? + + - - Don't have an account? + {/* Login button – blue */} + {loading ? ( + + ) : ( + Login + )} + + + {/* Register button – white/outlined */} + navigation.navigate('Register')} disabled={loading} > - Create one + Register - + ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - }, - content: { - flex: 1, - justifyContent: 'center', - paddingHorizontal: 24, - paddingVertical: 40, - }, - headerContainer: { - marginBottom: 40, - alignItems: 'center', - }, - appIcon: { - fontSize: 56, - marginBottom: 16, - }, - title: { - fontSize: 28, - fontWeight: '700', - color: '#202124', - marginBottom: 8, - }, - subtitle: { - fontSize: 15, - color: '#5f6368', - }, - inputContainer: { - marginBottom: 24, - }, - label: { - fontSize: 13, - fontWeight: '600', - color: '#3c4043', - marginBottom: 6, - marginTop: 16, - }, + container: { flex: 1 }, + scrollContent: { flexGrow: 1, justifyContent: 'center' }, + content: { paddingHorizontal: 24, paddingVertical: 40 }, + headerContainer: { marginBottom: 36, alignItems: 'center' }, + appIcon: { width: 80, height: 80, marginBottom: 16, borderRadius: 16 }, + title: { fontSize: 28, fontWeight: '700', marginBottom: 8 }, + subtitle: { fontSize: 15 }, + inputContainer: { marginBottom: 24 }, + label: { fontSize: 13, fontWeight: '600', marginBottom: 6, marginTop: 16 }, input: { + borderWidth: 1, borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12, fontSize: 15, + }, + forgotPassword: { alignSelf: 'flex-end', marginTop: 8 }, + forgotPasswordText: { fontSize: 13, fontWeight: '600' }, + primaryButton: { + borderRadius: 8, paddingVertical: 14, alignItems: 'center', marginBottom: 10, borderWidth: 1, - borderColor: '#dadce0', - borderRadius: 8, - paddingHorizontal: 14, - paddingVertical: 12, - fontSize: 15, - backgroundColor: '#fff', - color: '#202124', }, - loginButton: { - backgroundColor: '#1a73e8', - borderRadius: 8, - paddingVertical: 14, - alignItems: 'center', - marginBottom: 20, - }, - buttonDisabled: { - opacity: 0.6, - }, - buttonText: { - color: '#fff', - fontSize: 15, - fontWeight: '600', - }, - registerContainer: { - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - }, - registerText: { - color: '#5f6368', - fontSize: 14, - }, - registerLink: { - color: '#1a73e8', - fontSize: 14, - fontWeight: '600', + primaryButtonText: { color: '#fff', fontSize: 15, fontWeight: '600' }, + secondaryButton: { + borderRadius: 8, paddingVertical: 14, alignItems: 'center', borderWidth: 1, }, + secondaryButtonText: { fontSize: 15, fontWeight: '600' }, + buttonDisabled: { opacity: 0.6 }, }); export default LoginScreen; diff --git a/src/screens/OnboardingScreen.js b/src/screens/OnboardingScreen.js index 9167315..171df85 100644 --- a/src/screens/OnboardingScreen.js +++ b/src/screens/OnboardingScreen.js @@ -1,21 +1,17 @@ import React, { useState } from 'react'; import { - StyleSheet, - View, - Text, - ScrollView, - TouchableOpacity, - Alert, - ActivityIndicator, - Switch, - TextInput, + StyleSheet, View, Text, ScrollView, TouchableOpacity, Alert, + ActivityIndicator, Switch, TextInput, } 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'; const OnboardingScreen = ({ navigation, route }) => { const { userId } = route.params; + const { theme } = useTheme(); const [currentStep, setCurrentStep] = useState(0); const [loading, setLoading] = useState(false); @@ -36,20 +32,20 @@ const OnboardingScreen = ({ navigation, route }) => { const [nasPassword, setNasPassword] = useState(''); const [nasPhotoFolder, setNasPhotoFolder] = useState('/home/user/photos'); - // Default service - const [defaultService, setDefaultService] = useState(null); + // Auto-backup checklist (multi-select) + const [autoBackupServices, setAutoBackupServices] = useState([]); const steps = [ { title: 'Choose Services', description: 'Select which photo services to connect (1-3)' }, { title: 'Connect Services', description: 'Set up each selected service' }, - { title: 'Default Upload', description: 'Choose where new photos upload by default' }, + { title: 'Automatic Backup', description: 'Choose where new photos are backed up' }, ]; - const getEnabledServices = () => { + const getConnectedServices = () => { const services = []; - if (googleEnabled) services.push({ name: 'Google Photos', type: 'GOOGLE_PHOTOS' }); - if (iosEnabled) services.push({ name: 'iCloud Photos', type: 'ICLOUD_PHOTOS' }); - if (nasEnabled) services.push({ name: 'NAS Storage', type: 'NAS' }); + if (googleEnabled && googleConnected) services.push({ name: 'Google Photos', type: 'GOOGLE_PHOTOS', icon: '📷' }); + if (iosEnabled && iosConnected) services.push({ name: 'iCloud Photos', type: 'ICLOUD_PHOTOS', icon: '🍎' }); + if (nasEnabled && nasConnected) services.push({ name: 'NAS Storage', type: 'NAS', icon: '💾' }); return services; }; @@ -78,12 +74,10 @@ const OnboardingScreen = ({ navigation, route }) => { const handleIosAuth = async () => { try { setLoading(true); - // Placeholder - iCloud API not yet available await DatabaseService.linkAccount(userId, 'ICLOUD_PHOTOS', { status: 'placeholder', note: 'iCloud integration pending Apple API access', }); - setIosConnected(true); Alert.alert('Info', 'iCloud Photos registered. Full integration coming soon.'); } catch (error) { @@ -99,25 +93,19 @@ const OnboardingScreen = ({ navigation, route }) => { Alert.alert('Error', 'Please fill in all NAS connection fields'); return; } - try { setLoading(true); const nasAuth = new NASAUTH(); await nasAuth.authenticate( - nasHost.trim(), - parseInt(nasPort) || 22, - nasUsername.trim(), - nasPassword, - nasPhotoFolder.trim() + nasHost.trim(), parseInt(nasPort) || 22, + nasUsername.trim(), nasPassword, nasPhotoFolder.trim() ); - await DatabaseService.linkAccount(userId, 'NAS', { host: nasHost.trim(), port: parseInt(nasPort) || 22, username: nasUsername.trim(), photoFolder: nasPhotoFolder.trim(), }); - setNasConnected(true); Alert.alert('Success', 'NAS connected!'); } catch (error) { @@ -128,38 +116,28 @@ const OnboardingScreen = ({ navigation, route }) => { } }; + const toggleAutoBackup = (serviceType) => { + setAutoBackupServices((prev) => + prev.includes(serviceType) ? prev.filter((s) => s !== serviceType) : [...prev, serviceType] + ); + }; + const handleNext = async () => { if (currentStep === 0) { - if (getEnabledServices().length === 0) { - Alert.alert('Error', 'Please select at least one service'); - return; - } + const any = googleEnabled || iosEnabled || nasEnabled; + if (!any) { Alert.alert('Error', 'Please select at least one service'); return; } setCurrentStep(1); } else if (currentStep === 1) { - // Check at least one service is connected const anyConnected = (googleEnabled && googleConnected) || (iosEnabled && iosConnected) || (nasEnabled && nasConnected); - - if (!anyConnected) { - Alert.alert('Error', 'Please connect at least one service before continuing'); - return; - } + if (!anyConnected) { Alert.alert('Error', 'Please connect at least one service'); return; } setCurrentStep(2); } else if (currentStep === 2) { - if (!defaultService) { - Alert.alert('Error', 'Please select a default upload service'); - return; - } - try { setLoading(true); - const accounts = await DatabaseService.getLinkedAccounts(userId); - const defaultAccount = accounts.find((a) => a.service_type === defaultService); - if (defaultAccount) { - await DatabaseService.setDefaultService(userId, defaultAccount.id); - } + await DatabaseService.setAutoBackupServices(userId, autoBackupServices); navigation.replace('MainTabs', { userId }); } catch (error) { Alert.alert('Error', 'Failed to complete setup: ' + error.message); @@ -170,45 +148,36 @@ const OnboardingScreen = ({ navigation, route }) => { }; return ( - + {/* Progress bar */} {steps.map((_, i) => ( - + ))} - Step {currentStep + 1} of {steps.length} - {steps[currentStep].title} - {steps[currentStep].description} + Step {currentStep + 1} of {steps.length} + {steps[currentStep].title} + {steps[currentStep].description} {/* Step 0: Select services */} {currentStep === 0 && ( - setGoogleEnabled(!googleEnabled)} - /> - setGoogleEnabled(!googleEnabled)} /> + setIosEnabled(!iosEnabled)} - /> - setIosEnabled(!iosEnabled)} /> + setNasEnabled(!nasEnabled)} - /> + enabled={nasEnabled} onToggle={() => setNasEnabled(!nasEnabled)} /> + + {/* NAS setup instructions dropdown */} + {nasEnabled && } )} @@ -216,84 +185,75 @@ const OnboardingScreen = ({ navigation, route }) => { {currentStep === 1 && ( {googleEnabled && ( - + 📷 - Google Photos - {googleConnected && ✓ Connected} + Google Photos + {googleConnected && ✓ Connected} {!googleConnected && ( - {loading ? : ( - Sign in with Google - )} + {loading ? : Sign in with Google} )} )} {iosEnabled && ( - + 🍎 - iCloud Photos - {iosConnected && ✓ Connected} + iCloud Photos + {iosConnected && ✓ Connected} {!iosConnected && ( - {loading ? : ( - Connect iCloud - )} + {loading ? : Connect iCloud} )} )} {nasEnabled && ( - + 💾 - NAS Storage - {nasConnected && ✓ Connected} + NAS Storage + {nasConnected && ✓ Connected} {!nasConnected && ( <> - Server Address - + Server Address + - Port - + Port + - Username - + Username + - Password - + Password + - Photos Folder - + Photos Folder + - {loading ? : ( - Test & Connect - )} + {loading ? : Test & Connect} )} @@ -302,47 +262,65 @@ const OnboardingScreen = ({ navigation, route }) => { )} - {/* Step 2: Default service */} + {/* Step 2: Auto-backup multi-checkbox */} {currentStep === 2 && ( - {getEnabledServices().map((svc) => { - const connected = - (svc.type === 'GOOGLE_PHOTOS' && googleConnected) || - (svc.type === 'ICLOUD_PHOTOS' && iosConnected) || - (svc.type === 'NAS' && nasConnected); + + + Select which services new photos should automatically upload to. You can select multiple or none. + - if (!connected) return null; - - return ( + {getConnectedServices().map((svc) => ( setDefaultService(svc.type)} + style={[styles.checkRow, { borderBottomColor: theme.borderLight }]} + onPress={() => toggleAutoBackup(svc.type)} + activeOpacity={0.7} > - - - {svc.name} - + + {autoBackupServices.includes(svc.type) && ✓} + + {svc.icon} + {svc.name} - ); - })} + ))} + + setAutoBackupServices([])} + activeOpacity={0.7} + > + + {autoBackupServices.length === 0 && ✓} + + Don't automatically back up + + )} {/* Footer */} - + {currentStep > 0 ? ( - setCurrentStep(currentStep - 1)} disabled={loading}> - Back + setCurrentStep(currentStep - 1)} disabled={loading}> + Back ) : ( )} {loading ? : ( @@ -355,99 +333,82 @@ const OnboardingScreen = ({ navigation, route }) => { ); }; -const ServiceToggleCard = ({ icon, name, description, enabled, onToggle }) => ( - +const ServiceToggleCard = ({ theme, icon, name, description, enabled, onToggle }) => ( + {icon} - {name} - {description} + {name} + {description} ); const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#fff' }, + container: { flex: 1 }, progressBar: { flexDirection: 'row', justifyContent: 'center', gap: 8, paddingTop: 60, paddingBottom: 8, }, - progressDot: { - width: 32, height: 4, borderRadius: 2, backgroundColor: '#dadce0', - }, - progressDotActive: { backgroundColor: '#1a73e8' }, + progressDot: { width: 32, height: 4, borderRadius: 2 }, content: { flex: 1, paddingHorizontal: 20 }, header: { marginBottom: 24 }, - stepLabel: { fontSize: 12, color: '#1a73e8', fontWeight: '600', marginBottom: 6 }, - title: { fontSize: 26, fontWeight: '700', color: '#202124', marginBottom: 6 }, - description: { fontSize: 14, color: '#5f6368' }, + stepLabel: { fontSize: 12, fontWeight: '600', marginBottom: 6 }, + title: { fontSize: 26, fontWeight: '700', marginBottom: 6 }, + description: { fontSize: 14 }, cardsContainer: { gap: 12 }, // Toggle cards (Step 0) toggleCard: { - backgroundColor: '#f8f9fa', borderRadius: 12, padding: 16, - flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - borderWidth: 1.5, borderColor: '#e8eaed', + borderRadius: 12, padding: 16, flexDirection: 'row', + justifyContent: 'space-between', alignItems: 'center', borderWidth: 1.5, }, - toggleCardActive: { borderColor: '#1a73e8', backgroundColor: '#e8f0fe' }, toggleCardContent: { flex: 1, flexDirection: 'row', alignItems: 'center' }, toggleIcon: { fontSize: 28, marginRight: 14 }, toggleInfo: { flex: 1 }, - toggleName: { fontSize: 15, fontWeight: '600', color: '#202124', marginBottom: 2 }, - toggleDescription: { fontSize: 12, color: '#5f6368' }, + toggleName: { fontSize: 15, fontWeight: '600', marginBottom: 2 }, + toggleDescription: { fontSize: 12 }, // Config cards (Step 1) - configCard: { - backgroundColor: '#f8f9fa', borderRadius: 12, padding: 16, - borderWidth: 1, borderColor: '#e8eaed', - }, + configCard: { borderRadius: 12, padding: 16, borderWidth: 1 }, configHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, configIcon: { fontSize: 24, marginRight: 10 }, - configTitle: { fontSize: 16, fontWeight: '600', color: '#202124', flex: 1 }, - connectedBadge: { fontSize: 13, color: '#34a853', fontWeight: '600' }, - authButton: { - backgroundColor: '#1a73e8', borderRadius: 8, paddingVertical: 12, alignItems: 'center', - }, + 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', color: '#3c4043', marginTop: 10, marginBottom: 4 }, + fieldLabel: { fontSize: 12, fontWeight: '600', marginTop: 10, marginBottom: 4 }, fieldInput: { - borderWidth: 1, borderColor: '#dadce0', borderRadius: 8, - paddingHorizontal: 12, paddingVertical: 10, fontSize: 14, - backgroundColor: '#fff', color: '#202124', + borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14, }, - // Select cards (Step 2) - selectCard: { - backgroundColor: '#f8f9fa', borderRadius: 12, padding: 16, - flexDirection: 'row', alignItems: 'center', - borderWidth: 2, borderColor: '#e8eaed', + // Auto-backup (Step 2) + checklistInfo: { fontSize: 13, marginBottom: 8 }, + checkRow: { + flexDirection: 'row', alignItems: 'center', paddingVertical: 14, + borderBottomWidth: StyleSheet.hairlineWidth, }, - selectCardActive: { borderColor: '#1a73e8', backgroundColor: '#e8f0fe' }, - radio: { - width: 20, height: 20, borderRadius: 10, borderWidth: 2, - borderColor: '#dadce0', marginRight: 12, + checkbox: { + width: 22, height: 22, borderRadius: 4, borderWidth: 2, + justifyContent: 'center', alignItems: 'center', marginRight: 12, }, - radioActive: { borderColor: '#1a73e8', backgroundColor: '#1a73e8' }, - selectText: { fontSize: 15, color: '#202124', fontWeight: '500' }, - selectTextActive: { color: '#1a73e8', fontWeight: '600' }, + 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, borderTopColor: '#e8eaed', gap: 12, + borderTopWidth: 1, gap: 12, }, backButton: { - flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center', - backgroundColor: '#f8f9fa', borderWidth: 1, borderColor: '#dadce0', - }, - backButtonText: { fontSize: 15, fontWeight: '600', color: '#5f6368' }, - nextButton: { - flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center', - backgroundColor: '#1a73e8', + flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center', borderWidth: 1, }, + backButtonText: { fontSize: 15, fontWeight: '600' }, + nextButton: { flex: 1, paddingVertical: 12, borderRadius: 8, alignItems: 'center' }, nextButtonText: { fontSize: 15, fontWeight: '600', color: '#fff' }, buttonDisabled: { opacity: 0.6 }, }); diff --git a/src/screens/RegisterScreen.js b/src/screens/RegisterScreen.js index a896c28..9c581dc 100644 --- a/src/screens/RegisterScreen.js +++ b/src/screens/RegisterScreen.js @@ -1,19 +1,16 @@ import React, { useState } from 'react'; import { - StyleSheet, - View, - Text, - TextInput, - TouchableOpacity, - Alert, - ActivityIndicator, - KeyboardAvoidingView, - Platform, - ScrollView, + StyleSheet, View, Text, TextInput, TouchableOpacity, Alert, + ActivityIndicator, KeyboardAvoidingView, Platform, ScrollView, Image, } from 'react-native'; +import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; +// Same icon constant as LoginScreen – edit in one place +const APP_ICON = require('../../assets/icon.png'); + const RegisterScreen = ({ navigation }) => { + const { theme } = useTheme(); const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -21,51 +18,23 @@ const RegisterScreen = ({ navigation }) => { const [loading, setLoading] = useState(false); const handleRegister = async () => { - if (!username.trim()) { - Alert.alert('Error', 'Please enter a username'); - return; - } - if (username.trim().length < 3) { - Alert.alert('Error', 'Username must be at least 3 characters'); - return; - } - if (!email.trim()) { - Alert.alert('Error', 'Please enter an email'); - return; - } - if (!email.includes('@') || !email.includes('.')) { - Alert.alert('Error', 'Please enter a valid email address'); - return; - } - if (!password.trim()) { - Alert.alert('Error', 'Please enter a password'); - return; - } - if (password.length < 6) { - Alert.alert('Error', 'Password must be at least 6 characters'); - return; - } - if (password !== confirmPassword) { - Alert.alert('Error', 'Passwords do not match'); - return; - } + if (!username.trim()) { Alert.alert('Error', 'Please enter a username'); return; } + if (username.trim().length < 3) { Alert.alert('Error', 'Username must be at least 3 characters'); return; } + if (!email.trim()) { Alert.alert('Error', 'Please enter an email'); return; } + if (!email.includes('@') || !email.includes('.')) { Alert.alert('Error', 'Please enter a valid email address'); return; } + if (!password.trim()) { Alert.alert('Error', 'Please enter a password'); return; } + if (password.length < 6) { Alert.alert('Error', 'Password must be at least 6 characters'); return; } + if (password !== confirmPassword) { Alert.alert('Error', 'Passwords do not match'); return; } setLoading(true); try { const exists = await DatabaseService.userExists(username.trim(), email.trim().toLowerCase()); - if (exists) { - Alert.alert('Error', 'Username or email already taken'); - setLoading(false); - return; - } + if (exists) { Alert.alert('Error', 'Username or email already taken'); setLoading(false); return; } const userId = await DatabaseService.createUser(username.trim(), email.trim().toLowerCase(), password); Alert.alert('Success', 'Account created!', [ - { - text: 'OK', - onPress: () => navigation.replace('Onboarding', { userId }), - }, + { text: 'OK', onPress: () => navigation.replace('Onboarding', { userId }) }, ]); } catch (error) { console.error('Registration error:', error); @@ -78,85 +47,69 @@ const RegisterScreen = ({ navigation }) => { return ( - 📸 - Create Account - Set up your local photo library account + + Create Account + Set up your local photo library account - Username + Username - Email + Email - Password + Password - Confirm Password + Confirm Password + {/* Create Account button – blue */} - {loading ? ( - - ) : ( - Create Account + {loading ? : ( + Create Account )} - - Already have an account? - navigation.navigate('Login')} disabled={loading}> - Sign in - - + {/* Back to Login button – white/outlined */} + navigation.navigate('Login')} + disabled={loading} + > + Back to Login + @@ -164,86 +117,27 @@ const RegisterScreen = ({ navigation }) => { }; const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - }, - scrollContent: { - flexGrow: 1, - justifyContent: 'center', - }, - content: { - paddingHorizontal: 24, - paddingVertical: 40, - }, - headerContainer: { - marginBottom: 36, - alignItems: 'center', - }, - appIcon: { - fontSize: 56, - marginBottom: 16, - }, - title: { - fontSize: 28, - fontWeight: '700', - color: '#202124', - marginBottom: 8, - }, - subtitle: { - fontSize: 15, - color: '#5f6368', - textAlign: 'center', - }, - inputContainer: { - marginBottom: 24, - }, - label: { - fontSize: 13, - fontWeight: '600', - color: '#3c4043', - marginBottom: 6, - marginTop: 16, - }, + container: { flex: 1 }, + scrollContent: { flexGrow: 1, justifyContent: 'center' }, + content: { paddingHorizontal: 24, paddingVertical: 40 }, + headerContainer: { marginBottom: 36, alignItems: 'center' }, + appIcon: { width: 80, height: 80, marginBottom: 16, borderRadius: 16 }, + title: { fontSize: 28, fontWeight: '700', marginBottom: 8 }, + subtitle: { fontSize: 15, textAlign: 'center' }, + inputContainer: { marginBottom: 24 }, + label: { fontSize: 13, fontWeight: '600', marginBottom: 6, marginTop: 16 }, input: { - borderWidth: 1, - borderColor: '#dadce0', - borderRadius: 8, - paddingHorizontal: 14, - paddingVertical: 12, - fontSize: 15, - backgroundColor: '#fff', - color: '#202124', + borderWidth: 1, borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12, fontSize: 15, }, - registerButton: { - backgroundColor: '#1a73e8', - borderRadius: 8, - paddingVertical: 14, - alignItems: 'center', - marginBottom: 20, + primaryButton: { + borderRadius: 8, paddingVertical: 14, alignItems: 'center', marginBottom: 10, borderWidth: 1, }, - buttonDisabled: { - opacity: 0.6, - }, - buttonText: { - color: '#fff', - fontSize: 15, - fontWeight: '600', - }, - loginContainer: { - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - }, - loginText: { - color: '#5f6368', - fontSize: 14, - }, - loginLink: { - color: '#1a73e8', - fontSize: 14, - fontWeight: '600', + primaryButtonText: { color: '#fff', fontSize: 15, fontWeight: '600' }, + secondaryButton: { + borderRadius: 8, paddingVertical: 14, alignItems: 'center', borderWidth: 1, }, + secondaryButtonText: { fontSize: 15, fontWeight: '600' }, + buttonDisabled: { opacity: 0.6 }, }); export default RegisterScreen; diff --git a/src/screens/SearchScreen.js b/src/screens/SearchScreen.js index 3f92fee..28241b2 100644 --- a/src/screens/SearchScreen.js +++ b/src/screens/SearchScreen.js @@ -1,30 +1,25 @@ import React, { useState, useEffect } from 'react'; import { - StyleSheet, View, Text, TextInput, FlatList, TouchableOpacity, - Dimensions, Image, Modal, ActivityIndicator, + StyleSheet, View, Text, TextInput, TouchableOpacity, ActivityIndicator, } from 'react-native'; +import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; - -const { width } = Dimensions.get('window'); -const GRID_COLUMNS = 3; -const GRID_GAP = 2; -const imageSize = (width - GRID_GAP * (GRID_COLUMNS + 1)) / GRID_COLUMNS; +import PhotoGrid from '../components/PhotoGrid'; +import MediaViewer from '../components/MediaViewer'; const SearchScreen = ({ route }) => { const userId = route?.params?.userId; + const { theme } = useTheme(); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const [selectedMedia, setSelectedMedia] = useState(null); - const [modalVisible, setModalVisible] = useState(false); + const [viewerVisible, setViewerVisible] = useState(false); useEffect(() => { const timer = setTimeout(() => { - if (query.trim().length > 0) { - performSearch(); - } else { - setResults([]); - } + if (query.trim().length > 0) performSearch(); + else setResults([]); }, 300); return () => clearTimeout(timer); }, [query]); @@ -42,36 +37,14 @@ const SearchScreen = ({ route }) => { } }; - const getServiceIcon = (type) => ({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱'); - - const renderItem = ({ item }) => ( - { setSelectedMedia(item); setModalVisible(true); }} - activeOpacity={0.8} - > - {item.local_path ? ( - - ) : ( - - 📷 - - )} - - {getServiceIcon(item.source_service)} - - - ); - return ( - - {/* Search bar */} - + + 🔍 { /> {query.length > 0 && ( setQuery('')}> - ✕ + ✕ )} {searching && ( - + )} {!searching && query.length > 0 && results.length === 0 && ( 🔍 - No results - Try searching with different keywords + No results + Try different keywords )} {query.length === 0 && ( 🔍 - Search your photos - Search by title or description + Search your photos + Search by title or description )} {results.length > 0 && ( <> - {results.length} result{results.length !== 1 ? 's' : ''} - + {results.length} result{results.length !== 1 ? 's' : ''} + + item.id?.toString() || i.toString()} - numColumns={GRID_COLUMNS} - contentContainerStyle={styles.gridContent} + onItemPress={(item) => { setSelectedMedia(item); setViewerVisible(true); }} /> )} - {/* Photo viewer modal */} - setModalVisible(false)}> - - setModalVisible(false)}> - ✕ - - {selectedMedia?.local_path ? ( - - ) : ( - - 📷 - - )} - - {selectedMedia?.title || 'Photo'} - - - + setViewerVisible(false)} /> ); }; const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#fff' }, + container: { flex: 1 }, searchBar: { flexDirection: 'row', alignItems: 'center', marginHorizontal: 12, - marginTop: 48, marginBottom: 8, backgroundColor: '#f1f3f4', - borderRadius: 24, paddingHorizontal: 14, paddingVertical: 10, gap: 8, + marginTop: 48, marginBottom: 8, borderRadius: 24, + paddingHorizontal: 14, paddingVertical: 10, gap: 8, }, searchIcon: { fontSize: 16 }, - searchInput: { flex: 1, fontSize: 15, color: '#202124', padding: 0 }, - clearButton: { fontSize: 16, color: '#5f6368', padding: 4 }, + searchInput: { flex: 1, fontSize: 15, padding: 0 }, + clearButton: { fontSize: 16, padding: 4 }, loadingContainer: { padding: 20, alignItems: 'center' }, emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 24 }, emptyIcon: { fontSize: 48, marginBottom: 12 }, - emptyTitle: { fontSize: 18, fontWeight: '600', color: '#202124', marginBottom: 6 }, - emptyText: { fontSize: 13, color: '#5f6368', textAlign: 'center' }, - resultCount: { paddingHorizontal: 16, paddingVertical: 8, fontSize: 13, color: '#5f6368' }, - gridContent: { paddingBottom: 20 }, - gridItem: { - width: imageSize, height: imageSize, margin: GRID_GAP / 2, - backgroundColor: '#f1f3f4', position: 'relative', - }, - gridImage: { width: '100%', height: '100%' }, - placeholder: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', backgroundColor: '#e8eaed' }, - sourceBadge: { - position: 'absolute', bottom: 4, right: 4, backgroundColor: 'rgba(0,0,0,0.5)', - borderRadius: 8, paddingHorizontal: 4, paddingVertical: 2, - }, - modalContainer: { flex: 1, backgroundColor: '#000' }, - modalClose: { - position: 'absolute', top: 44, right: 16, zIndex: 20, width: 36, height: 36, - borderRadius: 18, backgroundColor: 'rgba(255,255,255,0.2)', justifyContent: 'center', alignItems: 'center', - }, - modalCloseText: { color: '#fff', fontSize: 20, fontWeight: '600' }, - fullImage: { flex: 1, width: '100%' }, - fullPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - mediaInfo: { backgroundColor: '#1a1a1a', padding: 16, paddingBottom: 32, borderTopLeftRadius: 16, borderTopRightRadius: 16 }, - mediaTitle: { fontSize: 16, fontWeight: '600', color: '#fff' }, + emptyTitle: { fontSize: 18, fontWeight: '600', marginBottom: 6 }, + emptyText: { fontSize: 13, textAlign: 'center' }, + resultCount: { paddingHorizontal: 16, paddingVertical: 8, fontSize: 13 }, }); export default SearchScreen; diff --git a/src/screens/SettingsScreen.js b/src/screens/SettingsScreen.js new file mode 100644 index 0000000..9d9789e --- /dev/null +++ b/src/screens/SettingsScreen.js @@ -0,0 +1,401 @@ +import React, { useState, useCallback } from 'react'; +import { + StyleSheet, View, Text, ScrollView, TouchableOpacity, Switch, TextInput, + Alert, ActivityIndicator, +} from 'react-native'; +import { useFocusEffect } from '@react-navigation/native'; +import { useTheme } from '../services/ThemeContext'; +import DatabaseService from '../services/DatabaseService'; +import NASSetupInstructions from '../components/NASSetupInstructions'; +import NASAUTH from '../AUTH/NASAUTH'; + +const SettingsScreen = ({ navigation, route }) => { + const userId = route?.params?.userId; + const { theme, themeName, setThemeName, themes } = useTheme(); + const [loading, setLoading] = useState(false); + const [accounts, setAccounts] = useState([]); + const [autoBackup, setAutoBackup] = useState([]); + const [nasOffload, setNasOffload] = useState(false); + + // NAS edit fields + const [editingNas, setEditingNas] = useState(false); + const [nasHost, setNasHost] = useState(''); + const [nasPort, setNasPort] = useState('22'); + const [nasUsername, setNasUsername] = useState(''); + const [nasPassword, setNasPassword] = useState(''); + const [nasPhotoFolder, setNasPhotoFolder] = useState('/home/user/photos'); + + useFocusEffect( + useCallback(() => { + if (userId) loadData(); + }, [userId]) + ); + + const loadData = async () => { + try { + const allAccounts = await DatabaseService.getAllLinkedAccounts(userId); + 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 || 22)); + setNasUsername(nasAcc.account_data.username || ''); + setNasPhotoFolder(nasAcc.account_data.photoFolder || '/home/user/photos'); + } + } catch (err) { + console.error('Error loading settings:', err); + } + }; + + const toggleService = async (accountId, currentEnabled) => { + try { + await DatabaseService.toggleAccountEnabled(accountId, !currentEnabled); + await loadData(); + } catch (err) { + Alert.alert('Error', 'Failed to update service'); + } + }; + + 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() || !nasPassword.trim()) { + Alert.alert('Error', 'Please fill in all NAS fields'); + return; + } + setLoading(true); + try { + const nasAuth = new NASAUTH(); + await nasAuth.authenticate( + nasHost.trim(), parseInt(nasPort) || 22, + nasUsername.trim(), nasPassword, nasPhotoFolder.trim() + ); + await DatabaseService.linkAccount(userId, 'NAS', { + host: nasHost.trim(), + port: parseInt(nasPort) || 22, + username: nasUsername.trim(), + photoFolder: nasPhotoFolder.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); + } + }; + + const handleLogout = () => { + Alert.alert('Logout', 'Are you sure you want to sign out?', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Sign Out', style: 'destructive', onPress: () => navigation.replace('Login') }, + ]); + }; + + const getServiceIcon = (type) => + ({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱'); + const getServiceName = (type) => + ({ GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud Photos', NAS: 'NAS Storage' }[type] || type); + + const themeOptions = [ + { 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' }, + ]; + + const hasNasConnected = accounts.some((a) => a.service_type === 'NAS' && a.is_enabled); + + return ( + + + navigation.goBack()}> + ← + + Settings + + + + + {/* Theme Section */} + APPEARANCE + + {themeOptions.map((opt) => ( + setThemeName(opt.key)} + > + {opt.icon} + + {opt.label} + {opt.desc} + + + + ))} + + + {/* Services Section */} + SERVICES + + {accounts.length === 0 ? ( + + + No services connected. Go to onboarding to add services. + + + ) : ( + accounts.map((acc) => ( + + {getServiceIcon(acc.service_type)} + + + {getServiceName(acc.service_type)} + + + {acc.is_enabled ? 'Enabled' : 'Disabled'} + + + {acc.service_type === 'NAS' && acc.is_enabled && ( + setEditingNas(!editingNas)} + > + Edit + + )} + 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 + + Password + + Photos Folder + + + {loading ? : Save Configuration} + + + )} + + {/* NAS Setup Instructions */} + + + {/* NAS Google Photos Offloading */} + {hasNasConnected && ( + <> + NAS OFFLOADING + + + 📷 + + Offload Google Photos to NAS + + Fetch Google Photos data through NAS server + + + + + + + )} + + {/* Auto Backup */} + AUTOMATIC BACKUP + + + 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) => { + const isConnected = accounts.some((a) => a.service_type === svc.type && a.is_enabled); + return ( + isConnected && toggleAutoBackup(svc.type)} + disabled={!isConnected} + activeOpacity={0.7} + > + + {autoBackup.includes(svc.type) && ✓} + + {svc.icon} + + {svc.name} + + {!isConnected && ( + Not connected + )} + + ); + })} + { + setAutoBackup([]); + DatabaseService.setAutoBackupServices(userId, []); + }} + activeOpacity={0.7} + > + + {autoBackup.length === 0 && ✓} + + + Don't automatically back up + + + + + {/* Sign Out */} + + Sign Out + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1 }, + header: { + flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', + 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 }, + cardSubtext: { fontSize: 12, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 4 }, + // Theme rows + themeRow: { + flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + themeIcon: { fontSize: 22, marginRight: 12 }, + themeInfo: { flex: 1 }, + themeName: { fontSize: 14, fontWeight: '600', marginBottom: 2 }, + themeDesc: { fontSize: 12 }, + radio: { + width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: '#dadce0', + }, + // Service rows + serviceRow: { + flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + serviceIcon: { fontSize: 22, marginRight: 12 }, + serviceInfo: { flex: 1 }, + serviceName: { fontSize: 14, fontWeight: '600', marginBottom: 2 }, + serviceStatus: { fontSize: 12 }, + editButton: { + borderWidth: 1, borderRadius: 6, paddingHorizontal: 10, paddingVertical: 4, marginRight: 10, + }, + editButtonText: { 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 + checkRow: { + flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + checkbox: { + width: 22, height: 22, borderRadius: 4, borderWidth: 2, + justifyContent: 'center', alignItems: 'center', marginRight: 12, + }, + checkmark: { color: '#fff', fontSize: 14, fontWeight: '700' }, + checkIcon: { fontSize: 18, marginRight: 8 }, + checkLabel: { fontSize: 14, fontWeight: '500', flex: 1 }, + notConnected: { fontSize: 11 }, + // Logout + logoutButton: { + borderWidth: 1.5, borderRadius: 8, paddingVertical: 14, + alignItems: 'center', marginTop: 24, + }, + logoutText: { fontSize: 15, fontWeight: '600' }, +}); + +export default SettingsScreen; + diff --git a/src/services/DatabaseService.js b/src/services/DatabaseService.js index 84cf176..a32aff3 100644 --- a/src/services/DatabaseService.js +++ b/src/services/DatabaseService.js @@ -63,6 +63,14 @@ const DatabaseService = { ) `); + // Migration: add columns if they don't exist + try { + await db.execAsync(`ALTER TABLE users ADD COLUMN auto_backup_services TEXT DEFAULT '[]'`); + } catch (e) { /* column already exists */ } + try { + await db.execAsync(`ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT 'light'`); + } catch (e) { /* column already exists */ } + console.log('Database initialized successfully'); return true; } catch (error) { @@ -299,6 +307,65 @@ const DatabaseService = { } }, + getAutoBackupServices: async (userId) => { + try { + await DatabaseService.ensureDb(); + const row = await db.getFirstAsync('SELECT auto_backup_services FROM users WHERE id = ?', [userId]); + if (!row?.auto_backup_services) return []; + return JSON.parse(row.auto_backup_services); + } catch (error) { + console.error('Error getting auto backup services:', error); + return []; + } + }, + + setAutoBackupServices: async (userId, services) => { + try { + await DatabaseService.ensureDb(); + await db.runAsync('UPDATE users SET auto_backup_services = ? WHERE id = ?', [JSON.stringify(services), userId]); + } catch (error) { + console.error('Error setting auto backup services:', error); + throw error; + } + }, + + toggleAccountEnabled: async (accountId, enabled) => { + try { + await DatabaseService.ensureDb(); + await db.runAsync('UPDATE linked_accounts SET is_enabled = ? WHERE id = ?', [enabled ? 1 : 0, accountId]); + } catch (error) { + console.error('Error toggling account:', error); + throw error; + } + }, + + removeLinkedAccount: async (accountId) => { + try { + await DatabaseService.ensureDb(); + await db.runAsync('DELETE FROM linked_accounts WHERE id = ?', [accountId]); + } catch (error) { + console.error('Error removing account:', error); + throw error; + } + }, + + getAllLinkedAccounts: async (userId) => { + try { + await DatabaseService.ensureDb(); + const rows = await db.getAllAsync( + 'SELECT * FROM linked_accounts WHERE user_id = ?', + [userId] + ); + return rows.map((row) => ({ + ...row, + account_data: CredentialEncryption.decrypt(row.account_data), + })); + } catch (error) { + console.error('Error getting all linked accounts:', error); + throw error; + } + }, + closeDatabase: async () => { try { if (db?.closeAsync) await db.closeAsync(); diff --git a/src/services/ThemeContext.js b/src/services/ThemeContext.js new file mode 100644 index 0000000..53419df --- /dev/null +++ b/src/services/ThemeContext.js @@ -0,0 +1,151 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import * as SecureStore from 'expo-secure-store'; + +const THEME_KEY = 'app_theme_preference'; + +const lightTheme = { + name: 'light', + background: '#ffffff', + surface: '#f8f9fa', + surfaceElevated: '#ffffff', + text: '#202124', + textSecondary: '#5f6368', + textTertiary: '#9aa0a6', + primary: '#1a73e8', + primaryLight: '#d2e3fc', + accent: '#34a853', + danger: '#d93025', + border: '#dadce0', + borderLight: '#e8eaed', + inputBg: '#ffffff', + chipBg: '#f1f3f4', + chipActiveBg: '#d2e3fc', + tabBar: '#ffffff', + tabBarBorder: '#e0e0e0', + tabActive: '#1a73e8', + tabInactive: '#5f6368', + card: '#f8f9fa', + cardActive: '#e8f0fe', + overlay: 'rgba(0,0,0,0.3)', + statusBarStyle: 'dark', + searchBg: '#f1f3f4', + profileBg: '#1a73e8', + modalBg: '#000000', + modalInfoBg: '#1a1a1a', + blur: false, +}; + +const darkTheme = { + name: 'dark', + background: '#121212', + surface: '#1e1e1e', + surfaceElevated: '#2d2d2d', + text: '#e8eaed', + textSecondary: '#9aa0a6', + textTertiary: '#5f6368', + primary: '#8ab4f8', + primaryLight: '#1f3a5f', + accent: '#81c995', + danger: '#f28b82', + border: '#3c4043', + borderLight: '#2d2d2d', + inputBg: '#2d2d2d', + chipBg: '#2d2d2d', + chipActiveBg: '#1f3a5f', + tabBar: '#1e1e1e', + tabBarBorder: '#3c4043', + tabActive: '#8ab4f8', + tabInactive: '#9aa0a6', + card: '#1e1e1e', + cardActive: '#1f3a5f', + overlay: 'rgba(0,0,0,0.6)', + statusBarStyle: 'light', + searchBg: '#2d2d2d', + profileBg: '#8ab4f8', + modalBg: '#000000', + modalInfoBg: '#1e1e1e', + blur: false, +}; + +const liquidGlassTheme = { + name: 'liquidGlass', + background: '#f0f0f3', + surface: 'rgba(255,255,255,0.45)', + surfaceElevated: 'rgba(255,255,255,0.7)', + text: '#1c1c1e', + textSecondary: '#636366', + textTertiary: '#aeaeb2', + primary: '#007aff', + primaryLight: 'rgba(0,122,255,0.12)', + accent: '#34c759', + danger: '#ff3b30', + border: 'rgba(0,0,0,0.08)', + borderLight: 'rgba(0,0,0,0.04)', + inputBg: 'rgba(255,255,255,0.6)', + chipBg: 'rgba(255,255,255,0.5)', + chipActiveBg: 'rgba(0,122,255,0.15)', + tabBar: 'rgba(249,249,249,0.8)', + tabBarBorder: 'rgba(0,0,0,0.08)', + tabActive: '#007aff', + tabInactive: '#8e8e93', + card: 'rgba(255,255,255,0.5)', + cardActive: 'rgba(0,122,255,0.12)', + overlay: 'rgba(0,0,0,0.25)', + statusBarStyle: 'dark', + searchBg: 'rgba(118,118,128,0.12)', + profileBg: '#007aff', + modalBg: 'rgba(0,0,0,0.9)', + modalInfoBg: 'rgba(30,30,30,0.9)', + blur: true, + blurIntensity: 60, + blurTint: 'light', +}; + +const themes = { light: lightTheme, dark: darkTheme, liquidGlass: liquidGlassTheme }; + +const ThemeContext = createContext({ + theme: lightTheme, + themeName: 'light', + setThemeName: () => {}, + themes, +}); + +export const ThemeProvider = ({ children }) => { + const [themeName, setThemeNameState] = useState('light'); + + useEffect(() => { + loadTheme(); + }, []); + + const loadTheme = async () => { + try { + const saved = await SecureStore.getItemAsync(THEME_KEY); + if (saved && themes[saved]) { + setThemeNameState(saved); + } + } catch (err) { + console.log('Could not load theme preference:', err.message); + } + }; + + const setThemeName = async (name) => { + if (!themes[name]) return; + setThemeNameState(name); + try { + await SecureStore.setItemAsync(THEME_KEY, name); + } catch (err) { + console.log('Could not save theme preference:', err.message); + } + }; + + return ( + + {children} + + ); +}; + +export const useTheme = () => useContext(ThemeContext); + +export default ThemeContext; +