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:
@@ -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
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user