import { useEffect, useState, useRef } from 'react' import { RefreshCw, Camera, Image, Plus, X } from 'lucide-react' import { normalizeImageFiles } from '../../utils/convertHeic' import { useJourneyStore } from '../../store/journeyStore' import { useTranslation } from '../../i18n' import { journeyApi, addonsApi } from '../../api/client' import { useToast } from '../shared/Toast' import { getApiErrorMessage } from '../../types' import type { JourneyEntry, GalleryPhoto, JourneyTrip } from '../../store/journeyStore' import { photoUrl } from '../../pages/journeyDetail/JourneyDetailPage.helpers' import { ProviderPicker } from './JourneyDetailPageProviderPicker' export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick, onRefresh }: { entries: JourneyEntry[] gallery: GalleryPhoto[] journeyId: number userId: number trips: JourneyTrip[] onPhotoClick: (photos: GalleryPhoto[], index: number) => void onRefresh: () => void }) { const { t } = useTranslation() const [showPicker, setShowPicker] = useState(false) const [pickerProvider, setPickerProvider] = useState(null) const [availableProviders, setAvailableProviders] = useState<{ id: string; name: string }[]>([]) const [galleryProgress, setGalleryProgress] = useState<{ done: number; total: number } | null>(null) const galleryUploading = galleryProgress !== null const toast = useToast() // check which providers are enabled AND connected for the current user useEffect(() => { (async () => { try { const addonsData = await addonsApi.enabled() const enabledProviders = (addonsData.addons || []).filter( (a: any) => a.type === 'photo_provider' && a.enabled ) const connected: { id: string; name: string }[] = [] for (const p of enabledProviders) { try { const res = await fetch(`/api/integrations/memories/${p.id}/status`, { credentials: 'include' }) if (res.ok) { const status = await res.json() if (status.connected) connected.push({ id: p.id, name: p.name }) } } catch {} } setAvailableProviders(connected) } catch {} })() }, []) const allPhotos = gallery const entriesWithContent = entries.filter(e => e.type !== 'skeleton' || e.title) const browseProvider = (provider: string) => { setPickerProvider(provider) setShowPicker(true) } const galleryFileRef = useRef(null) const handleGalleryUpload = async (e: React.ChangeEvent) => { const files = e.target.files if (!files?.length) return setGalleryProgress({ done: 0, total: files.length }) try { const normalized = await normalizeImageFiles(files) const { failed } = await useJourneyStore.getState().uploadGalleryPhotos(journeyId, normalized, { onProgress: p => setGalleryProgress({ done: p.done, total: p.total }), }) if (failed.length > 0) { toast.error(t('journey.editor.uploadPartialFailed', { failed: String(failed.length), total: String(normalized.length) })) } else { toast.success(t('journey.photosUploaded', { count: String(files.length) })) } onRefresh() } catch (err) { toast.error(getApiErrorMessage(err, t('journey.photosUploadFailed'))) } finally { setGalleryProgress(null) } e.target.value = '' } const handleDeletePhoto = async (galleryPhotoId: number) => { const store = useJourneyStore.getState() if (!store.current) return // Optimistic update — remove from gallery and all entry photo lists useJourneyStore.setState({ current: { ...store.current, gallery: (store.current.gallery || []).filter(p => p.id !== galleryPhotoId), entries: store.current.entries.map(e => ({ ...e, photos: e.photos.filter(p => p.id !== galleryPhotoId), })), }, }) try { await journeyApi.deleteGalleryPhoto(journeyId, galleryPhotoId) } catch { toast.error(t('common.error')) onRefresh() } } return (
{/* Header */}
{allPhotos.length} {t('journey.detail.photos')}
{availableProviders.map(p => ( ))}
{allPhotos.length === 0 ? (

{t('journey.detail.noPhotos')}

{t('journey.detail.noPhotosHint')}

) : (
{allPhotos.map((photo, i) => (
onPhotoClick(allPhotos, i)} > {photo.caption
{/* Delete button */} {photo.provider && photo.provider !== 'local' && (
{photo.provider === 'immich' ? 'Immich' : photo.provider === 'synology' ? 'Synology' : photo.provider}
)} {photo.caption && (

{photo.caption}

)}
))}
)} {/* Provider Photo Picker Modal */} {showPicker && ( p.asset_id).map(p => p.asset_id!))} onClose={() => setShowPicker(false)} onAdd={async (groups, entryId) => { let added = 0 let anyFailed = false for (const group of groups) { try { if (entryId) { const result = await journeyApi.addProviderPhotos(entryId, pickerProvider!, group.assetIds, undefined, group.passphrase) added += result.added || 0 } else { const result = await journeyApi.addProviderPhotosToGallery(journeyId, pickerProvider!, group.assetIds, group.passphrase) added += result.added || 0 } } catch { anyFailed = true } } if (added > 0) { toast.success(t('journey.photosAdded', { count: added })) onRefresh() } else if (anyFailed) { toast.error(t('common.error')) } setShowPicker(false) }} /> )}
) }