P6 Stream V: ImageCompression (expect/actual) + CameraPicker polish

Android: BitmapFactory + ExifInterface + JPEG quality 0.7 + 1920px downscale.
iOS: UIImage.jpegData. JVM/JS/WASM: no-op. CameraPicker uses TakePicture
ActivityResult + permission rationale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 13:35:54 -05:00
parent 3069ec41de
commit 10b57aabaa
10 changed files with 599 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
package com.tt.honeyDue.ui.components
/**
* Shared camera-picker helpers (commonMain).
*
* The actual `rememberCameraPicker` composable lives at
* `com.tt.honeyDue.platform.rememberCameraPicker` (expect/actual per
* platform). This file captures the pure logic that decides *what* to do
* when a user taps "Take photo" so it can be unit-tested without touching
* the ActivityResult contract or iOS `UIImagePickerController`.
*
* Mirrors the iOS reference `CameraPickerView.swift`, which simply falls
* back to the photo library if the camera source type is unavailable. On
* Android we add a permission-rationale step to match Google's UX
* guidelines, then delegate to the platform picker.
*/
/** The three possible outcomes of "user tapped Take photo". */
enum class CameraPermissionDecision {
/** Camera permission already granted — launch the picker immediately. */
Launch,
/** Permission denied previously; show a rationale dialog before asking again. */
ShowRationale,
/** First request (or "don't ask again" cleared) — prompt the OS directly. */
Request,
}
/**
* Pure decision function: given the current permission state, return the
* next UI action.
*
* @param isGranted `true` if the camera permission is currently held.
* @param shouldShowRationale Android's `shouldShowRequestPermissionRationale`
* flag — `true` when the user has previously denied the permission but
* hasn't selected "don't ask again".
*/
fun decideCameraPermissionFlow(
isGranted: Boolean,
shouldShowRationale: Boolean,
): CameraPermissionDecision = when {
isGranted -> CameraPermissionDecision.Launch
shouldShowRationale -> CameraPermissionDecision.ShowRationale
else -> CameraPermissionDecision.Request
}

View File

@@ -0,0 +1,41 @@
package com.tt.honeyDue.util
/**
* Cross-platform image compression matching the iOS helper
* `iosApp/iosApp/Helpers/ImageCompression.swift`.
*
* Contract:
* - Input is a raw encoded-image [ByteArray] (JPEG, PNG, HEIC…).
* - Output is a JPEG-encoded [ByteArray] at [quality] (default 0.7), with
* the long edge clamped to [maxEdgePx] (default 1920px) while preserving
* aspect ratio.
* - EXIF orientation is applied into the pixel data; the output image is
* always in the canonical "upright" orientation (no orientation tag, or
* `ORIENTATION_NORMAL`).
*
* Platforms:
* - Android → `BitmapFactory` + `ExifInterface` + `Matrix` + JPEG compress.
* - iOS → `UIImage(data:)` + `UIImage.jpegData(compressionQuality:)`.
* `UIImage` normalizes EXIF automatically during decode.
* - JVM / JS / WASM → no-op pass-through (web/desktop do not run this path
* in production; returning the input keeps common code simple).
*
* This replaces the size-capped `ImageCompressor` helper for new call sites
* that want to match iOS (quality-based, not size-based) semantics.
*/
expect object ImageCompression {
/**
* Compress [input] to JPEG, optionally downscaling.
*
* @param input encoded image bytes (JPEG/PNG/HEIC supported by the platform).
* @param maxEdgePx maximum long-edge size in pixels (aspect preserved).
* Defaults to `1920` to match iOS.
* @param quality JPEG quality in `[0.0f, 1.0f]`. Defaults to `0.7f` to match iOS.
* @return compressed JPEG bytes, or the original input on decode failure.
*/
suspend fun compress(
input: ByteArray,
maxEdgePx: Int = 1920,
quality: Float = 0.7f
): ByteArray
}