diff --git a/App.js b/App.js index 72014d1..4ce0669 100644 --- a/App.js +++ b/App.js @@ -3,13 +3,13 @@ import { StatusBar } from 'expo-status-bar'; 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 { registerRootComponent } from 'expo'; import * as WebBrowser from 'expo-web-browser'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ThemeProvider, useTheme } from './src/services/ThemeContext'; import DatabaseService from './src/services/DatabaseService'; import { getStoredUserId } from './src/services/AuthSession'; +import CenteredState from './src/components/CenteredState'; import LoginScreen from './src/screens/LoginScreen'; import RegisterScreen from './src/screens/RegisterScreen'; import OnboardingScreen from './src/screens/OnboardingScreen'; @@ -32,6 +32,7 @@ function MainTabs({ route }) { return ( - - - ); + return ; } if (error) { return ( - - {error} - - Retry - - + ); } @@ -129,6 +124,7 @@ function AppInner() { <> @@ -143,7 +139,7 @@ function AppInner() { - + ); } @@ -158,4 +154,3 @@ function App() { registerRootComponent(App); -export default App; diff --git a/package.json b/package.json index 6b01935..c19cfc3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "expo-blur": "~55.0.14", "expo-crypto": "~55.0.14", "expo-file-system": "~55.0.16", - "expo-image-picker": "~55.0.18", "expo-linking": "~55.0.13", "expo-media-library": "~55.0.14", "expo-secure-store": "~55.0.13", @@ -28,7 +27,6 @@ "expo-status-bar": "~55.0.5", "expo-system-ui": "^55.0.15", "expo-web-browser": "~55.0.14", - "metro-react-native-babel-transformer": "^0.77.0", "react": "19.2.0", "react-native": "0.83.4", "react-native-gesture-handler": "~2.30.0", diff --git a/src/components/PhotoGrid.js b/src/components/PhotoGrid.js index 1ca0f0c..c19787a 100644 --- a/src/components/PhotoGrid.js +++ b/src/components/PhotoGrid.js @@ -4,22 +4,13 @@ import { } from 'react-native'; import { useTheme } from '../services/ThemeContext'; import MediaMetadataService from '../services/MediaMetadataService'; +import ServiceGlyph from './ServiceGlyph'; import CameraIcon from '../../assets/icons/camera.svg'; import CheckIcon from '../../assets/icons/check.svg'; -import DeviceIcon from '../../assets/icons/device.svg'; -import GooglePhotosIcon from '../../assets/icons/google-photos.svg'; -import ICloudIcon from '../../assets/icons/icloud.svg'; -import NasIcon from '../../assets/icons/nas.svg'; const { width } = Dimensions.get('window'); const GRID_GAP = 2; -const ServiceGlyph = ({ serviceType, size }) => { - if (serviceType === 'GOOGLE_PHOTOS') return ; - if (serviceType === 'ICLOUD_PHOTOS') return ; - if (serviceType === 'NAS') return ; - return ; -}; const PhotoGrid = ({ data, diff --git a/src/screens/AlbumsScreen.js b/src/screens/AlbumsScreen.js index 4c4abbb..97bf396 100644 --- a/src/screens/AlbumsScreen.js +++ b/src/screens/AlbumsScreen.js @@ -1,21 +1,19 @@ import React, { useState, useCallback } from 'react'; import { StyleSheet, View, Text, SectionList, FlatList, TouchableOpacity, - Dimensions, Image, ActivityIndicator, RefreshControl, + Dimensions, Image, RefreshControl, } from 'react-native'; import { useFocusEffect } from '@react-navigation/native'; import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; import MediaMetadataService from '../services/MediaMetadataService'; +import CenteredState from '../components/CenteredState'; import MediaViewer from '../components/MediaViewer'; +import ServiceGlyph from '../components/ServiceGlyph'; import CameraIcon from '../../assets/icons/camera.svg'; import EmptyPhotoIcon from '../../assets/icons/empty-photo.svg'; import FolderIcon from '../../assets/icons/folder.svg'; import FolderOpenIcon from '../../assets/icons/folder-open.svg'; -import DeviceIcon from '../../assets/icons/device.svg'; -import GooglePhotosIcon from '../../assets/icons/google-photos.svg'; -import ICloudIcon from '../../assets/icons/icloud.svg'; -import NasIcon from '../../assets/icons/nas.svg'; import HiddenIcon from '../../assets/icons/hidden.svg'; import DuplicatesIcon from '../../assets/icons/duplicates.svg'; import TrashIcon from '../../assets/icons/trash.svg'; @@ -26,12 +24,6 @@ const ALBUM_COLUMNS = 2; const ALBUM_GAP = 12; const albumSize = (width - ALBUM_GAP * (ALBUM_COLUMNS + 1)) / ALBUM_COLUMNS; const SERVICE_NAMES = { DEVICE: 'On Device', GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud Photos', NAS: 'NAS Storage' }; -const ServiceGlyph = ({ serviceType, size = 20 }) => { - if (serviceType === 'GOOGLE_PHOTOS') return ; - if (serviceType === 'ICLOUD_PHOTOS') return ; - if (serviceType === 'NAS') return ; - return ; -}; const UtilityGlyph = ({ name, size = 20, color = '#8B949E' }) => { if (name === 'hidden') return ; @@ -145,10 +137,10 @@ const CollectionsScreen = ({ route }) => { {albumMedia.length === 0 ? ( - - - No items in this collection - + } + title="No items in this collection" + /> ) : ( { if (loading) { return ( - - - Loading collections... - + ); } @@ -243,13 +232,11 @@ const CollectionsScreen = ({ route }) => { {sections.length === 0 || !sections.some((sec) => sec.data?.[0]?.albums?.some((a) => (a.count || 0) > 0)) ? ( - - - No Collections - - Collections from your connected services will appear here - - + } + title="No Collections" + description="Collections from your connected services will appear here" + /> ) : ( { const styles = StyleSheet.create({ container: { flex: 1 }, - centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 24 }, header: { paddingHorizontal: 16, paddingTop: 52, paddingBottom: 12 }, headerTitle: { fontSize: 24, fontWeight: '700' }, listContent: { paddingHorizontal: ALBUM_GAP, paddingBottom: 20 }, @@ -299,9 +285,6 @@ const styles = StyleSheet.create({ gridItem: { margin: 1 }, gridImage: { width: '100%', height: '100%' }, gridPlaceholder: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center' }, - loadingText: { marginTop: 12, fontSize: 14 }, - emptyTitle: { fontSize: 18, fontWeight: '600', marginBottom: 6 }, - emptyText: { fontSize: 13, textAlign: 'center' }, }); export default CollectionsScreen; diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index d1d4433..ebfaf9c 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -9,6 +9,9 @@ import DeviceMediaService from '../services/DeviceMediaService'; import GoogleAUTH from '../AUTH/GoogleAUTH'; import NASAUTH from '../AUTH/NASAUTH'; import { File } from 'expo-file-system'; +import CenteredState from '../components/CenteredState'; +import MediaSortControls from '../components/MediaSortControls'; +import ServiceGlyph from '../components/ServiceGlyph'; import TopBar from '../components/TopBar'; import PhotoGrid from '../components/PhotoGrid'; import MediaViewer from '../components/MediaViewer'; @@ -18,18 +21,8 @@ import MediaMetadataService from '../services/MediaMetadataService'; import { logError } from '../services/ErrorHandler'; import CameraIcon from '../../assets/icons/camera.svg'; import EmptyPhotoIcon from '../../assets/icons/empty-photo.svg'; -import DeviceIcon from '../../assets/icons/device.svg'; -import GooglePhotosIcon from '../../assets/icons/google-photos.svg'; -import ICloudIcon from '../../assets/icons/icloud.svg'; -import NasIcon from '../../assets/icons/nas.svg'; const SERVICE_NAMES = { DEVICE: 'On Device', GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' }; -const ServiceGlyph = ({ serviceType, size = 14 }) => { - if (serviceType === 'GOOGLE_PHOTOS') return ; - if (serviceType === 'ICLOUD_PHOTOS') return ; - if (serviceType === 'NAS') return ; - return ; -}; /** Keeps device library timestamps and aligns `created_date` with resolved capture time after merges. */ const mergeCaptureFields = (target, donor) => { @@ -270,10 +263,10 @@ const HomeScreen = ({ navigation, route }) => { } }; - const refreshMedia = useCallback(() => { + const refreshMedia = () => { setRefreshing(true); loadAllMedia(); - }, [loadAllMedia]); + }; const importGooglePhotos = async () => { const googleAccount = linkedAccounts.find((a) => a.service_type === 'GOOGLE_PHOTOS'); @@ -509,10 +502,7 @@ const HomeScreen = ({ navigation, route }) => { if (loading && allMedia.length === 0) { return ( - - - Loading photos... - + ); } @@ -556,6 +546,7 @@ const HomeScreen = ({ navigation, route }) => { onMenuPress={() => navigation.navigate('Settings', { userId })} /> + { )} {filteredMedia.length === 0 ? ( - - - - - No Photos Yet - - {hasGoogleAccount - ? 'Tap "Import from Google Photos" above to select cloud photos,\nor grant media library permission to see on-device photos.' - : 'Grant media library permission or take some photos to get started'} - - - Refresh - - + } + title="No Photos Yet" + description={hasGoogleAccount + ? 'Tap "Import from Google Photos" above to select cloud photos, or grant media library permission to see on-device photos.' + : 'Grant media library permission or take some photos to get started'} + actionLabel="Refresh" + onAction={refreshMedia} + containerStyle={{ backgroundColor: theme.background }} + /> ) : ( { const styles = StyleSheet.create({ container: { flex: 1 }, - centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 24 }, - loadingText: { marginTop: 12, fontSize: 14 }, filterContainer: { maxHeight: 40 }, filterRow: { paddingHorizontal: 12, paddingVertical: 6, gap: 6, alignItems: 'center' }, chip: { @@ -660,10 +642,6 @@ const styles = StyleSheet.create({ borderRadius: 12, gap: 8, minHeight: 42, }, importText: { color: '#fff', fontSize: 14, fontWeight: '600' }, - emptyTitle: { fontSize: 18, fontWeight: '600', marginBottom: 6 }, - emptyText: { fontSize: 13, textAlign: 'center', marginBottom: 20, lineHeight: 20 }, - retryButton: { paddingHorizontal: 24, paddingVertical: 10, borderRadius: 20 }, - retryButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' }, }); export default HomeScreen; diff --git a/src/screens/OnboardingScreen.js b/src/screens/OnboardingScreen.js index 3b8315c..180c34b 100644 --- a/src/screens/OnboardingScreen.js +++ b/src/screens/OnboardingScreen.js @@ -9,11 +9,8 @@ import GoogleAUTH from '../AUTH/GoogleAUTH'; import NASAUTH from '../AUTH/NASAUTH'; import NASSetupInstructions from '../components/NASSetupInstructions'; import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; +import ServiceGlyph from '../components/ServiceGlyph'; import CheckIcon from '../../assets/icons/check.svg'; -import DeviceIcon from '../../assets/icons/device.svg'; -import GooglePhotosIcon from '../../assets/icons/google-photos.svg'; -import ICloudIcon from '../../assets/icons/icloud.svg'; -import NasIcon from '../../assets/icons/nas.svg'; const SERVICES = { GOOGLE_PHOTOS: { name: 'Google Photos', type: 'GOOGLE_PHOTOS' }, @@ -25,12 +22,6 @@ const ONBOARDING_STEPS = [ { title: 'Connect Services', description: 'Set up each selected service' }, { title: 'Automatic Backup', description: 'Choose where new photos are backed up' }, ]; -const ServiceGlyph = ({ serviceType, size = 20 }) => { - if (serviceType === 'GOOGLE_PHOTOS') return ; - if (serviceType === 'ICLOUD_PHOTOS') return ; - if (serviceType === 'NAS') return ; - return ; -}; const OnboardingScreen = ({ navigation, route }) => { const userId = route?.params?.userId; diff --git a/src/screens/SearchScreen.js b/src/screens/SearchScreen.js index 552ad5e..9778801 100644 --- a/src/screens/SearchScreen.js +++ b/src/screens/SearchScreen.js @@ -4,6 +4,7 @@ import { } from 'react-native'; import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; +import CenteredState from '../components/CenteredState'; import PhotoGrid from '../components/PhotoGrid'; import MediaViewer from '../components/MediaViewer'; import SearchIcon from '../../assets/icons/search.svg'; @@ -66,21 +67,19 @@ const SearchScreen = ({ route }) => { )} {!searching && query.length > 0 && results.length === 0 && ( - - - No results - Try different keywords - + } + title="No results" + description="Try different keywords" + /> )} {query.length === 0 && ( - - - Search your photos - - Search by filename and indexed metadata - - + } + title="Search your photos" + description="Search by filename and indexed metadata" + /> )} {results.length > 0 && ( @@ -110,9 +109,6 @@ const styles = StyleSheet.create({ 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 }, - emptyTitle: { fontSize: 18, fontWeight: '600', marginBottom: 6 }, - emptyText: { fontSize: 13, textAlign: 'center' }, resultCount: { paddingHorizontal: 16, paddingVertical: 8, fontSize: 13 }, }); diff --git a/src/screens/SettingsScreen.js b/src/screens/SettingsScreen.js index 3664538..bcaa99d 100644 --- a/src/screens/SettingsScreen.js +++ b/src/screens/SettingsScreen.js @@ -8,14 +8,11 @@ import { useTheme } from '../services/ThemeContext'; import DatabaseService from '../services/DatabaseService'; import NASSetupInstructions from '../components/NASSetupInstructions'; import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; +import ServiceGlyph from '../components/ServiceGlyph'; import NASAUTH from '../AUTH/NASAUTH'; import GoogleAUTH from '../AUTH/GoogleAUTH'; import { clearStoredUserId } from '../services/AuthSession'; import CheckIcon from '../../assets/icons/check.svg'; -import DeviceIcon from '../../assets/icons/device.svg'; -import GooglePhotosIcon from '../../assets/icons/google-photos.svg'; -import ICloudIcon from '../../assets/icons/icloud.svg'; -import NasIcon from '../../assets/icons/nas.svg'; const SERVICE_NAMES = { GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud Photos', NAS: 'NAS Storage' }; const AUTO_BACKUP_SERVICES = [ @@ -23,12 +20,6 @@ const AUTO_BACKUP_SERVICES = [ { type: 'ICLOUD_PHOTOS', name: 'iCloud Photos' }, { type: 'NAS', name: 'NAS Storage' }, ]; -const ServiceGlyph = ({ serviceType, size = 20 }) => { - if (serviceType === 'GOOGLE_PHOTOS') return ; - if (serviceType === 'ICLOUD_PHOTOS') return ; - if (serviceType === 'NAS') return ; - return ; -}; const SettingsScreen = ({ navigation, route }) => { const userId = route?.params?.userId; diff --git a/src/services/CredentialEncryption.js b/src/services/CredentialEncryption.js index 375e43e..4f9d37e 100644 --- a/src/services/CredentialEncryption.js +++ b/src/services/CredentialEncryption.js @@ -1,13 +1,14 @@ -import * as Crypto from 'expo-crypto'; +import { requireNativeModule } from 'expo-modules-core'; + +const ExpoCrypto = requireNativeModule('ExpoCrypto'); +const SHA_256 = 'SHA-256'; +const HEX_ENCODING = 'hex'; class CredentialEncryption { async hashPassword(password) { try { if (!password) throw new Error('Password is required'); - return await Crypto.digestStringAsync( - Crypto.CryptoDigestAlgorithm.SHA256, - password - ); + return await ExpoCrypto.digestStringAsync(SHA_256, password, { encoding: HEX_ENCODING }); } catch (error) { console.error('Password hashing error:', error); throw new Error('Failed to hash password: ' + error.message);