import React, { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { db } from '../services/db.js' import { getActiveMasterKey } from '../services/auth.js' import { getLogbookKey } from '../services/logbookKeys.js' import { decryptJson } from '../services/crypto.js' import { saveEntryPhoto, deleteEntryPhoto } from '../services/photoAttachments.js' import { fileToCompressedJpegDataUrl } from '../utils/imageCompress.js' import { useLiveQuery } from 'dexie-react-hooks' import { useDialog } from './ModalDialog.tsx' import { Camera, Trash2 } from 'lucide-react' interface PhotoCaptureProps { entryId: string logbookId: string readOnly?: boolean preloadedPhotos?: any[] } interface DecryptedPhoto { payloadId: string image: string caption: string updatedAt: string } export default function PhotoCapture({ entryId, logbookId, readOnly = false, preloadedPhotos }: PhotoCaptureProps) { const { t } = useTranslation() const { showConfirm } = useDialog() const [caption, setCaption] = useState('') const [uploading, setUploading] = useState(false) const [error, setError] = useState(null) const [decryptedPhotos, setDecryptedPhotos] = useState([]) const fileInputRef = useRef(null) // Reactively query local photos database const localPhotos = useLiveQuery( () => db.photos.where({ entryId }).toArray(), [entryId] ) // Decrypt photos on query updates useEffect(() => { async function decryptPhotosList() { if (readOnly && preloadedPhotos) { const filtered = preloadedPhotos .filter((p: any) => p.entryId === entryId) .map((p: any) => ({ payloadId: p.payloadId || p.id, image: p.image, caption: p.caption || '', updatedAt: p.updatedAt || new Date().toISOString() })) setDecryptedPhotos(filtered) return } if (!localPhotos) return const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) return const list: DecryptedPhoto[] = [] for (const p of localPhotos) { try { const decrypted = await decryptJson(p.encryptedData, p.iv, p.tag, masterKey) if (decrypted) { list.push({ payloadId: p.payloadId, image: decrypted.image, caption: decrypted.caption || '', updatedAt: p.updatedAt }) } } catch (e) { console.error('Failed to decrypt photo attachment:', e) } } setDecryptedPhotos(list) } decryptPhotosList() }, [localPhotos, readOnly, preloadedPhotos, entryId]) const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return setUploading(true) setError(null) try { const compressedBase64 = await fileToCompressedJpegDataUrl(file) await saveEntryPhoto({ logbookId, entryId, imageDataUrl: compressedBase64, caption: caption.trim(), analyticsContext: 'logbook' }) setCaption('') if (fileInputRef.current) fileInputRef.current.value = '' } catch (err: unknown) { console.error('Failed to process image:', err) setError(err instanceof Error ? err.message : 'Failed to process image') } finally { setUploading(false) } } const handleDelete = async (photoId: string) => { if (await showConfirm(t('logs.photo_delete_confirm'), t('logs.photos_title'), t('logs.confirm_yes'), t('logs.confirm_no'))) { try { await deleteEntryPhoto(logbookId, photoId) } catch (err: unknown) { console.error('Failed to delete photo:', err) } } } const triggerSelect = () => { if (fileInputRef.current) { fileInputRef.current.click() } } return (

{t('logs.photos_title')}

{error &&
{error}
} {/* Upload area */} {/* Upload Form */} {!readOnly && (
setCaption(e.target.value)} disabled={uploading} />
)} {/* Photo Grid */} {decryptedPhotos.length === 0 ? (
{t('logs.no_photos')}
) : (
{decryptedPhotos.map((photo) => (
{photo.caption {!readOnly && ( )}
{photo.caption && (
{photo.caption}
)}
))}
)}
) }