mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-08 03:40:20 +00:00
feat: refactor service glyphs and improve loading states; replace inline components with CenteredState and ServiceGlyph
This commit is contained in:
1 parent
bb1eb375ad
commit
126f545c44
9 files changed
+61
-137
No files matched your search
@@ -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;
|
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user