feat: refactor service glyphs and improve loading states; replace inline components with CenteredState and ServiceGlyph

This commit is contained in:
bobbert committed 2026-04-15 17:14:19 +01:00
1 parent bb1eb375ad
commit 126f545c44
9 files changed
+61 -137

No files matched your search

+12 -17
View File
@@ -3,13 +3,13 @@ import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { ActivityIndicator, View, Text, TouchableOpacity } from 'react-native';
import { registerRootComponent } from 'expo'; import { registerRootComponent } from 'expo';
import * as WebBrowser from 'expo-web-browser'; import * as WebBrowser from 'expo-web-browser';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ThemeProvider, useTheme } from './src/services/ThemeContext'; import { ThemeProvider, useTheme } from './src/services/ThemeContext';
import DatabaseService from './src/services/DatabaseService'; import DatabaseService from './src/services/DatabaseService';
import { getStoredUserId } from './src/services/AuthSession'; import { getStoredUserId } from './src/services/AuthSession';
import CenteredState from './src/components/CenteredState';
import LoginScreen from './src/screens/LoginScreen'; import LoginScreen from './src/screens/LoginScreen';
import RegisterScreen from './src/screens/RegisterScreen'; import RegisterScreen from './src/screens/RegisterScreen';
import OnboardingScreen from './src/screens/OnboardingScreen'; import OnboardingScreen from './src/screens/OnboardingScreen';
@@ -32,6 +32,7 @@ function MainTabs({ route }) {
return ( return (
<Tab.Navigator <Tab.Navigator
id="MainTabsNavigator"
screenOptions={{ screenOptions={{
headerShown: false, headerShown: false,
tabBarActiveTintColor: theme.tabActive, tabBarActiveTintColor: theme.tabActive,
@@ -104,24 +105,18 @@ function AppInner() {
}; };
if ((!isReady || !bootChecked) && !error) { if ((!isReady || !bootChecked) && !error) {
return ( return <CenteredState loading title="Loading app…" containerStyle={{ backgroundColor: theme.background }} />;
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: theme.background }}>
<ActivityIndicator size="large" color={theme.primary} />
</View>
);
} }
if (error) { if (error) {
return ( return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 20, backgroundColor: theme.background }}> <CenteredState
<Text style={{ fontSize: 16, color: theme.text, marginBottom: 12, textAlign: 'center' }}>{error}</Text> title="Unable to start app"
<TouchableOpacity description={error}
onPress={initializeApp} actionLabel="Retry"
style={{ backgroundColor: theme.primary, paddingHorizontal: 16, paddingVertical: 10, borderRadius: 6 }} onAction={initializeApp}
> containerStyle={{ backgroundColor: theme.background }}
<Text style={{ color: '#fff', fontWeight: '600' }}>Retry</Text> />
</TouchableOpacity>
</View>
); );
} }
@@ -129,6 +124,7 @@ function AppInner() {
<> <>
<NavigationContainer> <NavigationContainer>
<Stack.Navigator <Stack.Navigator
id="RootStackNavigator"
screenOptions={{ headerShown: false }} screenOptions={{ headerShown: false }}
initialRouteName={bootUserId ? 'MainTabs' : 'Login'} initialRouteName={bootUserId ? 'MainTabs' : 'Login'}
> >
@@ -143,7 +139,7 @@ function AppInner() {
<Stack.Screen name="Settings" component={SettingsScreen} /> <Stack.Screen name="Settings" component={SettingsScreen} />
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
<StatusBar style={theme.statusBarStyle} backgroundColor={theme.background} /> <StatusBar style={theme.statusBarStyle} />
</> </>
); );
} }
@@ -158,4 +154,3 @@ function App() {
registerRootComponent(App); registerRootComponent(App);
export default App;
-2
View File
@@ -20,7 +20,6 @@
"expo-blur": "~55.0.14", "expo-blur": "~55.0.14",
"expo-crypto": "~55.0.14", "expo-crypto": "~55.0.14",
"expo-file-system": "~55.0.16", "expo-file-system": "~55.0.16",
"expo-image-picker": "~55.0.18",
"expo-linking": "~55.0.13", "expo-linking": "~55.0.13",
"expo-media-library": "~55.0.14", "expo-media-library": "~55.0.14",
"expo-secure-store": "~55.0.13", "expo-secure-store": "~55.0.13",
@@ -28,7 +27,6 @@
"expo-status-bar": "~55.0.5", "expo-status-bar": "~55.0.5",
"expo-system-ui": "^55.0.15", "expo-system-ui": "^55.0.15",
"expo-web-browser": "~55.0.14", "expo-web-browser": "~55.0.14",
"metro-react-native-babel-transformer": "^0.77.0",
"react": "19.2.0", "react": "19.2.0",
"react-native": "0.83.4", "react-native": "0.83.4",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.30.0",
+1 -10
View File
@@ -4,22 +4,13 @@ import {
} from 'react-native'; } from 'react-native';
import { useTheme } from '../services/ThemeContext'; import { useTheme } from '../services/ThemeContext';
import MediaMetadataService from '../services/MediaMetadataService'; import MediaMetadataService from '../services/MediaMetadataService';
import ServiceGlyph from './ServiceGlyph';
import CameraIcon from '../../assets/icons/camera.svg'; import CameraIcon from '../../assets/icons/camera.svg';
import CheckIcon from '../../assets/icons/check.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 { width } = Dimensions.get('window');
const GRID_GAP = 2; const GRID_GAP = 2;
const ServiceGlyph = ({ serviceType, size }) => {
if (serviceType === 'GOOGLE_PHOTOS') return <GooglePhotosIcon width={size} height={size} />;
if (serviceType === 'ICLOUD_PHOTOS') return <ICloudIcon width={size} height={size} />;
if (serviceType === 'NAS') return <NasIcon width={size} height={size} />;
return <DeviceIcon width={size} height={size} />;
};
const PhotoGrid = ({ const PhotoGrid = ({
data, data,
+13 -30
View File
@@ -1,21 +1,19 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { import {
StyleSheet, View, Text, SectionList, FlatList, TouchableOpacity, StyleSheet, View, Text, SectionList, FlatList, TouchableOpacity,
Dimensions, Image, ActivityIndicator, RefreshControl, Dimensions, Image, RefreshControl,
} from 'react-native'; } from 'react-native';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import { useTheme } from '../services/ThemeContext'; import { useTheme } from '../services/ThemeContext';
import DatabaseService from '../services/DatabaseService'; import DatabaseService from '../services/DatabaseService';
import MediaMetadataService from '../services/MediaMetadataService'; import MediaMetadataService from '../services/MediaMetadataService';
import CenteredState from '../components/CenteredState';
import MediaViewer from '../components/MediaViewer'; import MediaViewer from '../components/MediaViewer';
import ServiceGlyph from '../components/ServiceGlyph';
import CameraIcon from '../../assets/icons/camera.svg'; import CameraIcon from '../../assets/icons/camera.svg';
import EmptyPhotoIcon from '../../assets/icons/empty-photo.svg'; import EmptyPhotoIcon from '../../assets/icons/empty-photo.svg';
import FolderIcon from '../../assets/icons/folder.svg'; import FolderIcon from '../../assets/icons/folder.svg';
import FolderOpenIcon from '../../assets/icons/folder-open.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 HiddenIcon from '../../assets/icons/hidden.svg';
import DuplicatesIcon from '../../assets/icons/duplicates.svg'; import DuplicatesIcon from '../../assets/icons/duplicates.svg';
import TrashIcon from '../../assets/icons/trash.svg'; import TrashIcon from '../../assets/icons/trash.svg';
@@ -26,12 +24,6 @@ const ALBUM_COLUMNS = 2;
const ALBUM_GAP = 12; const ALBUM_GAP = 12;
const albumSize = (width - ALBUM_GAP * (ALBUM_COLUMNS + 1)) / ALBUM_COLUMNS; 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 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 <GooglePhotosIcon width={size} height={size} />;
if (serviceType === 'ICLOUD_PHOTOS') return <ICloudIcon width={size} height={size} />;
if (serviceType === 'NAS') return <NasIcon width={size} height={size} />;
return <DeviceIcon width={size} height={size} />;
};
const UtilityGlyph = ({ name, size = 20, color = '#8B949E' }) => { const UtilityGlyph = ({ name, size = 20, color = '#8B949E' }) => {
if (name === 'hidden') return <HiddenIcon width={size} height={size} color={color} />; if (name === 'hidden') return <HiddenIcon width={size} height={size} color={color} />;
@@ -145,10 +137,10 @@ const CollectionsScreen = ({ route }) => {
</View> </View>
</View> </View>
{albumMedia.length === 0 ? ( {albumMedia.length === 0 ? (
<View style={styles.centerContainer}> <CenteredState
<UtilityGlyph name="emptyMail" size={48} /> icon={<UtilityGlyph name="emptyMail" size={48} />}
<Text style={[styles.emptyTitle, { color: theme.text }]}>No items in this collection</Text> title="No items in this collection"
</View> />
) : ( ) : (
<FlatList <FlatList
data={albumMedia} data={albumMedia}
@@ -179,10 +171,7 @@ const CollectionsScreen = ({ route }) => {
if (loading) { if (loading) {
return ( return (
<View style={[styles.centerContainer, { backgroundColor: theme.background }]}> <CenteredState loading title="Loading collections…" containerStyle={{ backgroundColor: theme.background }} />
<ActivityIndicator size="large" color={theme.primary} />
<Text style={[styles.loadingText, { color: theme.textSecondary }]}>Loading collections...</Text>
</View>
); );
} }
@@ -243,13 +232,11 @@ const CollectionsScreen = ({ route }) => {
{sections.length === 0 {sections.length === 0
|| !sections.some((sec) => sec.data?.[0]?.albums?.some((a) => (a.count || 0) > 0)) ? ( || !sections.some((sec) => sec.data?.[0]?.albums?.some((a) => (a.count || 0) > 0)) ? (
<View style={styles.centerContainer}> <CenteredState
<FolderIcon width={48} height={48} color="#8B949E" /> icon={<FolderIcon width={48} height={48} color="#8B949E" />}
<Text style={[styles.emptyTitle, { color: theme.text }]}>No Collections</Text> title="No Collections"
<Text style={[styles.emptyText, { color: theme.textSecondary }]}> description="Collections from your connected services will appear here"
Collections from your connected services will appear here />
</Text>
</View>
) : ( ) : (
<SectionList <SectionList
sections={sections} sections={sections}
@@ -269,7 +256,6 @@ const CollectionsScreen = ({ route }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1 },
centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 24 },
header: { paddingHorizontal: 16, paddingTop: 52, paddingBottom: 12 }, header: { paddingHorizontal: 16, paddingTop: 52, paddingBottom: 12 },
headerTitle: { fontSize: 24, fontWeight: '700' }, headerTitle: { fontSize: 24, fontWeight: '700' },
listContent: { paddingHorizontal: ALBUM_GAP, paddingBottom: 20 }, listContent: { paddingHorizontal: ALBUM_GAP, paddingBottom: 20 },
@@ -299,9 +285,6 @@ const styles = StyleSheet.create({
gridItem: { margin: 1 }, gridItem: { margin: 1 },
gridImage: { width: '100%', height: '100%' }, gridImage: { width: '100%', height: '100%' },
gridPlaceholder: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center' }, 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; export default CollectionsScreen;
+16 -38
View File
@@ -9,6 +9,9 @@ import DeviceMediaService from '../services/DeviceMediaService';
import GoogleAUTH from '../AUTH/GoogleAUTH'; import GoogleAUTH from '../AUTH/GoogleAUTH';
import NASAUTH from '../AUTH/NASAUTH'; import NASAUTH from '../AUTH/NASAUTH';
import { File } from 'expo-file-system'; 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 TopBar from '../components/TopBar';
import PhotoGrid from '../components/PhotoGrid'; import PhotoGrid from '../components/PhotoGrid';
import MediaViewer from '../components/MediaViewer'; import MediaViewer from '../components/MediaViewer';
@@ -18,18 +21,8 @@ import MediaMetadataService from '../services/MediaMetadataService';
import { logError } from '../services/ErrorHandler'; import { logError } from '../services/ErrorHandler';
import CameraIcon from '../../assets/icons/camera.svg'; import CameraIcon from '../../assets/icons/camera.svg';
import EmptyPhotoIcon from '../../assets/icons/empty-photo.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 SERVICE_NAMES = { DEVICE: 'On Device', GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' };
const ServiceGlyph = ({ serviceType, size = 14 }) => {
if (serviceType === 'GOOGLE_PHOTOS') return <GooglePhotosIcon width={size} height={size} />;
if (serviceType === 'ICLOUD_PHOTOS') return <ICloudIcon width={size} height={size} />;
if (serviceType === 'NAS') return <NasIcon width={size} height={size} />;
return <DeviceIcon width={size} height={size} />;
};
/** Keeps device library timestamps and aligns `created_date` with resolved capture time after merges. */ /** Keeps device library timestamps and aligns `created_date` with resolved capture time after merges. */
const mergeCaptureFields = (target, donor) => { const mergeCaptureFields = (target, donor) => {
@@ -270,10 +263,10 @@ const HomeScreen = ({ navigation, route }) => {
} }
}; };
const refreshMedia = useCallback(() => { const refreshMedia = () => {
setRefreshing(true); setRefreshing(true);
loadAllMedia(); loadAllMedia();
}, [loadAllMedia]); };
const importGooglePhotos = async () => { const importGooglePhotos = async () => {
const googleAccount = linkedAccounts.find((a) => a.service_type === 'GOOGLE_PHOTOS'); const googleAccount = linkedAccounts.find((a) => a.service_type === 'GOOGLE_PHOTOS');
@@ -509,10 +502,7 @@ const HomeScreen = ({ navigation, route }) => {
if (loading && allMedia.length === 0) { if (loading && allMedia.length === 0) {
return ( return (
<View style={[styles.centerContainer, { backgroundColor: theme.background }]}> <CenteredState loading title="Loading photos…" containerStyle={{ backgroundColor: theme.background }} />
<ActivityIndicator size="large" color={theme.primary} />
<Text style={[styles.loadingText, { color: theme.textSecondary }]}>Loading photos...</Text>
</View>
); );
} }
@@ -556,6 +546,7 @@ const HomeScreen = ({ navigation, route }) => {
onMenuPress={() => navigation.navigate('Settings', { userId })} onMenuPress={() => navigation.navigate('Settings', { userId })}
/> />
<MediaSortControls value={sortMode} onChange={setSortMode} />
<FilterChips /> <FilterChips />
<SelectionToolbar <SelectionToolbar
@@ -584,23 +575,16 @@ const HomeScreen = ({ navigation, route }) => {
)} )}
{filteredMedia.length === 0 ? ( {filteredMedia.length === 0 ? (
<View style={styles.centerContainer}> <CenteredState
<View style={{ marginBottom: 16 }}> icon={<EmptyPhotoIcon width={56} height={56} color="#9CA3AF" />}
<EmptyPhotoIcon width={56} height={56} color="#9CA3AF" /> title="No Photos Yet"
</View> description={hasGoogleAccount
<Text style={[styles.emptyTitle, { color: theme.text }]}>No Photos Yet</Text> ? 'Tap "Import from Google Photos" above to select cloud photos, or grant media library permission to see on-device photos.'
<Text style={[styles.emptyText, { color: theme.textSecondary }]}>
{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'} : 'Grant media library permission or take some photos to get started'}
</Text> actionLabel="Refresh"
<TouchableOpacity onAction={refreshMedia}
style={[styles.retryButton, { backgroundColor: theme.primary }]} containerStyle={{ backgroundColor: theme.background }}
onPress={refreshMedia} />
>
<Text style={styles.retryButtonText}>Refresh</Text>
</TouchableOpacity>
</View>
) : ( ) : (
<PhotoGrid <PhotoGrid
data={sortMode === 'date_taken' ? [] : filteredMedia} data={sortMode === 'date_taken' ? [] : filteredMedia}
@@ -645,8 +629,6 @@ const HomeScreen = ({ navigation, route }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1 },
centerContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 24 },
loadingText: { marginTop: 12, fontSize: 14 },
filterContainer: { maxHeight: 40 }, filterContainer: { maxHeight: 40 },
filterRow: { paddingHorizontal: 12, paddingVertical: 6, gap: 6, alignItems: 'center' }, filterRow: { paddingHorizontal: 12, paddingVertical: 6, gap: 6, alignItems: 'center' },
chip: { chip: {
@@ -660,10 +642,6 @@ const styles = StyleSheet.create({
borderRadius: 12, gap: 8, minHeight: 42, borderRadius: 12, gap: 8, minHeight: 42,
}, },
importText: { color: '#fff', fontSize: 14, fontWeight: '600' }, 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; export default HomeScreen;
+1 -10
View File
@@ -9,11 +9,8 @@ import GoogleAUTH from '../AUTH/GoogleAUTH';
import NASAUTH from '../AUTH/NASAUTH'; import NASAUTH from '../AUTH/NASAUTH';
import NASSetupInstructions from '../components/NASSetupInstructions'; import NASSetupInstructions from '../components/NASSetupInstructions';
import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; import ServiceHealthIndicator from '../components/ServiceHealthIndicator';
import ServiceGlyph from '../components/ServiceGlyph';
import CheckIcon from '../../assets/icons/check.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 SERVICES = { const SERVICES = {
GOOGLE_PHOTOS: { name: 'Google Photos', type: 'GOOGLE_PHOTOS' }, 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: 'Connect Services', description: 'Set up each selected service' },
{ title: 'Automatic Backup', description: 'Choose where new photos are backed up' }, { title: 'Automatic Backup', description: 'Choose where new photos are backed up' },
]; ];
const ServiceGlyph = ({ serviceType, size = 20 }) => {
if (serviceType === 'GOOGLE_PHOTOS') return <GooglePhotosIcon width={size} height={size} />;
if (serviceType === 'ICLOUD_PHOTOS') return <ICloudIcon width={size} height={size} />;
if (serviceType === 'NAS') return <NasIcon width={size} height={size} />;
return <DeviceIcon width={size} height={size} />;
};
const OnboardingScreen = ({ navigation, route }) => { const OnboardingScreen = ({ navigation, route }) => {
const userId = route?.params?.userId; const userId = route?.params?.userId;
+11 -15
View File
@@ -4,6 +4,7 @@ import {
} from 'react-native'; } from 'react-native';
import { useTheme } from '../services/ThemeContext'; import { useTheme } from '../services/ThemeContext';
import DatabaseService from '../services/DatabaseService'; import DatabaseService from '../services/DatabaseService';
import CenteredState from '../components/CenteredState';
import PhotoGrid from '../components/PhotoGrid'; import PhotoGrid from '../components/PhotoGrid';
import MediaViewer from '../components/MediaViewer'; import MediaViewer from '../components/MediaViewer';
import SearchIcon from '../../assets/icons/search.svg'; import SearchIcon from '../../assets/icons/search.svg';
@@ -66,21 +67,19 @@ const SearchScreen = ({ route }) => {
)} )}
{!searching && query.length > 0 && results.length === 0 && ( {!searching && query.length > 0 && results.length === 0 && (
<View style={styles.emptyContainer}> <CenteredState
<SearchIcon width={48} height={48} color={theme.textTertiary} /> icon={<SearchIcon width={48} height={48} color={theme.textTertiary} />}
<Text style={[styles.emptyTitle, { color: theme.text }]}>No results</Text> title="No results"
<Text style={[styles.emptyText, { color: theme.textSecondary }]}>Try different keywords</Text> description="Try different keywords"
</View> />
)} )}
{query.length === 0 && ( {query.length === 0 && (
<View style={styles.emptyContainer}> <CenteredState
<SearchIcon width={48} height={48} color={theme.textTertiary} /> icon={<SearchIcon width={48} height={48} color={theme.textTertiary} />}
<Text style={[styles.emptyTitle, { color: theme.text }]}>Search your photos</Text> title="Search your photos"
<Text style={[styles.emptyText, { color: theme.textSecondary }]}> description="Search by filename and indexed metadata"
Search by filename and indexed metadata />
</Text>
</View>
)} )}
{results.length > 0 && ( {results.length > 0 && (
@@ -110,9 +109,6 @@ const styles = StyleSheet.create({
searchInput: { flex: 1, fontSize: 15, padding: 0 }, searchInput: { flex: 1, fontSize: 15, padding: 0 },
clearButton: { fontSize: 16, padding: 4 }, clearButton: { fontSize: 16, padding: 4 },
loadingContainer: { padding: 20, alignItems: 'center' }, 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 }, resultCount: { paddingHorizontal: 16, paddingVertical: 8, fontSize: 13 },
}); });
+1 -10
View File
@@ -8,14 +8,11 @@ import { useTheme } from '../services/ThemeContext';
import DatabaseService from '../services/DatabaseService'; import DatabaseService from '../services/DatabaseService';
import NASSetupInstructions from '../components/NASSetupInstructions'; import NASSetupInstructions from '../components/NASSetupInstructions';
import ServiceHealthIndicator from '../components/ServiceHealthIndicator'; import ServiceHealthIndicator from '../components/ServiceHealthIndicator';
import ServiceGlyph from '../components/ServiceGlyph';
import NASAUTH from '../AUTH/NASAUTH'; import NASAUTH from '../AUTH/NASAUTH';
import GoogleAUTH from '../AUTH/GoogleAUTH'; import GoogleAUTH from '../AUTH/GoogleAUTH';
import { clearStoredUserId } from '../services/AuthSession'; import { clearStoredUserId } from '../services/AuthSession';
import CheckIcon from '../../assets/icons/check.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 SERVICE_NAMES = { GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud Photos', NAS: 'NAS Storage' }; const SERVICE_NAMES = { GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud Photos', NAS: 'NAS Storage' };
const AUTO_BACKUP_SERVICES = [ const AUTO_BACKUP_SERVICES = [
@@ -23,12 +20,6 @@ const AUTO_BACKUP_SERVICES = [
{ type: 'ICLOUD_PHOTOS', name: 'iCloud Photos' }, { type: 'ICLOUD_PHOTOS', name: 'iCloud Photos' },
{ type: 'NAS', name: 'NAS Storage' }, { type: 'NAS', name: 'NAS Storage' },
]; ];
const ServiceGlyph = ({ serviceType, size = 20 }) => {
if (serviceType === 'GOOGLE_PHOTOS') return <GooglePhotosIcon width={size} height={size} />;
if (serviceType === 'ICLOUD_PHOTOS') return <ICloudIcon width={size} height={size} />;
if (serviceType === 'NAS') return <NasIcon width={size} height={size} />;
return <DeviceIcon width={size} height={size} />;
};
const SettingsScreen = ({ navigation, route }) => { const SettingsScreen = ({ navigation, route }) => {
const userId = route?.params?.userId; const userId = route?.params?.userId;
+6 -5
View File
@@ -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 { class CredentialEncryption {
async hashPassword(password) { async hashPassword(password) {
try { try {
if (!password) throw new Error('Password is required'); if (!password) throw new Error('Password is required');
return await Crypto.digestStringAsync( return await ExpoCrypto.digestStringAsync(SHA_256, password, { encoding: HEX_ENCODING });
Crypto.CryptoDigestAlgorithm.SHA256,
password
);
} catch (error) { } catch (error) {
console.error('Password hashing error:', error); console.error('Password hashing error:', error);
throw new Error('Failed to hash password: ' + error.message); throw new Error('Failed to hash password: ' + error.message);