mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-08 03:40:20 +00:00
266 lines
9.2 KiB
JavaScript
266 lines
9.2 KiB
JavaScript
import React, { useRef, useState, useEffect } 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) =>
|
||
({ DEVICE: '📱', GOOGLE_PHOTOS: '📷', ICLOUD_PHOTOS: '🍎', NAS: '💾' }[type] || '📱');
|
||
const getServiceName = (type) =>
|
||
({ DEVICE: 'On Device', GOOGLE_PHOTOS: 'Google Photos', ICLOUD_PHOTOS: 'iCloud', NAS: 'NAS' }[type] || type);
|
||
|
||
/**
|
||
* 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 activeIndexRef = useRef(0);
|
||
|
||
const onMainScroll = (event) => {
|
||
const offsetX = event.nativeEvent.contentOffset.x;
|
||
const idx = Math.round(offsetX / SCREEN_WIDTH);
|
||
if (idx >= 0 && idx < mediaList.length && idx !== activeIndexRef.current) {
|
||
activeIndexRef.current = idx;
|
||
setActiveIndex(idx);
|
||
setCurrentMedia(mediaList[idx]);
|
||
if (thumbListRef.current) {
|
||
try {
|
||
thumbListRef.current.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 });
|
||
} catch (_) { /* ignore */ }
|
||
}
|
||
}
|
||
};
|
||
|
||
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>
|
||
|
||
{/* 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}>{displayMedia.title || 'Photo'}</Text>
|
||
<View style={styles.row}>
|
||
<Text style={styles.label}>Source</Text>
|
||
<Text style={styles.value}>
|
||
{displayMedia.sources && displayMedia.sources.length > 1
|
||
? displayMedia.sources.map((s) => `${getServiceIcon(s)} ${getServiceName(s)}`).join(' · ')
|
||
: `${getServiceIcon(displayMedia.source_service)} ${getServiceName(displayMedia.source_service)}`
|
||
}
|
||
</Text>
|
||
</View>
|
||
{displayMedia.sources && displayMedia.sources.length > 1 && (
|
||
<View style={styles.row}>
|
||
<Text style={styles.label}>Available on</Text>
|
||
<Text style={[styles.value, { color: '#8ab4f8' }]}>
|
||
{displayMedia.sources.length} services
|
||
</Text>
|
||
</View>
|
||
)}
|
||
{displayMedia.created_date && (
|
||
<View style={styles.row}>
|
||
<Text style={styles.label}>Date</Text>
|
||
<Text style={styles.value}>
|
||
{new Date(displayMedia.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' },
|
||
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,
|
||
},
|
||
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;
|
||
|