fix: fresh export URI per save (stale thumbnail), lock spot-meter + quick EV slider, honor photo GPS over device fallback

This commit is contained in:
2026-09-08 10:42:08 +07:00
parent 8eca31ce58
commit 358f9b0f2f
4 changed files with 150 additions and 20 deletions
+24 -8
View File
@@ -60,9 +60,13 @@ function screenSharpenImage(image: SkImage, amount = 0.5): SkImage {
return sharpened;
}
// Fixed filename: every export overwrites the previous one, so temp files
// never accumulate in the cache directory.
const EXPORT_FILE = `${FileSystem.cacheDirectory}camrecipe_pro_last_export.jpg`;
// Unique filename per export. A fixed path made every export share one URI, and
// RN's <Image> cache (keyed by URI) then kept showing the previous photo in the
// thumbnail after a new capture overwrote the file. A fresh URI per export busts
// that cache. Previous export file is deleted after the new one is written and
// gallery-saved, so at most one stale temp file can ever exist.
let lastExportPath: string | null = null;
const nextExportFile = () => `${FileSystem.cacheDirectory}camrecipe_pro_export_${Date.now()}.jpg`;
export async function processAndExportPhoto(
sourceUri: string,
@@ -299,9 +303,12 @@ export async function processAndExportPhoto(
bytes = patchJpegDpi(bytes, options.dpi);
}
// 10. Write binary bytes to temporary local file (overwrites previous export)
// 10. Write binary bytes to a unique temporary local file. The unique name
// is load-bearing: a fixed URI would be served from RN's <Image> cache and
// the thumbnail/preview would keep showing the previous export.
const exportFile = nextExportFile();
try {
new File(EXPORT_FILE).write(bytes);
new File(exportFile).write(bytes);
} catch (e) {
console.error('Failed to write export file: ' + e);
return null;
@@ -313,8 +320,17 @@ export async function processAndExportPhoto(
const mediaPermission = await MediaLibrary.requestPermissionsAsync();
if (mediaPermission.granted) {
try {
await MediaLibrary.saveToLibraryAsync(EXPORT_FILE);
return { uri: EXPORT_FILE, savedToLibrary: true };
await MediaLibrary.saveToLibraryAsync(exportFile);
// New file is safely in the gallery — retire the previous export temp.
if (lastExportPath && lastExportPath !== exportFile) {
try {
await FileSystem.deleteAsync(lastExportPath, { idempotent: true });
} catch (e) {
console.warn('Cleanup of previous export temp failed:', e);
}
}
lastExportPath = exportFile;
return { uri: exportFile, savedToLibrary: true };
} catch (e) {
console.warn('Save to gallery failed:', e);
}
@@ -323,7 +339,7 @@ export async function processAndExportPhoto(
}
console.error(`[export] gallery-save ${Date.now() - t0}ms`);
return { uri: EXPORT_FILE, savedToLibrary: false };
return { uri: exportFile, savedToLibrary: false };
} catch (error) {
console.error('Error during photo processing and export:', error);
return null;
+33 -7
View File
@@ -34,25 +34,42 @@ function toMediaLibraryId(assetId: string): string {
return m ? m[1] : assetId;
}
// ImagePicker's asset.assetId can be null on Android 13+ (system photo picker)
// while its uri is "content://media/external/images/media/<id>" — fall back to
// parsing the id out of the uri so MediaStore lookups still work.
function pickerAssetId(asset: PickedPhotoAsset): string | null {
if (asset.assetId) return asset.assetId;
const uri = asset.uri || '';
const m = uri.match(/\/media\/(\d+)\s*$/);
return m ? m[1] : null;
}
async function resolveFromMediaLibrary(
assetId: string,
asset: PickedPhotoAsset,
useGeotag: boolean
): Promise<{ gps: GPSInfo | null; permissionDenied: boolean }> {
let perm;
try {
perm = await MediaLibrary.requestPermissionsAsync(false, ['photo']);
} catch (e) {
console.warn('MediaLibrary permission request failed:', e);
console.error('[gps] tier2 MediaLibrary permission request failed: ' + String(e).slice(0, 120));
return { gps: null, permissionDenied: true };
}
if (!perm.granted) {
console.error('[gps] tier2 MediaLibrary permission denied');
return { gps: null, permissionDenied: true };
}
const id = pickerAssetId(asset);
if (!id) {
console.error('[gps] tier2 no assetId to query MediaStore');
return { gps: null, permissionDenied: false };
}
try {
const info = await getAssetInfoAsync(toMediaLibraryId(assetId));
const info = await getAssetInfoAsync(toMediaLibraryId(id));
if (info.location && Number.isFinite(info.location.latitude) && Number.isFinite(info.location.longitude)) {
const { latitude, longitude } = info.location;
const locality = useGeotag ? await localityFor(latitude, longitude) : undefined;
console.error(`[gps] tier2 MediaStore location lat=${latitude.toFixed(5)} lon=${longitude.toFixed(5)}`);
return {
gps: {
latitude,
@@ -65,8 +82,9 @@ async function resolveFromMediaLibrary(
permissionDenied: false,
};
}
console.error('[gps] tier2 MediaStore asset has no location (photo may have no GPS, or ACCESS_MEDIA_LOCATION read failed silently)');
} catch (e) {
console.warn('MediaLibrary getAssetInfoAsync failed:', e);
console.error('[gps] tier2 getAssetInfoAsync failed: ' + String(e).slice(0, 160));
}
return { gps: null, permissionDenied: false };
}
@@ -84,22 +102,30 @@ export async function resolvePhotoGPS(
if (exifCoord) {
const timestamp = parseExifTimestamp(asset.exif) ?? Date.now();
const locality = useGeotag ? await localityFor(exifCoord.latitude, exifCoord.longitude) : undefined;
console.error(`[gps] tier1 EXIF lat=${exifCoord.latitude.toFixed(5)} lon=${exifCoord.longitude.toFixed(5)}`);
return {
gps: { ...exifCoord, locality, country: undefined, timestamp, source: 'exif' },
};
}
console.error('[gps] tier1 no EXIF GPS in picker metadata');
// Tier 2 — MediaLibrary metadata (Android 10+ requires ACCESS_MEDIA_LOCATION)
if (asset.assetId) {
const { gps, permissionDenied } = await resolveFromMediaLibrary(asset.assetId, useGeotag);
const assetId = pickerAssetId(asset);
if (assetId) {
const { gps, permissionDenied } = await resolveFromMediaLibrary(asset, useGeotag);
if (gps) return { gps };
if (permissionDenied) return { gps: null, permissionDenied: true };
} else {
console.error('[gps] tier2 skipped — no asset id/uri');
}
// Tier 3 — device fallback (photo has no location; stamp the device's current spot)
if (allowDeviceFallback) {
const deviceGps = await getCurrentGPS();
if (deviceGps) return { gps: { ...deviceGps, source: 'device' } };
if (deviceGps) {
console.error(`[gps] tier3 DEVICE GPS fallback lat=${deviceGps.latitude.toFixed(5)} lon=${deviceGps.longitude.toFixed(5)}`);
return { gps: { ...deviceGps, source: 'device' } };
}
}
return { gps: null };