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;
+