mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-08 03:40:20 +00:00
added NAS configuration file; updated MediaViewer to support thumbnail carousel; improved error handling in NASAPI and NASAUTH; refactored various comments for clarity
This commit is contained in:
1 parent
9519b32d68
commit
f8e11fc368
27 files changed
+1036
-558
No files matched your search
+191
-11
@@ -1,45 +1,202 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Modal, View, Text, TouchableOpacity, Image, StyleSheet,
|
||||
Dimensions, FlatList,
|
||||
} from 'react-native';
|
||||
import { useTheme } from '../services/ThemeContext';
|
||||
|
||||
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
const THUMB_SIZE = 60;
|
||||
const THUMB_GAP = 4;
|
||||
|
||||
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 }) => {
|
||||
/**
|
||||
* Fullscreen media viewer with thumbnail carousel.
|
||||
*
|
||||
* Props:
|
||||
* visible – boolean to show/hide the modal
|
||||
* media – currently selected media item
|
||||
* allMedia – (optional) full array of media items for carousel navigation
|
||||
* onClose – callback when user dismisses the viewer
|
||||
*/
|
||||
const MediaViewer = ({ visible, media, allMedia, onClose }) => {
|
||||
const { theme } = useTheme();
|
||||
const mainListRef = useRef(null);
|
||||
const thumbListRef = useRef(null);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [currentMedia, setCurrentMedia] = useState(media);
|
||||
|
||||
const mediaList = allMedia && allMedia.length > 0 ? allMedia : (media ? [media] : []);
|
||||
const hasCarousel = mediaList.length > 1;
|
||||
|
||||
// When the modal opens or media prop changes, scroll to the correct item
|
||||
useEffect(() => {
|
||||
if (!visible || !media || mediaList.length === 0) return;
|
||||
|
||||
const idx = mediaList.findIndex(
|
||||
(m) => (m.source_id || m.id) === (media.source_id || media.id)
|
||||
);
|
||||
const newIndex = idx >= 0 ? idx : 0;
|
||||
setActiveIndex(newIndex);
|
||||
setCurrentMedia(mediaList[newIndex]);
|
||||
|
||||
// Delay scroll so FlatList has rendered
|
||||
setTimeout(() => {
|
||||
if (mainListRef.current && newIndex >= 0) {
|
||||
mainListRef.current.scrollToOffset({ offset: newIndex * SCREEN_WIDTH, animated: false });
|
||||
}
|
||||
if (thumbListRef.current && newIndex >= 0) {
|
||||
try {
|
||||
thumbListRef.current.scrollToIndex({ index: newIndex, animated: false, viewPosition: 0.5 });
|
||||
} catch (e) { /* index might be out of range during initial render */ }
|
||||
}
|
||||
}, 100);
|
||||
}, [visible, media]);
|
||||
|
||||
const onMainScroll = useCallback(
|
||||
(event) => {
|
||||
const offsetX = event.nativeEvent.contentOffset.x;
|
||||
const idx = Math.round(offsetX / SCREEN_WIDTH);
|
||||
if (idx >= 0 && idx < mediaList.length && idx !== activeIndex) {
|
||||
setActiveIndex(idx);
|
||||
setCurrentMedia(mediaList[idx]);
|
||||
if (thumbListRef.current) {
|
||||
try {
|
||||
thumbListRef.current.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 });
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeIndex, mediaList]
|
||||
);
|
||||
|
||||
const onThumbPress = (index) => {
|
||||
setActiveIndex(index);
|
||||
setCurrentMedia(mediaList[index]);
|
||||
if (mainListRef.current) {
|
||||
mainListRef.current.scrollToOffset({ offset: index * SCREEN_WIDTH, animated: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (!media) return null;
|
||||
|
||||
const renderMainItem = ({ item }) => (
|
||||
<View style={{ width: SCREEN_WIDTH, height: SCREEN_HEIGHT }}>
|
||||
{item.local_path ? (
|
||||
<Image source={{ uri: item.local_path }} style={styles.fullImage} resizeMode="contain" />
|
||||
) : (
|
||||
<View style={styles.fullPlaceholder}>
|
||||
<Text style={{ fontSize: 64 }}>📷</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderThumb = ({ item, index }) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.thumbItem,
|
||||
index === activeIndex && { borderColor: '#fff', borderWidth: 2 },
|
||||
]}
|
||||
onPress={() => onThumbPress(index)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{item.local_path ? (
|
||||
<Image source={{ uri: item.local_path }} style={styles.thumbImage} resizeMode="cover" />
|
||||
) : (
|
||||
<View style={styles.thumbPlaceholder}>
|
||||
<Text style={{ fontSize: 20 }}>📷</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const displayMedia = currentMedia || media;
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
||||
<View style={[styles.container, { backgroundColor: theme.modalBg }]}>
|
||||
{/* Close button */}
|
||||
<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>
|
||||
|
||||
{/* Counter */}
|
||||
{hasCarousel && (
|
||||
<View style={styles.counter}>
|
||||
<Text style={styles.counterText}>
|
||||
{activeIndex + 1} / {mediaList.length}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Main image area – swipeable */}
|
||||
{hasCarousel ? (
|
||||
<FlatList
|
||||
ref={mainListRef}
|
||||
data={mediaList}
|
||||
renderItem={renderMainItem}
|
||||
keyExtractor={(item, i) => item.source_id || item.id?.toString() || i.toString()}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
onMomentumScrollEnd={onMainScroll}
|
||||
getItemLayout={(_, index) => ({
|
||||
length: SCREEN_WIDTH,
|
||||
offset: SCREEN_WIDTH * index,
|
||||
index,
|
||||
})}
|
||||
style={styles.mainList}
|
||||
/>
|
||||
) : (
|
||||
// Single image mode (backward compat)
|
||||
displayMedia.local_path ? (
|
||||
<Image source={{ uri: displayMedia.local_path }} style={styles.fullImage} resizeMode="contain" />
|
||||
) : (
|
||||
<View style={styles.fullPlaceholder}>
|
||||
<Text style={{ fontSize: 64 }}>📷</Text>
|
||||
</View>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Thumbnail carousel */}
|
||||
{hasCarousel && (
|
||||
<View style={styles.thumbCarousel}>
|
||||
<FlatList
|
||||
ref={thumbListRef}
|
||||
data={mediaList}
|
||||
renderItem={renderThumb}
|
||||
keyExtractor={(item, i) => 'thumb_' + (item.source_id || item.id?.toString() || i)}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.thumbContent}
|
||||
getItemLayout={(_, index) => ({
|
||||
length: THUMB_SIZE + THUMB_GAP,
|
||||
offset: (THUMB_SIZE + THUMB_GAP) * index,
|
||||
index,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Info panel */}
|
||||
<View style={[styles.infoPanel, { backgroundColor: theme.modalInfoBg }]}>
|
||||
<Text style={styles.title}>{media.title || 'Photo'}</Text>
|
||||
<Text style={styles.title}>{displayMedia.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)}
|
||||
{getServiceIcon(displayMedia.source_service)} {getServiceName(displayMedia.source_service)}
|
||||
</Text>
|
||||
</View>
|
||||
{media.created_date && (
|
||||
{displayMedia.created_date && (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>Date</Text>
|
||||
<Text style={styles.value}>
|
||||
{new Date(media.created_date).toLocaleDateString()}
|
||||
{new Date(displayMedia.created_date).toLocaleDateString()}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -58,8 +215,31 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
},
|
||||
closeText: { color: '#fff', fontSize: 20, fontWeight: '600' },
|
||||
counter: {
|
||||
position: 'absolute', top: 48, left: 0, right: 0, zIndex: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
counterText: { color: '#fff', fontSize: 14, fontWeight: '600' },
|
||||
mainList: { flex: 1 },
|
||||
fullImage: { flex: 1, width: '100%' },
|
||||
fullPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
// Thumbnail carousel
|
||||
thumbCarousel: {
|
||||
position: 'absolute', bottom: 110, left: 0, right: 0, zIndex: 15,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
paddingVertical: 8,
|
||||
},
|
||||
thumbContent: { paddingHorizontal: 12, gap: THUMB_GAP },
|
||||
thumbItem: {
|
||||
width: THUMB_SIZE, height: THUMB_SIZE, borderRadius: 6,
|
||||
overflow: 'hidden', borderWidth: 2, borderColor: 'transparent',
|
||||
},
|
||||
thumbImage: { width: '100%', height: '100%' },
|
||||
thumbPlaceholder: {
|
||||
width: '100%', height: '100%', backgroundColor: '#333',
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
},
|
||||
// Info panel
|
||||
infoPanel: {
|
||||
padding: 16, paddingBottom: 32,
|
||||
borderTopLeftRadius: 16, borderTopRightRadius: 16,
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
|
||||
/**
|
||||
* Circular health indicator dot
|
||||
*
|
||||
* status values:
|
||||
* 'inactive' → grey (not used / not connected)
|
||||
* 'healthy' → green (connected and working)
|
||||
* 'error' → red (connected but errors fetching data)
|
||||
* 'checking' → amber (currently checking)
|
||||
*/
|
||||
// Circular health indicator: inactive=grey, healthy=green, error=red, checking=amber
|
||||
const COLORS = {
|
||||
inactive: '#9e9e9e',
|
||||
healthy: '#34a853',
|
||||
|
||||
Reference in new issue
Block a user