import React, { useState, useEffect, useRef, useCallback } from 'react'; import { View, ScrollView, Image, TouchableOpacity, StyleSheet, Modal, Alert, SafeAreaView, Dimensions, Platform, Animated as RNAnimated, } from 'react-native'; import * as Haptics from 'expo-haptics'; // Import zoom toolkit for native platforms let Gallery: any = null; let ResumableZoom: any = null; let GestureHandlerRootView: any = null; if (Platform.OS !== 'web') { try { const zoomToolkit = require('react-native-zoom-toolkit'); Gallery = zoomToolkit.Gallery; ResumableZoom = zoomToolkit.ResumableZoom; // Import GestureHandlerRootView const gestureHandler = require('react-native-gesture-handler'); GestureHandlerRootView = gestureHandler.GestureHandlerRootView; } catch (error) { console.warn('react-native-zoom-toolkit not available:', error); } } const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); import { useTheme } from '~/features/theme/ThemeProvider'; import { MemoPhoto } from '~/features/storage/storage.types'; import Text from '~/components/atoms/Text'; import Icon from '~/components/atoms/Icon'; import { useTranslation } from 'react-i18next'; interface PhotoGalleryProps { memoId: string; photos: MemoPhoto[]; onPhotosChange?: (photos: MemoPhoto[]) => void; onPhotoDelete?: (photoPath: string) => void; onAddPhotoPress?: () => void; editable?: boolean; showAddButton?: boolean; loading?: boolean; } interface PhotoViewerModalProps { visible: boolean; photos: MemoPhoto[]; initialIndex: number; onClose: () => void; onDelete?: (photoPath: string) => void; editable?: boolean; } function PhotoViewerModal({ visible, photos, initialIndex, onClose, onDelete, editable, }: PhotoViewerModalProps) { const { isDark } = useTheme(); const { t } = useTranslation(); const [currentIndex, setCurrentIndex] = useState(initialIndex); const [showMenuElements, setShowMenuElements] = useState(true); const [autoHideEnabled, setAutoHideEnabled] = useState(true); const fadeAnim = useRef(new RNAnimated.Value(1)).current; const hideMenuTimer = useRef(null); // Reset current index when initialIndex changes useEffect(() => { setCurrentIndex(initialIndex); }, [initialIndex]); // Auto-hide menu elements after 3 seconds useEffect(() => { if (visible && autoHideEnabled) { setShowMenuElements(true); // Clear any existing timer if (hideMenuTimer.current) { clearTimeout(hideMenuTimer.current); } // Set new timer hideMenuTimer.current = setTimeout(() => { setShowMenuElements(false); }, 3000); return () => { if (hideMenuTimer.current) { clearTimeout(hideMenuTimer.current); } }; } }, [visible, autoHideEnabled]); const toggleMenuElements = useCallback(() => { const newShowState = !showMenuElements; setShowMenuElements(newShowState); setAutoHideEnabled(false); // Disable auto-hide after manual toggle // Clear any existing timer if (hideMenuTimer.current) { clearTimeout(hideMenuTimer.current); } RNAnimated.timing(fadeAnim, { toValue: newShowState ? 1 : 0, duration: 300, useNativeDriver: true, }).start(); }, [showMenuElements, fadeAnim]); // Animate menu elements visibility useEffect(() => { RNAnimated.timing(fadeAnim, { toValue: showMenuElements ? 1 : 0, duration: 300, useNativeDriver: true, }).start(); }, [showMenuElements, fadeAnim]); const currentPhoto = photos[currentIndex] || null; const handleDelete = () => { if (!currentPhoto || !onDelete) return; Alert.alert( t('memo.delete_photo_title', 'Foto löschen'), t('memo.delete_photo_message', 'Möchten Sie dieses Foto wirklich löschen?'), [ { text: t('common.cancel', 'Abbrechen'), style: 'cancel', }, { text: t('common.delete', 'Löschen'), style: 'destructive', onPress: () => { onDelete(currentPhoto.path); if (photos.length <= 1) { onClose(); } else if (currentIndex >= photos.length - 1) { setCurrentIndex(Math.max(0, currentIndex - 1)); } }, }, ] ); }; if (!currentPhoto || photos.length === 0) return null; // Use Gallery component for native platforms if (Platform.OS !== 'web' && Gallery && ResumableZoom) { // Debug logging console.log('Gallery photos:', photos); console.log('Gallery initialIndex:', initialIndex); console.log('Current photo:', currentPhoto); return ( {/* Header */} {currentPhoto.filename} ({currentIndex + 1}/{photos.length}) {editable && onDelete && ( )} {/* Gallery Component */} item?.path || index.toString()} initialIndex={initialIndex} onIndexChange={(index) => { console.log('Gallery index changed to:', index); setCurrentIndex(index); }} renderItem={(photo) => { console.log('Gallery renderItem called with photo:', photo); // Safety check for photo if (!photo || !photo.signedUrl) { console.log('No photo or signedUrl, showing placeholder'); return ( {t('memo.image_not_available', 'Bild nicht verfügbar')} ); } console.log('Rendering image with URL:', photo.signedUrl); return ( console.log('Loading image:', photo.path)} onLoad={() => console.log('Image loaded:', photo.path)} onError={(e) => console.error('Image error:', photo.path, e)} /> ); }} onTap={toggleMenuElements} onDoubleTap={() => {}} // Handled by ResumableZoom pinchCenteringMode="sync" style={{ flex: 1 }} /> {/* Footer */} {t('memo.uploaded_at', 'Hochgeladen am: {{date}}', { date: new Date(currentPhoto.uploadedAt).toLocaleDateString(), })} {currentPhoto.fileSize && ( {t('memo.file_size', 'Größe: {{size}}', { size: (currentPhoto.fileSize / 1024 / 1024).toFixed(2) + ' MB', })} )} ); } // Fallback for web - simple image viewer return ( {/* Header */} {currentPhoto.filename} ({currentIndex + 1}/{photos.length}) {editable && onDelete && ( )} {/* Simple Image Display for Web */} { const newIndex = Math.round(event.nativeEvent.contentOffset.x / screenWidth); setCurrentIndex(newIndex); }} contentOffset={{ x: initialIndex * screenWidth, y: 0 }}> {photos.map((photo, index) => ( {photo.signedUrl ? ( ) : ( {t('memo.image_not_available', 'Bild nicht verfügbar')} )} ))} {/* Footer */} {t('memo.uploaded_at', 'Hochgeladen am: {{date}}', { date: new Date(currentPhoto.uploadedAt).toLocaleDateString(), })} {currentPhoto.fileSize && ( {t('memo.file_size', 'Größe: {{size}}', { size: (currentPhoto.fileSize / 1024 / 1024).toFixed(2) + ' MB', })} )} ); } export default function PhotoGallery({ memoId, photos, onPhotosChange, onPhotoDelete, onAddPhotoPress, editable = true, showAddButton = false, loading = false, }: PhotoGalleryProps) { const { isDark, themeVariant } = useTheme(); const { t } = useTranslation(); const [viewerVisible, setViewerVisible] = useState(false); const [initialPhotoIndex, setInitialPhotoIndex] = useState(0); const handlePhotoPress = (photo: MemoPhoto) => { const photoIndex = photos.findIndex((p) => p.path === photo.path); setInitialPhotoIndex(photoIndex >= 0 ? photoIndex : 0); setViewerVisible(true); }; const handleCloseViewer = () => { setViewerVisible(false); }; const handleDeletePhoto = (photoPath: string) => { if (onPhotoDelete) { onPhotoDelete(photoPath); } // Update local state if onPhotosChange is provided if (onPhotosChange) { const updatedPhotos = photos.filter((photo) => photo.path !== photoPath); onPhotosChange(updatedPhotos); } }; const handleDeletePhotoWithConfirmation = (photoPath: string) => { Alert.alert( t('memo.delete_photo_title', 'Foto löschen'), t('memo.delete_photo_message', 'Möchten Sie dieses Foto wirklich löschen?'), [ { text: t('common.cancel', 'Abbrechen'), style: 'cancel', }, { text: t('common.delete', 'Löschen'), style: 'destructive', onPress: () => handleDeletePhoto(photoPath), }, ] ); }; // Haptic feedback for long press const triggerLongPressHaptic = async () => { try { await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); } catch (error) { console.debug('Haptic feedback error:', error); } }; // Get photo menu items for Zeego DropdownMenu const getPhotoMenuItems = (photo: MemoPhoto) => { const menuItems = [ { key: 'preview', title: t('memo.preview_photo', 'Vorschau anzeigen'), systemIcon: 'eye', onSelect: () => handlePhotoPress(photo), } ]; if (editable) { menuItems.push({ key: 'delete', title: t('common.delete', 'Löschen'), systemIcon: 'trash', destructive: true, onSelect: () => handleDeletePhotoWithConfirmation(photo.path), }); } return menuItems; }; return ( {photos.length}{' '} {photos.length === 1 ? t('memo.photo_singular', 'Photo') : t('memo.photos_section', 'Fotos')} {/* Existing Photos */} {photos.map((photo) => { const photoMenuItems = getPhotoMenuItems(photo); // Photo thumbnail content with long-press handler const handlePhotoLongPress = () => { triggerLongPressHaptic(); // Show action sheet for native platforms if (Platform.OS !== 'web' && photoMenuItems.length > 0) { const buttons = photoMenuItems.map(item => ({ text: item.title, style: item.destructive ? 'destructive' as const : 'default' as const, onPress: item.onSelect, })); buttons.push({ text: t('cancel'), style: 'cancel' }); Alert.alert( t('photo_options', 'Photo Options'), undefined, buttons ); } }; const thumbnailContent = ( handlePhotoPress(photo)} onLongPress={handlePhotoLongPress} activeOpacity={0.8}> {photo.signedUrl ? ( ) : ( )} ); // Return the thumbnail directly (context menu functionality moved to long-press) return {thumbnailContent}; })} {/* Add Photo Button at the end */} {editable && ( console.log('Add photo pressed but no handler provided')) } disabled={loading || !onAddPhotoPress} activeOpacity={0.8}> {loading ? ( <> {t('memo.uploading', 'Wird hochgeladen...')} ) : ( <> {t('memo.add_photos', 'Fotos hinzufügen')} )} )} ); } const styles = StyleSheet.create({ fullWidthContainer: { marginHorizontal: -20, // Optimiertes negatives Margin für perfekte Ausrichtung marginTop: 16, // Erhöhter Abstand nach oben }, header: { marginBottom: 12, paddingLeft: 16, }, title: { fontSize: 16, fontWeight: '600', }, scrollContainer: { // ScrollView nimmt volle Breite ein }, scrollContent: { paddingLeft: 16, paddingRight: 16, gap: 16, }, photoThumbnail: { width: 120, height: 120, borderRadius: 12, borderWidth: 1, overflow: 'hidden', position: 'relative', }, addPhotoButton: { width: 120, height: 120, borderRadius: 12, borderWidth: 1, borderStyle: 'dashed', justifyContent: 'center', alignItems: 'center', marginRight: 12, }, addPhotoText: { fontSize: 12, textAlign: 'center', marginTop: 6, fontWeight: '500', }, addPhotoThumbnail: { width: 120, height: 120, borderRadius: 12, borderWidth: 1, borderStyle: 'dashed', justifyContent: 'center', alignItems: 'center', overflow: 'hidden', }, addPhotoThumbnailText: { fontSize: 11, textAlign: 'center', marginTop: 6, fontWeight: '500', lineHeight: 13, }, thumbnailImage: { width: '100%', height: '100%', }, placeholderThumbnail: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', }, // Modal styles modalOverlay: { flex: 1, }, modalSafeArea: { flex: 1, }, modalHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12, zIndex: 10, }, modalTitle: { fontSize: 16, fontWeight: '600', flex: 1, marginRight: 16, }, modalHeaderActions: { flexDirection: 'row', gap: 16, }, modalHeaderButton: { padding: 8, }, modalContent: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16, }, modalImageContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', position: 'relative', }, photoCarousel: { flex: 1, }, photoSlide: { flex: 1, justifyContent: 'center', alignItems: 'center', position: 'relative', }, galleryContainer: { flex: 1, backgroundColor: 'transparent', }, webImage: { width: '100%', height: '100%', }, modalImage: { backgroundColor: 'transparent', }, imageWrapper: { flex: 1, justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%', }, placeholderContainer: { width: 200, height: 200, borderRadius: 12, justifyContent: 'center', alignItems: 'center', }, modalFooter: { paddingHorizontal: 16, paddingVertical: 12, paddingBottom: 20, }, imageInfo: { fontSize: 14, textAlign: 'center', marginVertical: 2, }, // Loading styles loadingContainer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', zIndex: 10, }, loadingText: { color: '#FFFFFF', fontSize: 16, marginTop: 16, textAlign: 'center', }, });