Initial commit for Greenlens
This commit is contained in:
113
services/imageCacheService.ts
Normal file
113
services/imageCacheService.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user