created a localised login system

This commit is contained in:
bobbert committed 2026-02-21 13:14:01 +00:00
1 parent 59ac1fc926
commit 37511780b7
16 files changed
+3721 -20

No files matched your search

+821
View File
@@ -0,0 +1,821 @@
import React, { useState, useEffect } from 'react';
import {
StyleSheet,
View,
Text,
FlatList,
ActivityIndicator,
Modal,
TouchableOpacity,
Dimensions,
Image,
Alert,
ScrollView,
RefreshControl,
} from 'react-native';
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 imageSize = (width - 50) / GRID_COLUMNS;
/**
* HomeScreen - Main photo gallery
*
* Responsibilities:
* - Load user data from database
* - Fetch photos from all connected services
* - Display photos in grid layout
* - Handle photo viewing in modal
* - Manage logout
*/
const HomeScreen = ({ navigation, route }) => {
const userId = route?.params?.userId;
// User data
const [currentUser, setCurrentUser] = useState(null);
const [linkedAccounts, setLinkedAccounts] = useState([]);
// Media data
const [allMedia, setAllMedia] = useState([]);
const [filteredMedia, setFilteredMedia] = useState([]);
const [selectedFilter, setSelectedFilter] = useState('all');
// UI state
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [selectedMedia, setSelectedMedia] = useState(null);
const [modalVisible, setModalVisible] = useState(false);
const [error, setError] = useState(null);
// Effect: Load user data on mount
useEffect(() => {
if (!userId) {
console.error('No userId provided to HomeScreen');
setError('User ID is missing');
setLoading(false);
return;
}
loadUserData();
}, [userId]);
// Effect: Load media when user data is ready
useEffect(() => {
if (currentUser) {
loadAllMedia();
}
}, [currentUser]);
// Effect: Filter media when filter changes
useEffect(() => {
filterMedia();
}, [selectedFilter, allMedia]);
/**
* Load user and linked accounts from database
*/
const loadUserData = async () => {
try {
const user = await DatabaseService.getUserById(userId);
setCurrentUser(user);
const accounts = await DatabaseService.getLinkedAccounts(userId);
setLinkedAccounts(accounts);
setError(null);
} catch (err) {
console.error('Error loading user data:', err);
setError('Failed to load user information');
Alert.alert('Error', 'Failed to load user information');
}
};
/**
* Load media from all connected services
*/
const loadAllMedia = async () => {
setLoading(true);
try {
// First, try to get media from database
const mediaFromDb = await DatabaseService.getMediaByUser(userId);
// Then fetch new media from each connected service
const newMedia = [];
// Fetch from Google Photos if enabled
const googleAccount = linkedAccounts.find(
(acc) => acc.service_type === 'GOOGLE_PHOTOS'
);
if (googleAccount) {
try {
const googleAuth = new GoogleAUTH();
console.log('Google Photos would be fetched here');
} catch (err) {
console.error('Google Photos fetch failed:', err);
}
}
// Fetch from iCloud if enabled
const iosAccount = linkedAccounts.find(
(acc) => acc.service_type === 'ICLOUD_PHOTOS'
);
if (iosAccount) {
try {
const iosAuth = new IOSAUTH();
console.log('iCloud Photos would be fetched here');
} catch (err) {
console.error('iCloud Photos fetch failed:', err);
}
}
// Fetch from NAS if enabled
const nasAccount = linkedAccounts.find(
(acc) => acc.service_type === 'NAS'
);
if (nasAccount) {
try {
const nasAuth = new NASAUTH();
const nasData = nasAccount.account_data;
// Connect to NAS via backend API
await nasAuth.authenticate(
nasData.host,
nasData.port,
nasData.username,
nasData.password,
nasData.photoFolder
);
// Get photos
const nasPhotos = await nasAuth.getPhotos(nasData.photoFolder);
if (nasPhotos.mediaItems) {
newMedia.push(...nasPhotos.mediaItems);
}
// Disconnect
await nasAuth.revokeAccess();
} catch (err) {
console.error('NAS Photos fetch failed:', err);
}
}
// Combine database media with newly fetched media
const combinedMedia = [...mediaFromDb, ...newMedia];
// Remove duplicates
const uniqueMedia = Array.from(
new Map(combinedMedia.map((m) => [m.source_id || m.id || m.path, m])).values()
);
setAllMedia(uniqueMedia);
setError(null);
} catch (err) {
console.error('Error loading media:', err);
setError('Failed to load photos');
Alert.alert('Error', 'Failed to load photos from services');
} finally {
setLoading(false);
setRefreshing(false);
}
};
/**
* Filter media based on selected service
*/
const filterMedia = () => {
let filtered = allMedia;
if (selectedFilter !== 'all') {
filtered = allMedia.filter(
(media) => media.source_service === selectedFilter
);
}
// Sort by date (newest first)
filtered.sort((a, b) => {
const dateA = new Date(a.created_date || a.modifyTime || 0);
const dateB = new Date(b.created_date || b.modifyTime || 0);
return dateB - dateA;
});
setFilteredMedia(filtered);
};
/**
* Handle refresh gesture
*/
const onRefresh = () => {
setRefreshing(true);
loadAllMedia();
};
/**
* Handle logout
*/
const handleLogout = () => {
Alert.alert(
'Logout',
'Are you sure you want to logout?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Logout',
style: 'destructive',
onPress: () => {
navigation.replace('Login');
},
},
]
);
};
/**
* Get service display name
*/
const getServiceName = (serviceType) => {
const serviceMap = {
GOOGLE_PHOTOS: 'Google Photos',
ICLOUD_PHOTOS: 'iCloud',
NAS: 'NAS',
};
return serviceMap[serviceType] || serviceType;
};
/**
* Get service icon
*/
const getServiceIcon = (serviceType) => {
const iconMap = {
GOOGLE_PHOTOS: '🔵',
ICLOUD_PHOTOS: '🍎',
NAS: '💾',
};
return iconMap[serviceType] || '📱';
};
/**
* Render grid item (photo)
*/
const renderMediaItem = ({ item }) => (
<TouchableOpacity
style={styles.gridItem}
onPress={() => {
setSelectedMedia(item);
setModalVisible(true);
}}
activeOpacity={0.7}
>
{item.local_path ? (
<Image
source={{ uri: item.local_path }}
style={styles.gridImage}
resizeMode="cover"
/>
) : (
<View style={styles.placeholderImage}>
<Text style={styles.placeholderIcon}>📷</Text>
</View>
)}
{item.media_type === 'video' && (
<View style={styles.videoOverlay}>
<Text style={styles.playIcon}>▶</Text>
</View>
)}
<View style={styles.serviceBadge}>
<Text style={styles.badgeIcon}>{getServiceIcon(item.source_service)}</Text>
</View>
</TouchableOpacity>
);
// Loading state
if (loading) {
return (
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Loading your photos...</Text>
</View>
);
}
// Error state
if (error && filteredMedia.length === 0) {
return (
<View style={styles.centerContainer}>
<Text style={styles.errorIcon}>⚠️</Text>
<Text style={styles.errorTitle}>Unable to Load Photos</Text>
<Text style={styles.errorText}>{error}</Text>
<TouchableOpacity style={styles.retryButton} onPress={loadAllMedia}>
<Text style={styles.retryButtonText}>Try Again</Text>
</TouchableOpacity>
</View>
);
}
// Empty state
if (filteredMedia.length === 0) {
return (
<View style={styles.container}>
<View style={styles.header}>
<View>
<Text style={styles.headerTitle}>Photo Library</Text>
<Text style={styles.headerSubtitle}>
Welcome, {currentUser?.username}
</Text>
</View>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutText}>Logout</Text>
</TouchableOpacity>
</View>
<View style={styles.centerContainer}>
<Text style={styles.emptyIcon}>📸</Text>
<Text style={styles.emptyTitle}>No Photos Yet</Text>
<Text style={styles.emptyText}>
Photos from your connected services will appear here
</Text>
</View>
</View>
);
}
// Main content
return (
<View style={styles.container}>
<View style={styles.header}>
<View>
<Text style={styles.headerTitle}>Photo Library</Text>
<Text style={styles.headerSubtitle}>
Welcome, {currentUser?.username}
</Text>
</View>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutText}>Logout</Text>
</TouchableOpacity>
</View>
{linkedAccounts.length > 0 && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.filterScroll}
>
<TouchableOpacity
style={[
styles.filterButton,
selectedFilter === 'all' && styles.filterButtonActive,
]}
onPress={() => setSelectedFilter('all')}
>
<Text
style={[
styles.filterButtonText,
selectedFilter === 'all' && styles.filterButtonTextActive,
]}
>
All
</Text>
</TouchableOpacity>
{linkedAccounts.map((account) => (
<TouchableOpacity
key={account.id}
style={[
styles.filterButton,
selectedFilter === account.service_type &&
styles.filterButtonActive,
]}
onPress={() => setSelectedFilter(account.service_type)}
>
<Text style={styles.filterIcon}>
{getServiceIcon(account.service_type)}
</Text>
<Text
style={[
styles.filterButtonText,
selectedFilter === account.service_type &&
styles.filterButtonTextActive,
]}
>
{getServiceName(account.service_type)}
</Text>
</TouchableOpacity>
))}
</ScrollView>
)}
<FlatList
data={filteredMedia}
renderItem={renderMediaItem}
keyExtractor={(item) => item.id?.toString() || item.path || Math.random().toString()}
numColumns={GRID_COLUMNS}
columnWrapperStyle={styles.columnWrapper}
contentContainerStyle={styles.gridContent}
scrollEnabled={true}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
/>
<Modal
visible={modalVisible}
transparent={true}
onRequestClose={() => setModalVisible(false)}
animationType="fade"
>
<MediaDetailModal
media={selectedMedia}
onClose={() => setModalVisible(false)}
serviceIcon={getServiceIcon(selectedMedia?.source_service)}
serviceName={getServiceName(selectedMedia?.source_service)}
/>
</Modal>
</View>
);
};
/**
* MediaDetailModal - Full screen photo viewer
*/
const MediaDetailModal = ({ media, onClose, serviceIcon, serviceName }) => {
if (!media) return null;
return (
<View style={styles.modalContainer}>
<TouchableOpacity
style={styles.modalBackdrop}
onPress={onClose}
activeOpacity={1}
/>
<View style={styles.modalContent}>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<Text style={styles.closeButtonText}>✕</Text>
</TouchableOpacity>
<ScrollView
style={styles.imageContainer}
contentContainerStyle={styles.imageContent}
scrollEnabled={true}
showsVerticalScrollIndicator={false}
>
{media.local_path ? (
<Image
source={{ uri: media.local_path }}
style={styles.fullImage}
resizeMode="contain"
/>
) : (
<View style={styles.placeholderLarge}>
<Text style={styles.placeholderLargeIcon}>📷</Text>
</View>
)}
</ScrollView>
<View style={styles.infoPanel}>
<Text style={styles.infoTitle}>{media.title || 'Untitled Photo'}</Text>
{media.description && (
<Text style={styles.infoDescription}>{media.description}</Text>
)}
<View style={styles.infoDivider} />
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>From</Text>
<View style={styles.infoServiceBadge}>
<Text style={styles.infoServiceIcon}>{serviceIcon}</Text>
<Text style={styles.infoServiceName}>{serviceName}</Text>
</View>
</View>
{media.media_type && (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Type</Text>
<Text style={styles.infoValue}>
{media.media_type === 'video' ? '🎥 Video' : '📷 Photo'}
</Text>
</View>
)}
{(media.created_date || media.modifyTime) && (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Date</Text>
<Text style={styles.infoValue}>
{new Date(
media.created_date || media.modifyTime
).toLocaleDateString()}
</Text>
</View>
)}
{media.size && (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Size</Text>
<Text style={styles.infoValue}>
{(media.size / 1024 / 1024).toFixed(2)} MB
</Text>
</View>
)}
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
centerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20,
},
header: {
backgroundColor: '#fff',
paddingHorizontal: 16,
paddingVertical: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold',
color: '#1a1a1a',
},
headerSubtitle: {
fontSize: 13,
color: '#999',
marginTop: 2,
},
logoutButton: {
backgroundColor: '#ff3b30',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 6,
},
logoutText: {
color: '#fff',
fontSize: 13,
fontWeight: '600',
},
filterScroll: {
paddingHorizontal: 12,
paddingVertical: 12,
gap: 8,
},
filterButton: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 8,
backgroundColor: '#fff',
borderRadius: 16,
borderWidth: 1,
borderColor: '#e0e0e0',
gap: 6,
},
filterButtonActive: {
backgroundColor: '#007AFF',
borderColor: '#007AFF',
},
filterButtonText: {
fontSize: 13,
fontWeight: '600',
color: '#333',
},
filterButtonTextActive: {
color: '#fff',
},
filterIcon: {
fontSize: 14,
},
columnWrapper: {
gap: 8,
paddingHorizontal: 8,
marginBottom: 8,
},
gridContent: {
paddingVertical: 8,
paddingBottom: 20,
},
gridItem: {
width: imageSize,
height: imageSize,
borderRadius: 8,
overflow: 'hidden',
backgroundColor: '#e8e8e8',
position: 'relative',
},
gridImage: {
width: '100%',
height: '100%',
},
placeholderImage: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#e8e8e8',
},
placeholderIcon: {
fontSize: 32,
},
videoOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'center',
alignItems: 'center',
},
playIcon: {
color: '#fff',
fontSize: 24,
fontWeight: 'bold',
},
serviceBadge: {
position: 'absolute',
bottom: 6,
right: 6,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
borderRadius: 12,
paddingHorizontal: 6,
paddingVertical: 3,
},
badgeIcon: {
fontSize: 12,
},
loadingText: {
marginTop: 12,
fontSize: 16,
color: '#666',
},
errorIcon: {
fontSize: 64,
marginBottom: 16,
},
errorTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
errorText: {
fontSize: 14,
color: '#666',
marginBottom: 24,
textAlign: 'center',
},
retryButton: {
backgroundColor: '#007AFF',
paddingHorizontal: 24,
paddingVertical: 10,
borderRadius: 8,
},
retryButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
emptyIcon: {
fontSize: 64,
marginBottom: 16,
},
emptyTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
emptyText: {
fontSize: 14,
color: '#999',
textAlign: 'center',
},
modalContainer: {
flex: 1,
backgroundColor: '#000',
},
modalBackdrop: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
modalContent: {
flex: 1,
zIndex: 10,
},
closeButton: {
position: 'absolute',
top: 12,
right: 12,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 20,
},
closeButtonText: {
color: '#fff',
fontSize: 24,
fontWeight: '600',
},
imageContainer: {
flex: 0.6,
backgroundColor: '#000',
},
imageContent: {
justifyContent: 'center',
alignItems: 'center',
minHeight: '100%',
},
fullImage: {
width: '100%',
height: '100%',
},
placeholderLarge: {
width: 200,
height: 200,
backgroundColor: '#333',
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
placeholderLargeIcon: {
fontSize: 64,
},
infoPanel: {
flex: 0.4,
backgroundColor: '#fff',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
padding: 16,
paddingBottom: 24,
},
infoTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1a1a1a',
marginBottom: 6,
},
infoDescription: {
fontSize: 13,
color: '#666',
marginBottom: 12,
},
infoDivider: {
height: 1,
backgroundColor: '#f0f0f0',
marginVertical: 12,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 10,
},
infoLabel: {
fontSize: 12,
fontWeight: '600',
color: '#999',
},
infoValue: {
fontSize: 13,
color: '#333',
fontWeight: '600',
},
infoServiceBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
infoServiceIcon: {
fontSize: 14,
},
infoServiceName: {
fontSize: 13,
color: '#333',
fontWeight: '600',
},
});
export default HomeScreen;