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 }) => (
{item.local_path ? (
) : (
๐ท
)}
);
const renderThumb = ({ item, index }) => (
onThumbPress(index)}
activeOpacity={0.8}
>
{item.local_path ? (
) : (
๐ท
)}
);
const displayMedia = currentMedia || media;
return (
{/* Close button */}
โ
{/* Counter */}
{hasCarousel && (
{activeIndex + 1} / {mediaList.length}
)}
{/* Main image area โ swipeable */}
{hasCarousel ? (
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 ? (
) : (
๐ท
)
)}
{/* Thumbnail carousel */}
{hasCarousel && (
'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,
})}
/>
)}
{/* Info panel */}
{displayMedia.title || 'Photo'}
Source
{displayMedia.sources && displayMedia.sources.length > 1
? displayMedia.sources.map((s) => `${getServiceIcon(s)} ${getServiceName(s)}`).join(' ยท ')
: `${getServiceIcon(displayMedia.source_service)} ${getServiceName(displayMedia.source_service)}`
}
{displayMedia.sources && displayMedia.sources.length > 1 && (
Available on
{displayMedia.sources.length} services
)}
{displayMedia.created_date && (
Date
{new Date(displayMedia.created_date).toLocaleDateString()}
)}
);
};
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;