Zoom the FRAME preview instead of the photo, and save to DCIM/Camera
- FRAME tab now pinches the whole view (frame + photo + watermark) in both camera and library; one finger pans that view. Preview only: the exported file is identical at 1x and while the view is magnified. - Watermark pinch/drag stay relative and keep priority while the WATERMARK row is open, so they still work after returning from the FRAME tab. - Exports go through a native MediaStore insert into DCIM/Camera, falling back to expo-media-library when the native save fails.
This commit is contained in:
+41
@@ -1,5 +1,6 @@
|
||||
package com.locphamtran.recipescamera.export
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
@@ -10,6 +11,8 @@ import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import expo.modules.kotlin.functions.Coroutine
|
||||
@@ -74,6 +77,44 @@ class RecipescamExportModule : Module() {
|
||||
Name("RecipesCamExport")
|
||||
Log.i(TAG, "module defined RecipesCamExport")
|
||||
|
||||
// Save an encoded JPEG/PNG into DCIM/Camera. expo-media-library's JS API
|
||||
// only ever inserts into DCIM (root) or Pictures/Camera, so a camera-roll
|
||||
// export lands where the stock camera puts its shots by inserting the
|
||||
// MediaStore row ourselves with an explicit RELATIVE_PATH. The file is
|
||||
// staged IS_PENDING, copied from the cache path, then published. Returns the
|
||||
// MediaStore _ID (same shape as expo's Asset.id, usable with
|
||||
// deleteAssetsAsync), or "" when the insert/copy fails so JS can fall back.
|
||||
AsyncFunction("saveToCameraRollAsync") Coroutine { srcPath: String, mimeType: String ->
|
||||
val ctx = appContext.reactContext ?: error("react context lost")
|
||||
withContext(Dispatchers.IO) {
|
||||
val src = File(srcPath)
|
||||
if (!src.exists()) return@withContext ""
|
||||
val resolver = ctx.contentResolver
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, src.name)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/Camera")
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
val uri = try {
|
||||
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "saveToCameraRollAsync insert failed: ${e.message}")
|
||||
null
|
||||
} ?: return@withContext ""
|
||||
try {
|
||||
resolver.openOutputStream(uri)?.use { out -> src.inputStream().use { it.copyTo(out) } }
|
||||
?: error("openOutputStream returned null")
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "saveToCameraRollAsync copy failed: ${e.message}")
|
||||
resolver.delete(uri, null, null)
|
||||
return@withContext ""
|
||||
}
|
||||
resolver.update(uri, ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }, null, null)
|
||||
uri.lastPathSegment ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
// P0 spike: decode (BitmapFactory) + JPEG encode + file write, all off the
|
||||
// JS/main threads. Returns per-stage ms so JS can prove the JS thread is
|
||||
// never blocked while native rendering runs.
|
||||
|
||||
@@ -91,6 +91,12 @@ export interface RecipescamExportModule {
|
||||
processPhotoAsync(srcPath: string, dstPath: string, matrix: number[], crop: CropRect | null, adjust?: PhotoAdjust): Promise<ColorTimings>;
|
||||
/** P1 alias: matrix + crop, no tone/cinema/enhance passes. */
|
||||
processColorAsync(srcPath: string, dstPath: string, matrix: number[], crop: CropRect | null, quality: number): Promise<ColorTimings>;
|
||||
/**
|
||||
* Insert an encoded image into DCIM/Camera (MediaStore RELATIVE_PATH) — where
|
||||
* the stock camera writes. Returns the MediaStore _ID (expo Asset.id shape),
|
||||
* or '' when the insert/copy fails, so JS falls back to MediaLibrary.
|
||||
*/
|
||||
saveToCameraRollAsync(srcPath: string, mimeType: string): Promise<string>;
|
||||
/**
|
||||
* Raw device identity the stock Xiaomi camera folds into EXIF 0x889e (the
|
||||
* watermark blob HyperOS Gallery reads) but CameraX/HAL never supplies:
|
||||
|
||||
Reference in New Issue
Block a user