import * as FileSystemLegacy from 'expo-file-system/legacy'; const getCacheDir = (): string => { const baseDir = FileSystemLegacy.documentDirectory ?? FileSystemLegacy.cacheDirectory; if (!baseDir) { throw new Error('No writable file system directory is available for image caching.'); } return `${baseDir}plant-images/`; }; const ensureCacheDir = async (): Promise => { const cacheDir = getCacheDir(); const dirInfo = await FileSystemLegacy.getInfoAsync(cacheDir); if (!dirInfo.exists) { await FileSystemLegacy.makeDirectoryAsync(cacheDir, { intermediates: true }); } return cacheDir; }; const hashString = (value: string): string => { // FNV-1a 32-bit hash for stable cache file names. let hash = 2166136261; for (let index = 0; index < value.length; index += 1) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0).toString(36); }; const getDataUriExtension = (uri: string): string => { const mimeMatch = uri.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,/i); const mimeType = mimeMatch?.[1]?.toLowerCase() || ''; if (mimeType.includes('png')) return 'png'; if (mimeType.includes('webp')) return 'webp'; if (mimeType.includes('gif')) return 'gif'; if (mimeType.includes('heic')) return 'heic'; if (mimeType.includes('heif')) return 'heif'; return 'jpg'; }; const getUriExtension = (uri: string): string => { const cleanPath = uri.split(/[?#]/)[0]; const extensionMatch = cleanPath.match(/\.([a-zA-Z0-9]+)$/); return extensionMatch?.[1]?.toLowerCase() || 'jpg'; }; const getFileName = (uri: string): string => { const extension = uri.startsWith('data:') ? getDataUriExtension(uri) : getUriExtension(uri); return `${hashString(uri)}.${extension}`; }; export const ImageCacheService = { /** * Check if an image is already cached locally. */ isCached: async (uri: string): Promise<{ exists: boolean; localUri: string }> => { const cacheDir = await ensureCacheDir(); const fileName = getFileName(uri); const localUri = `${cacheDir}${fileName}`; const info = await FileSystemLegacy.getInfoAsync(localUri); return { exists: info.exists, localUri }; }, /** * Cache an image (base64 data URI or remote URL) and return the local file path. */ cacheImage: async (uri: string): Promise => { const cacheDir = await ensureCacheDir(); const fileName = getFileName(uri); const localUri = `${cacheDir}${fileName}`; const info = await FileSystemLegacy.getInfoAsync(localUri); const exists = info.exists; if (exists) return localUri; if (uri.startsWith('data:')) { // Extract base64 content after the comma const base64Data = uri.split(',')[1]; if (!base64Data) throw new Error('Invalid base64 data URI'); await FileSystemLegacy.writeAsStringAsync(localUri, base64Data, { encoding: FileSystemLegacy.EncodingType.Base64, }); } else if (/^(file:\/\/|content:\/\/)/i.test(uri)) { await FileSystemLegacy.copyAsync({ from: uri, to: localUri }); } else { // Remote URL - download it const downloadResult = await FileSystemLegacy.downloadAsync(uri, localUri); if (downloadResult.status !== 200) { throw new Error(`Failed to download image: HTTP ${downloadResult.status}`); } } return localUri; }, /** * Delete a cached image by its local path. */ deleteCachedImage: async (localUri: string): Promise => { try { const info = await FileSystemLegacy.getInfoAsync(localUri); if (info.exists) { await FileSystemLegacy.deleteAsync(localUri, { idempotent: true }); } } catch (e) { console.error('Failed to delete cached image', e); } }, };