114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
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<string> => {
|
|
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<string> => {
|
|
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<void> => {
|
|
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);
|
|
}
|
|
},
|
|
};
|