Add bottom bar gesture exclusion, dual image save, thumbnail preview, and gallery picker
- Increase bottom padding and add systemGestureExclusion to prevent accidental navigation gestures from the capture controls - Save both original and processed images with shared timestamps (TILTSHIFT_* and ORIGINAL_*) via new saveBitmapPair() pipeline - Show animated thumbnail of last captured photo at bottom-right; tap opens the image in the default photo viewer - Add gallery picker button to process existing photos through the tilt-shift pipeline with full EXIF rotation support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7abb2ea5a0
commit
780a8ab167
3 changed files with 355 additions and 57 deletions
|
|
@ -21,7 +21,12 @@ import java.util.Locale
|
|||
* Result of a photo save operation.
|
||||
*/
|
||||
sealed class SaveResult {
|
||||
data class Success(val uri: Uri, val path: String) : SaveResult()
|
||||
data class Success(
|
||||
val uri: Uri,
|
||||
val path: String,
|
||||
val originalUri: Uri? = null,
|
||||
val thumbnail: android.graphics.Bitmap? = null
|
||||
) : SaveResult()
|
||||
data class Error(val message: String, val exception: Exception? = null) : SaveResult()
|
||||
}
|
||||
|
||||
|
|
@ -44,10 +49,44 @@ class PhotoSaver(private val context: Context) {
|
|||
orientation: Int,
|
||||
location: Location?
|
||||
): SaveResult = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val fileName = "TILTSHIFT_${fileNameFormat.format(Date())}.jpg"
|
||||
val fileName = "TILTSHIFT_${fileNameFormat.format(Date())}.jpg"
|
||||
saveSingleBitmap(fileName, bitmap, orientation, location)
|
||||
}
|
||||
|
||||
// Create content values for MediaStore
|
||||
/**
|
||||
* Saves both original and processed bitmaps to the gallery.
|
||||
* Uses a shared timestamp so paired files sort together.
|
||||
* Returns the processed image's URI as the primary result.
|
||||
*/
|
||||
suspend fun saveBitmapPair(
|
||||
original: Bitmap,
|
||||
processed: Bitmap,
|
||||
orientation: Int,
|
||||
location: Location?
|
||||
): SaveResult = withContext(Dispatchers.IO) {
|
||||
val timestamp = fileNameFormat.format(Date())
|
||||
val processedFileName = "TILTSHIFT_${timestamp}.jpg"
|
||||
val originalFileName = "ORIGINAL_${timestamp}.jpg"
|
||||
|
||||
val processedResult = saveSingleBitmap(processedFileName, processed, orientation, location)
|
||||
if (processedResult is SaveResult.Error) return@withContext processedResult
|
||||
|
||||
val originalResult = saveSingleBitmap(originalFileName, original, orientation, location)
|
||||
val originalUri = (originalResult as? SaveResult.Success)?.uri
|
||||
|
||||
(processedResult as SaveResult.Success).copy(originalUri = originalUri)
|
||||
}
|
||||
|
||||
/**
|
||||
* Core save logic: writes a single bitmap to MediaStore with EXIF data.
|
||||
*/
|
||||
private fun saveSingleBitmap(
|
||||
fileName: String,
|
||||
bitmap: Bitmap,
|
||||
orientation: Int,
|
||||
location: Location?
|
||||
): SaveResult {
|
||||
return try {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||
|
|
@ -57,29 +96,23 @@ class PhotoSaver(private val context: Context) {
|
|||
put(MediaStore.Images.Media.IS_PENDING, 1)
|
||||
}
|
||||
|
||||
// Insert into MediaStore
|
||||
val contentResolver = context.contentResolver
|
||||
val uri = contentResolver.insert(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
contentValues
|
||||
) ?: return@withContext SaveResult.Error("Failed to create MediaStore entry")
|
||||
) ?: return SaveResult.Error("Failed to create MediaStore entry")
|
||||
|
||||
// Write bitmap to output stream
|
||||
contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStream)
|
||||
} ?: return@withContext SaveResult.Error("Failed to open output stream")
|
||||
} ?: return SaveResult.Error("Failed to open output stream")
|
||||
|
||||
// Write EXIF data
|
||||
writeExifToUri(uri, orientation, location)
|
||||
|
||||
// Mark as complete
|
||||
contentValues.clear()
|
||||
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||
contentResolver.update(uri, contentValues, null, null)
|
||||
|
||||
// Get the file path for display
|
||||
val path = getPathFromUri(uri)
|
||||
|
||||
SaveResult.Success(uri, path)
|
||||
} catch (e: Exception) {
|
||||
SaveResult.Error("Failed to save photo: ${e.message}", e)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue