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

+74
View File
@@ -0,0 +1,74 @@
import React from 'react';
import {
Modal, View, Text, TouchableOpacity, Image, StyleSheet,
} from 'react-native';
import { useTheme } from '../services/ThemeContext';
const getServiceIcon = (type) =>
({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱');
const getServiceName = (type) =>
({ GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' }[type] || type);
const MediaViewer = ({ visible, media, onClose }) => {
const { theme } = useTheme();
if (!media) return null;
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<View style={[styles.container, { backgroundColor: theme.modalBg }]}>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<Text style={styles.closeText}>✕</Text>
</TouchableOpacity>
{media.local_path ? (
<Image source={{ uri: media.local_path }} style={styles.fullImage} resizeMode="contain" />
) : (
<View style={styles.fullPlaceholder}>
<Text style={{ fontSize: 64 }}>📷</Text>
</View>
)}
<View style={[styles.infoPanel, { backgroundColor: theme.modalInfoBg }]}>
<Text style={styles.title}>{media.title || 'Photo'}</Text>
<View style={styles.row}>
<Text style={styles.label}>Source</Text>
<Text style={styles.value}>
{getServiceIcon(media.source_service)} {getServiceName(media.source_service)}
</Text>
</View>
{media.created_date && (
<View style={styles.row}>
<Text style={styles.label}>Date</Text>
<Text style={styles.value}>
{new Date(media.created_date).toLocaleDateString()}
</Text>
</View>
)}
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
container: { flex: 1 },
closeButton: {
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',
},
closeText: { color: '#fff', fontSize: 20, fontWeight: '600' },
fullImage: { flex: 1, width: '100%' },
fullPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center' },
infoPanel: {
padding: 16, paddingBottom: 32,
borderTopLeftRadius: 16, borderTopRightRadius: 16,
},
title: { fontSize: 16, fontWeight: '600', color: '#fff', marginBottom: 8 },
row: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 },
label: { fontSize: 12, color: '#9aa0a6' },
value: { fontSize: 12, color: '#e8eaed', fontWeight: '500' },
});
export default MediaViewer;
+101
View File
@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useTheme } from '../services/ThemeContext';
const NASSetupInstructions = () => {
const { theme } = useTheme();
const [expanded, setExpanded] = useState(false);
return (
<View style={[styles.container, { backgroundColor: theme.card, borderColor: theme.border }]}>
<TouchableOpacity
style={styles.header}
onPress={() => setExpanded(!expanded)}
activeOpacity={0.7}
>
<Text style={[styles.headerText, { color: theme.text }]}>
📋 How to set up a NAS for photo storage
</Text>
<Text style={[styles.arrow, { color: theme.textSecondary }]}>
{expanded ? '▲' : '▼'}
</Text>
</TouchableOpacity>
{expanded && (
<View style={styles.body}>
<Text style={[styles.stepTitle, { color: theme.primary }]}>Prerequisites</Text>
<Text style={[styles.stepText, { color: theme.textSecondary }]}>
• An Ubuntu server (20.04 LTS or later) with SSH access enabled{'\n'}
• A user account with sudo access on the server{'\n'}
• The server's IP address and SSH port (default: 22)
</Text>
<Text style={[styles.stepTitle, { color: theme.primary }]}>Step 1: Enable SSH</Text>
<Text style={[styles.stepText, { color: theme.textSecondary }]}>
If SSH is not already enabled on your Ubuntu server, open a terminal on the server and run:{'\n\n'}
sudo apt update && sudo apt install openssh-server{'\n'}
sudo systemctl enable ssh && sudo systemctl start ssh
</Text>
<Text style={[styles.stepTitle, { color: theme.primary }]}>Step 2: Create a Photos Directory</Text>
<Text style={[styles.stepText, { color: theme.textSecondary }]}>
Create a dedicated folder for your photo storage:{'\n\n'}
mkdir -p ~/photos{'\n'}
chmod 755 ~/photos
</Text>
<Text style={[styles.stepTitle, { color: theme.primary }]}>Step 3: Configure Firewall (if needed)</Text>
<Text style={[styles.stepText, { color: theme.textSecondary }]}>
If your server uses UFW, allow SSH connections:{'\n\n'}
sudo ufw allow ssh{'\n'}
sudo ufw enable
</Text>
<Text style={[styles.stepTitle, { color: theme.primary }]}>Step 4: Find Your Server IP</Text>
<Text style={[styles.stepText, { color: theme.textSecondary }]}>
Run this on your server to find its local IP:{'\n\n'}
hostname -I{'\n\n'}
Use that IP address in the "Server Address" field above.
</Text>
<Text style={[styles.stepTitle, { color: theme.primary }]}>Step 5: Connect from the App</Text>
<Text style={[styles.stepText, { color: theme.textSecondary }]}>
Enter the server IP, SSH port (22), your username, password, and the photos folder path (e.g. /home/youruser/photos).{'\n\n'}
The app's backend server will handle the SFTP connection securely.
</Text>
<Text style={[styles.stepTitle, { color: theme.primary }]}>Step 6: Run the Backend Server</Text>
<Text style={[styles.stepText, { color: theme.textSecondary }]}>
The NAS connection uses a backend server. Navigate to the ServerFiles folder and run:{'\n\n'}
npm install{'\n'}
node server.js{'\n\n'}
Ensure the backend URL in the app settings matches the server address (e.g. http://192.168.1.100:3001).
</Text>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
borderRadius: 12,
borderWidth: 1,
overflow: 'hidden',
marginTop: 8,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 14,
},
headerText: { fontSize: 13, fontWeight: '600', flex: 1, marginRight: 8 },
arrow: { fontSize: 12 },
body: { paddingHorizontal: 14, paddingBottom: 14 },
stepTitle: { fontSize: 13, fontWeight: '700', marginTop: 12, marginBottom: 4 },
stepText: { fontSize: 12, lineHeight: 18 },
});
export default NASSetupInstructions;
+83
View File
@@ -0,0 +1,83 @@
import React from 'react';
import {
View, Text, FlatList, TouchableOpacity, Image, Dimensions, StyleSheet,
} from 'react-native';
import { useTheme } from '../services/ThemeContext';
const { width } = Dimensions.get('window');
const GRID_GAP = 2;
const PhotoGrid = ({
data,
onItemPress,
columns = 3,
refreshControl,
ListHeaderComponent,
ListEmptyComponent,
}) => {
const { theme } = useTheme();
const itemSize = (width - GRID_GAP * (columns + 1)) / columns;
const getServiceIcon = (type) =>
({ GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱');
const renderItem = ({ item }) => (
<TouchableOpacity
style={[styles.gridItem, { width: itemSize, height: itemSize, backgroundColor: theme.chipBg }]}
onPress={() => onItemPress?.(item)}
activeOpacity={0.8}
>
{item.local_path ? (
<Image source={{ uri: item.local_path }} style={styles.gridImage} resizeMode="cover" />
) : (
<View style={[styles.placeholder, { backgroundColor: theme.borderLight }]}>
<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>
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item, i) => item.id?.toString() || item.source_id || i.toString()}
numColumns={columns}
contentContainerStyle={styles.gridContent}
refreshControl={refreshControl}
ListHeaderComponent={ListHeaderComponent}
ListEmptyComponent={ListEmptyComponent}
/>
);
};
const styles = StyleSheet.create({
gridContent: { paddingBottom: 20 },
gridItem: {
margin: GRID_GAP / 2,
position: 'relative',
},
gridImage: { width: '100%', height: '100%' },
placeholder: {
width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center',
},
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,
},
});
export default PhotoGrid;
+65
View File
@@ -0,0 +1,65 @@
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useTheme } from '../services/ThemeContext';
const TopBar = ({ currentUser, onSearchPress, onProfilePress, onMenuPress }) => {
const { theme } = useTheme();
return (
<View style={[styles.topBar, { backgroundColor: theme.background }]}>
<TouchableOpacity
style={[styles.searchPill, { backgroundColor: theme.searchBg }]}
onPress={onSearchPress}
>
<Text style={styles.searchIcon}>🔍</Text>
<Text style={[styles.searchText, { color: theme.textSecondary }]}>Search your photos</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.profileButton, { backgroundColor: theme.profileBg }]}
onPress={onProfilePress}
>
<Text style={styles.profileInitial}>
{currentUser?.username?.charAt(0)?.toUpperCase() || '?'}
</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.menuButton} onPress={onMenuPress}>
<Text style={[styles.menuDots, { color: theme.textSecondary }]}>⋮</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
topBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingTop: 48,
paddingBottom: 8,
gap: 8,
},
searchPill: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 24,
paddingHorizontal: 14,
paddingVertical: 10,
gap: 8,
},
searchIcon: { fontSize: 16 },
searchText: { fontSize: 14 },
profileButton: {
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
},
profileInitial: { color: '#fff', fontSize: 14, fontWeight: '700' },
menuButton: { width: 32, height: 32, justifyContent: 'center', alignItems: 'center' },
menuDots: { fontSize: 22, fontWeight: '700' },
});
export default TopBar;