import { Ionicons } from '@expo/vector-icons'; import { File } from 'expo-file-system'; import * as Sharing from 'expo-sharing'; import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Modal, View, StyleSheet, TouchableOpacity, ScrollView, Dimensions } from 'react-native'; import ImageViewer from 'react-native-image-zoom-viewer'; import { useColors } from '@/hooks/useColorScheme'; import { ThemedText } from '@/components/themed/ThemedText'; import { ThemedView } from '@/components/themed/ThemedView'; import { useDialog } from '@/context/DialogContext'; type FilePreviewModalProps = { visible: boolean; /** * Close modal handler. */ onClose: () => void; fileName: string; filePath: string; fileExtension: string; }; const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); /** * File preview modal component for displaying images and text files. */ export const FilePreviewModal: React.FC = ({ visible, onClose, fileName, filePath, fileExtension, }) => { const [fileContent, setFileContent] = useState(''); const [loading, setLoading] = useState(true); const [fileSize, setFileSize] = useState(''); const colors = useColors(); const { t } = useTranslation(); const { showAlert } = useDialog(); const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; const textExtensions = ['txt', 'md', 'json', 'csv', 'log', 'xml', 'js', 'ts', 'tsx', 'jsx', 'html', 'css']; const isImage = imageExtensions.includes(fileExtension.toLowerCase()); const isText = textExtensions.includes(fileExtension.toLowerCase()); /** * Get file size formatted string. */ const getFileSize = useCallback((): void => { try { const file = new File(filePath); if (file.exists) { const size = file.size; if (size < 1024) { setFileSize(`${size} B`); } else if (size < 1024 * 1024) { setFileSize(`${(size / 1024).toFixed(1)} KB`); } else { setFileSize(`${(size / (1024 * 1024)).toFixed(1)} MB`); } } } catch (error) { console.error('Error getting file size:', error); } }, [filePath]); /** * Load text file content. */ const loadTextFile = useCallback(async (): Promise => { try { setLoading(true); const content = await new File(filePath).text(); setFileContent(content); } catch (error) { console.error('Error reading text file:', error); showAlert('Error', 'Could not read file content'); setFileContent('Error loading file content'); } finally { setLoading(false); } }, [filePath, showAlert]); useEffect(() => { if (visible) { getFileSize(); if (isText) { loadTextFile(); } else { setLoading(false); } } }, [visible, filePath, isText, loadTextFile, getFileSize]); /** * Download file to system. */ const downloadFile = async (): Promise => { try { const canShare = await Sharing.isAvailableAsync(); if (canShare) { await Sharing.shareAsync(filePath, { dialogTitle: `Save ${fileName}`, }); } else { showAlert(t('common.success'), `${t('items.fileSavedTo')}: ${filePath}`); } } catch (error) { console.error('Error downloading file:', error); showAlert('Error', 'Failed to download file'); } }; const styles = StyleSheet.create({ closeButton: { alignItems: 'center', backgroundColor: colors.accentBackground, borderRadius: 20, height: 40, justifyContent: 'center', width: 40, }, downloadButton: { alignItems: 'center', backgroundColor: colors.accentBackground, borderRadius: 20, height: 40, justifyContent: 'center', marginRight: 12, width: 40, }, fileInfo: { flex: 1, marginRight: 12, }, fileName: { fontSize: 16, fontWeight: '600', marginBottom: 2, }, fileSize: { fontSize: 12, opacity: 0.7, }, header: { alignItems: 'center', backgroundColor: colors.background, borderBottomColor: colors.text + '20', borderBottomWidth: 1, flexDirection: 'row', paddingBottom: 12, paddingHorizontal: 20, paddingTop: 60, }, headerButtons: { flexDirection: 'row', }, image: { height: screenHeight * 0.8, resizeMode: 'contain', width: screenWidth, }, loadingText: { fontSize: 16, textAlign: 'center', }, modalView: { backgroundColor: colors.background, flex: 1, }, scrollContainer: { flexGrow: 1, }, textContent: { fontFamily: 'monospace', fontSize: 13, lineHeight: 20, padding: 20, }, textScrollView: { flex: 1, }, unsupportedContainer: { alignItems: 'center', flex: 1, justifyContent: 'center', padding: 40, }, unsupportedSubtext: { fontSize: 14, opacity: 0.7, textAlign: 'center', }, unsupportedText: { fontSize: 16, marginBottom: 16, textAlign: 'center', }, }); /** * Render the file content based on file type. */ const renderContent = (): React.ReactNode => { if (loading) { return ( {t('common.loading')}... ); } if (isImage) { const imageUrls = [{ url: filePath }]; return ( ); } if (isText) { return ( {fileContent} ); } return ( {t('items.previewNotSupported')} {t('items.downloadToView')} ); }; return ( {fileName} {fileSize ? ( {fileSize} ) : null} {renderContent()} ); };