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:
bobbert committed 2026-02-21 15:04:58 +00:00
1 parent c1d7139055
commit 6f4ec66e2e
17 files changed
+1761 -1018

No files matched your search

+32 -98
View File
@@ -1,30 +1,25 @@
import React, { useState, useEffect } from 'react';
import {
StyleSheet, View, Text, TextInput, FlatList, TouchableOpacity,
Dimensions, Image, Modal, ActivityIndicator,
StyleSheet, View, Text, TextInput, TouchableOpacity, ActivityIndicator,
} from 'react-native';
import { useTheme } from '../services/ThemeContext';
import DatabaseService from '../services/DatabaseService';
const { width } = Dimensions.get('window');
const GRID_COLUMNS = 3;
const GRID_GAP = 2;
const imageSize = (width - GRID_GAP * (GRID_COLUMNS + 1)) / GRID_COLUMNS;
import PhotoGrid from '../components/PhotoGrid';
import MediaViewer from '../components/MediaViewer';
const SearchScreen = ({ route }) => {
const userId = route?.params?.userId;
const { theme } = useTheme();
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [searching, setSearching] = useState(false);
const [selectedMedia, setSelectedMedia] = useState(null);
const [modalVisible, setModalVisible] = useState(false);
const [viewerVisible, setViewerVisible] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
if (query.trim().length > 0) {
performSearch();
} else {
setResults([]);
}
if (query.trim().length > 0) performSearch();
else setResults([]);
}, 300);
return () => clearTimeout(timer);
}, [query]);
@@ -42,36 +37,14 @@ const SearchScreen = ({ route }) => {
}
};
const getServiceIcon = (type) => ({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱');
const renderItem = ({ 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>
)}
<View style={styles.sourceBadge}>
<Text style={{ fontSize: 10 }}>{getServiceIcon(item.source_service)}</Text>
</View>
</TouchableOpacity>
);
return (
<View style={styles.container}>
{/* Search bar */}
<View style={styles.searchBar}>
<View style={[styles.container, { backgroundColor: theme.background }]}>
<View style={[styles.searchBar, { backgroundColor: theme.searchBg }]}>
<Text style={styles.searchIcon}>🔍</Text>
<TextInput
style={styles.searchInput}
style={[styles.searchInput, { color: theme.text }]}
placeholder="Search photos and videos"
placeholderTextColor="#9aa0a6"
placeholderTextColor={theme.textTertiary}
value={query}
onChangeText={setQuery}
autoFocus
@@ -80,105 +53,66 @@ const SearchScreen = ({ route }) => {
/>
{query.length > 0 && (
<TouchableOpacity onPress={() => setQuery('')}>
<Text style={styles.clearButton}>✕</Text>
<Text style={[styles.clearButton, { color: theme.textSecondary }]}>✕</Text>
</TouchableOpacity>
)}
</View>
{searching && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color="#1a73e8" />
<ActivityIndicator size="small" color={theme.primary} />
</View>
)}
{!searching && query.length > 0 && results.length === 0 && (
<View style={styles.emptyContainer}>
<Text style={styles.emptyIcon}>🔍</Text>
<Text style={styles.emptyTitle}>No results</Text>
<Text style={styles.emptyText}>Try searching with different keywords</Text>
<Text style={[styles.emptyTitle, { color: theme.text }]}>No results</Text>
<Text style={[styles.emptyText, { color: theme.textSecondary }]}>Try different keywords</Text>
</View>
)}
{query.length === 0 && (
<View style={styles.emptyContainer}>
<Text style={styles.emptyIcon}>🔍</Text>
<Text style={styles.emptyTitle}>Search your photos</Text>
<Text style={styles.emptyText}>Search by title or description</Text>
<Text style={[styles.emptyTitle, { color: theme.text }]}>Search your photos</Text>
<Text style={[styles.emptyText, { color: theme.textSecondary }]}>Search by title or description</Text>
</View>
)}
{results.length > 0 && (
<>
<Text style={styles.resultCount}>{results.length} result{results.length !== 1 ? 's' : ''}</Text>
<FlatList
<Text style={[styles.resultCount, { color: theme.textSecondary }]}>
{results.length} result{results.length !== 1 ? 's' : ''}
</Text>
<PhotoGrid
data={results}
renderItem={renderItem}
keyExtractor={(item, i) => item.id?.toString() || i.toString()}
numColumns={GRID_COLUMNS}
contentContainerStyle={styles.gridContent}
onItemPress={(item) => { setSelectedMedia(item); setViewerVisible(true); }}
/>
</>
)}
{/* Photo viewer modal */}
<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>
</View>
</Modal>
<MediaViewer visible={viewerVisible} media={selectedMedia} onClose={() => setViewerVisible(false)} />
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' },
container: { flex: 1 },
searchBar: {
flexDirection: 'row', alignItems: 'center', marginHorizontal: 12,
marginTop: 48, marginBottom: 8, backgroundColor: '#f1f3f4',
borderRadius: 24, paddingHorizontal: 14, paddingVertical: 10, gap: 8,
marginTop: 48, marginBottom: 8, borderRadius: 24,
paddingHorizontal: 14, paddingVertical: 10, gap: 8,
},
searchIcon: { fontSize: 16 },
searchInput: { flex: 1, fontSize: 15, color: '#202124', padding: 0 },
clearButton: { fontSize: 16, color: '#5f6368', padding: 4 },
searchInput: { flex: 1, fontSize: 15, padding: 0 },
clearButton: { fontSize: 16, padding: 4 },
loadingContainer: { padding: 20, alignItems: 'center' },
emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 24 },
emptyIcon: { fontSize: 48, marginBottom: 12 },
emptyTitle: { fontSize: 18, fontWeight: '600', color: '#202124', marginBottom: 6 },
emptyText: { fontSize: 13, color: '#5f6368', textAlign: 'center' },
resultCount: { paddingHorizontal: 16, paddingVertical: 8, fontSize: 13, color: '#5f6368' },
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' },
sourceBadge: {
position: 'absolute', bottom: 4, right: 4, backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 8, paddingHorizontal: 4, paddingVertical: 2,
},
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' },
emptyTitle: { fontSize: 18, fontWeight: '600', marginBottom: 6 },
emptyText: { fontSize: 13, textAlign: 'center' },
resultCount: { paddingHorizontal: 16, paddingVertical: 8, fontSize: 13 },
});
export default SearchScreen;