Add ViewModel for state preservation and centralize color constants
- Create CameraViewModel with AndroidViewModel to survive configuration changes (rotation). All blur params, capture state, gallery preview, and thumbnail state now live in StateFlow fields - Create AppColors object to centralize the 12+ hardcoded color literals into a single source of truth - Add lifecycle-viewmodel-compose dependency Fixes: state lost on rotation, orphaned capture coroutines on config change, accent color maintenance risk Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5e08fb9c13
commit
6a1d66bd4b
4 changed files with 229 additions and 0 deletions
|
|
@ -80,6 +80,7 @@ dependencies {
|
|||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
// Compose
|
||||
|
|
|
|||
209
app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt
Normal file
209
app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
package no.naiv.tiltshift.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Bitmap
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import no.naiv.tiltshift.camera.CameraManager
|
||||
import no.naiv.tiltshift.camera.ImageCaptureHandler
|
||||
import no.naiv.tiltshift.effect.BlurParameters
|
||||
import no.naiv.tiltshift.storage.PhotoSaver
|
||||
import no.naiv.tiltshift.storage.SaveResult
|
||||
import no.naiv.tiltshift.util.HapticFeedback
|
||||
import no.naiv.tiltshift.util.LocationProvider
|
||||
import no.naiv.tiltshift.util.OrientationDetector
|
||||
|
||||
/**
|
||||
* ViewModel for the camera screen.
|
||||
* Survives configuration changes (rotation) and process death (via SavedStateHandle for primitives).
|
||||
*/
|
||||
class CameraViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CameraViewModel"
|
||||
}
|
||||
|
||||
val cameraManager = CameraManager(application)
|
||||
val photoSaver = PhotoSaver(application)
|
||||
val captureHandler = ImageCaptureHandler(application, photoSaver)
|
||||
val haptics = HapticFeedback(application)
|
||||
val orientationDetector = OrientationDetector(application)
|
||||
val locationProvider = LocationProvider(application)
|
||||
|
||||
// Blur parameters — preserved across config changes
|
||||
private val _blurParams = MutableStateFlow(BlurParameters.DEFAULT)
|
||||
val blurParams: StateFlow<BlurParameters> = _blurParams.asStateFlow()
|
||||
|
||||
// Capture state
|
||||
private val _isCapturing = MutableStateFlow(false)
|
||||
val isCapturing: StateFlow<Boolean> = _isCapturing.asStateFlow()
|
||||
|
||||
private val _showSaveSuccess = MutableStateFlow(false)
|
||||
val showSaveSuccess: StateFlow<Boolean> = _showSaveSuccess.asStateFlow()
|
||||
|
||||
private val _showSaveError = MutableStateFlow<String?>(null)
|
||||
val showSaveError: StateFlow<String?> = _showSaveError.asStateFlow()
|
||||
|
||||
private val _showControls = MutableStateFlow(false)
|
||||
val showControls: StateFlow<Boolean> = _showControls.asStateFlow()
|
||||
|
||||
// Thumbnail state
|
||||
private val _lastSavedUri = MutableStateFlow<Uri?>(null)
|
||||
val lastSavedUri: StateFlow<Uri?> = _lastSavedUri.asStateFlow()
|
||||
|
||||
private val _lastThumbnailBitmap = MutableStateFlow<Bitmap?>(null)
|
||||
val lastThumbnailBitmap: StateFlow<Bitmap?> = _lastThumbnailBitmap.asStateFlow()
|
||||
|
||||
// Gallery preview state
|
||||
private val _galleryBitmap = MutableStateFlow<Bitmap?>(null)
|
||||
val galleryBitmap: StateFlow<Bitmap?> = _galleryBitmap.asStateFlow()
|
||||
|
||||
private val _galleryImageUri = MutableStateFlow<Uri?>(null)
|
||||
val galleryImageUri: StateFlow<Uri?> = _galleryImageUri.asStateFlow()
|
||||
|
||||
val isGalleryPreview: Boolean get() = _galleryBitmap.value != null
|
||||
|
||||
// Device state
|
||||
private val _currentRotation = MutableStateFlow(Surface.ROTATION_0)
|
||||
val currentRotation: StateFlow<Int> = _currentRotation.asStateFlow()
|
||||
|
||||
private val _currentLocation = MutableStateFlow<Location?>(null)
|
||||
val currentLocation: StateFlow<Location?> = _currentLocation.asStateFlow()
|
||||
|
||||
// Processing indicator
|
||||
private val _isProcessing = MutableStateFlow(false)
|
||||
val isProcessing: StateFlow<Boolean> = _isProcessing.asStateFlow()
|
||||
|
||||
fun updateBlurParams(params: BlurParameters) {
|
||||
_blurParams.value = params
|
||||
}
|
||||
|
||||
fun resetBlurParams() {
|
||||
_blurParams.value = BlurParameters.DEFAULT
|
||||
}
|
||||
|
||||
fun toggleControls() {
|
||||
_showControls.value = !_showControls.value
|
||||
}
|
||||
|
||||
fun updateRotation(rotation: Int) {
|
||||
_currentRotation.value = rotation
|
||||
}
|
||||
|
||||
fun updateLocation(location: Location?) {
|
||||
_currentLocation.value = location
|
||||
}
|
||||
|
||||
fun loadGalleryImage(uri: Uri) {
|
||||
if (_isCapturing.value || isGalleryPreview) return
|
||||
viewModelScope.launch {
|
||||
_isProcessing.value = true
|
||||
val bitmap = captureHandler.loadGalleryImage(uri)
|
||||
_isProcessing.value = false
|
||||
if (bitmap != null) {
|
||||
_galleryBitmap.value = bitmap
|
||||
_galleryImageUri.value = uri
|
||||
} else {
|
||||
haptics.error()
|
||||
showError("Failed to load image")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelGalleryPreview() {
|
||||
val old = _galleryBitmap.value
|
||||
_galleryBitmap.value = null
|
||||
_galleryImageUri.value = null
|
||||
old?.recycle()
|
||||
}
|
||||
|
||||
fun applyGalleryEffect() {
|
||||
val uri = _galleryImageUri.value ?: return
|
||||
if (_isCapturing.value) return
|
||||
_isCapturing.value = true
|
||||
_isProcessing.value = true
|
||||
haptics.heavyClick()
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = captureHandler.processExistingImage(
|
||||
imageUri = uri,
|
||||
blurParams = _blurParams.value,
|
||||
location = _currentLocation.value
|
||||
)
|
||||
handleSaveResult(result)
|
||||
cancelGalleryPreview()
|
||||
_isCapturing.value = false
|
||||
_isProcessing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun capturePhoto() {
|
||||
if (_isCapturing.value) return
|
||||
val imageCapture = cameraManager.imageCapture ?: return
|
||||
_isCapturing.value = true
|
||||
_isProcessing.value = true
|
||||
haptics.heavyClick()
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = captureHandler.capturePhoto(
|
||||
imageCapture = imageCapture,
|
||||
executor = cameraManager.getExecutor(),
|
||||
blurParams = _blurParams.value,
|
||||
deviceRotation = _currentRotation.value,
|
||||
location = _currentLocation.value,
|
||||
isFrontCamera = cameraManager.isFrontCamera.value
|
||||
)
|
||||
handleSaveResult(result)
|
||||
_isCapturing.value = false
|
||||
_isProcessing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSaveResult(result: SaveResult) {
|
||||
when (result) {
|
||||
is SaveResult.Success -> {
|
||||
haptics.success()
|
||||
val oldThumb = _lastThumbnailBitmap.value
|
||||
_lastThumbnailBitmap.value = result.thumbnail
|
||||
_lastSavedUri.value = result.uri
|
||||
oldThumb?.recycle()
|
||||
viewModelScope.launch {
|
||||
_showSaveSuccess.value = true
|
||||
kotlinx.coroutines.delay(1500)
|
||||
_showSaveSuccess.value = false
|
||||
}
|
||||
}
|
||||
is SaveResult.Error -> {
|
||||
haptics.error()
|
||||
showError(result.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
viewModelScope.launch {
|
||||
_showSaveError.value = message
|
||||
kotlinx.coroutines.delay(2000)
|
||||
_showSaveError.value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun showCameraError(message: String) {
|
||||
showError(message)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
cameraManager.release()
|
||||
_lastThumbnailBitmap.value?.recycle()
|
||||
_galleryBitmap.value?.recycle()
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/no/naiv/tiltshift/ui/theme/AppColors.kt
Normal file
18
app/src/main/java/no/naiv/tiltshift/ui/theme/AppColors.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package no.naiv.tiltshift.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Centralized color definitions for the Tilt-Shift Camera app.
|
||||
*/
|
||||
object AppColors {
|
||||
val Accent = Color(0xFFFFB300)
|
||||
val OverlayDark = Color(0x80000000)
|
||||
val OverlayDarker = Color(0xCC000000)
|
||||
val Success = Color(0xFF4CAF50)
|
||||
val Error = Color(0xFFF44336)
|
||||
val OverlayLinearBlur = Color(0x40FFFFFF)
|
||||
val OverlayRadialBlur = Color(0x30FFFFFF)
|
||||
/** Dark outline behind overlay guide lines for visibility over bright scenes. */
|
||||
val OverlayOutline = Color(0x80000000)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue