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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user