mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-08 03:40:20 +00:00
refactored code into components (major cleanup for reused code) added email for local acccount creation and changes to how nas and google photo connections work and added themes
This commit is contained in:
1 parent
c1d7139055
commit
6f4ec66e2e
17 files changed
+1761
-1018
No files matched your search
+74
-233
@@ -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 }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.gridItem}
|
||||
onPress={() => { setSelectedMedia(item); setModalVisible(true); }}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{item.local_path ? (
|
||||
<Image source={{ uri: item.local_path }} style={styles.gridImage} resizeMode="cover" />
|
||||
) : (
|
||||
<View style={styles.placeholder}>
|
||||
<Text style={{ fontSize: 28 }}>📷</Text>
|
||||
</View>
|
||||
)}
|
||||
{item.media_type === 'video' && (
|
||||
<View style={styles.videoOverlay}>
|
||||
<Text style={{ color: '#fff', fontSize: 20 }}>▶</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.sourceBadge}>
|
||||
<Text style={{ fontSize: 10 }}>{getServiceIcon(item.source_service)}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
// Top bar component
|
||||
const TopBar = () => (
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity style={styles.topBarSearch} onPress={() => navigation.navigate('Search')}>
|
||||
<Text style={styles.topBarSearchIcon}>🔍</Text>
|
||||
<Text style={styles.topBarSearchText}>Search your photos</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.profileButton} onPress={() => setMenuVisible(true)}>
|
||||
<Text style={styles.profileInitial}>
|
||||
{currentUser?.username?.charAt(0)?.toUpperCase() || '?'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.menuButton} onPress={() => setMenuVisible(true)}>
|
||||
<Text style={styles.menuDots}>⋮</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
// Dropdown menu
|
||||
const DropdownMenu = () => (
|
||||
<Modal visible={menuVisible} transparent animationType="fade" onRequestClose={() => setMenuVisible(false)}>
|
||||
<TouchableOpacity style={styles.menuOverlay} onPress={() => setMenuVisible(false)} activeOpacity={1}>
|
||||
<View style={styles.menuDropdown}>
|
||||
<View style={styles.menuProfile}>
|
||||
<View style={styles.menuAvatar}>
|
||||
<Text style={styles.menuAvatarText}>{currentUser?.username?.charAt(0)?.toUpperCase() || '?'}</Text>
|
||||
</View>
|
||||
<Text style={styles.menuUsername}>{currentUser?.username || 'User'}</Text>
|
||||
</View>
|
||||
<View style={styles.menuDivider} />
|
||||
<TouchableOpacity style={styles.menuItem} onPress={() => { setMenuVisible(false); navigation.navigate('Onboarding', { userId }); }}>
|
||||
<Text style={styles.menuItemText}>Manage Services</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.menuItem} onPress={() => { setMenuVisible(false); handleLogout(); }}>
|
||||
<Text style={[styles.menuItemText, { color: '#d93025' }]}>Sign Out</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
if (loading && allMedia.length === 0) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<ActivityIndicator size="large" color="#1a73e8" />
|
||||
<Text style={styles.loadingText}>Loading photos...</Text>
|
||||
<View style={[styles.centerContainer, { backgroundColor: theme.background }]}>
|
||||
<ActivityIndicator size="large" color={theme.primary} />
|
||||
<Text style={[styles.loadingText, { color: theme.textSecondary }]}>Loading photos...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TopBar />
|
||||
<DropdownMenu />
|
||||
const FilterChips = () => (
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.chip, { backgroundColor: selectedFilter === 'all' ? theme.chipActiveBg : theme.chipBg }]}
|
||||
onPress={() => setSelectedFilter('all')}
|
||||
>
|
||||
<Text style={[styles.chipText, selectedFilter === 'all' && { color: theme.primary, fontWeight: '600' }]}>All</Text>
|
||||
</TouchableOpacity>
|
||||
{linkedAccounts.map((acc) => (
|
||||
<TouchableOpacity
|
||||
key={acc.id}
|
||||
style={[styles.chip, { backgroundColor: selectedFilter === acc.service_type ? theme.chipActiveBg : theme.chipBg }]}
|
||||
onPress={() => setSelectedFilter(acc.service_type)}
|
||||
>
|
||||
<Text style={styles.chipIcon}>{getServiceIcon(acc.service_type)}</Text>
|
||||
<Text style={[styles.chipText, selectedFilter === acc.service_type && { color: theme.primary, fontWeight: '600' }]}>
|
||||
{getServiceName(acc.service_type)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
{/* Filter chips */}
|
||||
{linkedAccounts.length > 0 && (
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.chip, selectedFilter === 'all' && styles.chipActive]}
|
||||
onPress={() => setSelectedFilter('all')}
|
||||
>
|
||||
<Text style={[styles.chipText, selectedFilter === 'all' && styles.chipTextActive]}>All</Text>
|
||||
</TouchableOpacity>
|
||||
{linkedAccounts.map((acc) => (
|
||||
<TouchableOpacity
|
||||
key={acc.id}
|
||||
style={[styles.chip, selectedFilter === acc.service_type && styles.chipActive]}
|
||||
onPress={() => setSelectedFilter(acc.service_type)}
|
||||
>
|
||||
<Text style={styles.chipIcon}>{getServiceIcon(acc.service_type)}</Text>
|
||||
<Text style={[styles.chipText, selectedFilter === acc.service_type && styles.chipTextActive]}>
|
||||
{getServiceName(acc.service_type)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<TopBar
|
||||
currentUser={currentUser}
|
||||
onSearchPress={() => navigation.navigate('Search')}
|
||||
onProfilePress={() => navigation.navigate('Settings', { userId })}
|
||||
onMenuPress={() => navigation.navigate('Settings', { userId })}
|
||||
/>
|
||||
|
||||
{linkedAccounts.length > 0 && <FilterChips />}
|
||||
|
||||
{filteredMedia.length === 0 ? (
|
||||
<View style={styles.centerContainer}>
|
||||
<Text style={{ fontSize: 56, marginBottom: 16 }}>📸</Text>
|
||||
<Text style={styles.emptyTitle}>No Photos Yet</Text>
|
||||
<Text style={styles.emptyText}>Photos from your connected services will appear here</Text>
|
||||
<TouchableOpacity style={styles.retryButton} onPress={() => { setRefreshing(true); loadAllMedia(); }}>
|
||||
<Text style={[styles.emptyTitle, { color: theme.text }]}>No Photos Yet</Text>
|
||||
<Text style={[styles.emptyText, { color: theme.textSecondary }]}>
|
||||
Photos from your connected services will appear here
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.retryButton, { backgroundColor: theme.primary }]}
|
||||
onPress={() => { setRefreshing(true); loadAllMedia(); }}
|
||||
>
|
||||
<Text style={styles.retryButtonText}>Refresh</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
<PhotoGrid
|
||||
data={filteredMedia}
|
||||
renderItem={renderMediaItem}
|
||||
keyExtractor={(item, i) => item.id?.toString() || item.source_id || i.toString()}
|
||||
numColumns={GRID_COLUMNS}
|
||||
contentContainerStyle={styles.gridContent}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); loadAllMedia(); }} />}
|
||||
onItemPress={(item) => { setSelectedMedia(item); setViewerVisible(true); }}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); loadAllMedia(); }} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Full-screen photo viewer */}
|
||||
<Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
|
||||
<View style={styles.modalContainer}>
|
||||
<TouchableOpacity style={styles.modalClose} onPress={() => setModalVisible(false)}>
|
||||
<Text style={styles.modalCloseText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
{selectedMedia?.local_path ? (
|
||||
<Image source={{ uri: selectedMedia.local_path }} style={styles.fullImage} resizeMode="contain" />
|
||||
) : (
|
||||
<View style={styles.fullPlaceholder}>
|
||||
<Text style={{ fontSize: 64 }}>📷</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.mediaInfo}>
|
||||
<Text style={styles.mediaTitle}>{selectedMedia?.title || 'Photo'}</Text>
|
||||
<View style={styles.mediaRow}>
|
||||
<Text style={styles.mediaLabel}>Source</Text>
|
||||
<Text style={styles.mediaValue}>
|
||||
{getServiceIcon(selectedMedia?.source_service)} {getServiceName(selectedMedia?.source_service)}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedMedia?.created_date && (
|
||||
<View style={styles.mediaRow}>
|
||||
<Text style={styles.mediaLabel}>Date</Text>
|
||||
<Text style={styles.mediaValue}>{new Date(selectedMedia.created_date).toLocaleDateString()}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
<MediaViewer
|
||||
visible={viewerVisible}
|
||||
media={selectedMedia}
|
||||
onClose={() => setViewerVisible(false)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user