import { FoodLogRepository } from '@/add-log/repository'; import { STORAGE_KEYS } from '@/common/constants/storageKeys'; import { useProfile } from '@/profile/ProfileProvider'; import { useDatabase } from '@/remote/database'; import type { FoodLog } from '@/types/foodlog'; import NetInfo from '@react-native-community/netinfo'; import * as FileSystem from 'expo-file-system'; import * as SecureStore from 'expo-secure-store'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:4321'; const PHOTOS_DIR = `${FileSystem.documentDirectory}user_camera_photos/`; async function ensurePhotosDir(profileId: string): Promise { const dir = `${PHOTOS_DIR}${profileId}/`; const info = await FileSystem.getInfoAsync(dir); if (!info.exists) { await FileSystem.makeDirectoryAsync(dir, { intermediates: true }); } return dir; } async function getSignedUrl(path: string): Promise { const token = await SecureStore.getItemAsync(STORAGE_KEYS.SESSION_TOKEN); if (!token) return null; const response = await fetch( `${API_BASE_URL}/api/sync/photo-url?path=${encodeURIComponent(path)}`, { headers: { Authorization: `Bearer ${token}` } } ); if (!response.ok) return null; const data = await response.json(); return data.success ? data.signedUrl : null; } async function downloadPhoto(signedUrl: string, localPath: string): Promise { try { const result = await FileSystem.downloadAsync(signedUrl, localPath); return result.status === 200; } catch { return false; } } type PhotoState = { uri: string | null; loading: boolean; }; export function useFoodLogPhoto(log: FoodLog | undefined): PhotoState { const { profile } = useProfile(); const db = useDatabase(); const foodLogRepo = useMemo(() => new FoodLogRepository(db), [db]); const [state, setState] = useState({ uri: null, loading: false }); const downloadingRef = useRef(false); const loadPhoto = useCallback(async () => { if (!log || !profile?.id) { setState({ uri: null, loading: false }); return; } let localUri: string | null = null; try { localUri = await foodLogRepo.getLocalPhotoUri(log.id); } catch { /* ignore */ } if (localUri) { setState({ uri: localUri, loading: false }); return; } if (!log.remotePhotoPath) { setState({ uri: null, loading: false }); return; } const localPath = `${PHOTOS_DIR}${log.remotePhotoPath}`; const localInfo = await FileSystem.getInfoAsync(localPath); if (localInfo.exists) { await foodLogRepo.updatePhotoUri(log.id, localPath); setState({ uri: localPath, loading: false }); return; } const netState = await NetInfo.fetch(); if (!netState.isConnected) { setState({ uri: null, loading: false }); return; } if (downloadingRef.current) return; downloadingRef.current = true; setState(prev => ({ ...prev, loading: true })); try { const signedUrl = await getSignedUrl(log.remotePhotoPath); if (!signedUrl) { setState({ uri: null, loading: false }); return; } setState({ uri: signedUrl, loading: true }); await ensurePhotosDir(profile.id); const success = await downloadPhoto(signedUrl, localPath); if (success) { await foodLogRepo.updatePhotoUri(log.id, localPath); setState({ uri: localPath, loading: false }); } else { setState({ uri: signedUrl, loading: false }); } } catch { setState({ uri: null, loading: false }); } finally { downloadingRef.current = false; } }, [log?.id, log?.remotePhotoPath, profile?.id, foodLogRepo]); useEffect(() => { loadPhoto(); }, [loadPhoto]); return state; }