- Camerafunktionalität dokumentiert

- Aufräumarbeiten
This commit is contained in:
2026-02-09 22:20:59 +01:00
parent 7d6bf0fdd7
commit 48802606e8
10 changed files with 162 additions and 262 deletions

View File

@@ -1,5 +1,6 @@
package com.example.snapandsolve package com.example.snapandsolve
import MainScreen import MainScreen
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity

View File

@@ -1,119 +0,0 @@
package com.example.snapandsolve.camera
import android.app.Application
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.content.FileProvider
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.example.snapandsolve.BuildConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.io.File
import kotlin.coroutines.CoroutineContext
/**
* This variant inherits from [AndroidViewModel] and has access to the application context
*/
class AlbumAndroidViewModel(private val appContext: Application,
private val coroutineContext: CoroutineContext
): AndroidViewModel(appContext) {
//region View State
private val _albumViewState: MutableStateFlow<AlbumViewState> = MutableStateFlow(
AlbumViewState()
)
val viewStateFlow: StateFlow<AlbumViewState>
get() = _albumViewState
//endregion
fun onEvent(intent: Intent) = viewModelScope.launch(coroutineContext) {
when(intent) {
is Intent.OnPermissionGranted -> {
// Create an empty image file in the app's cache directory
val file = File.createTempFile(
"temp_image_file_", /* prefix */
".jpg", /* suffix */
appContext.cacheDir /* cache directory */
)
// Create sandboxed url for this temp file - needed for the camera API
val uri = FileProvider.getUriForFile(appContext,
"${BuildConfig.APPLICATION_ID}.provider",
file
)
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = uri)
}
is Intent.OnPermissionDenied -> {
// maybe log the permission denial event
println("User did not grant permission to use the camera")
}
is Intent.OnFinishPickingImages -> {
if (intent.imageUrls.isNotEmpty()) {
// Handle picked images
val newImages = mutableListOf<ImageBitmap>()
for (eachImageUrl in intent.imageUrls) {
val inputStream = appContext.contentResolver.openInputStream(eachImageUrl)
val bytes = inputStream?.readBytes()
inputStream?.close()
if (bytes != null) {
val bitmapOptions = BitmapFactory.Options()
bitmapOptions.inMutable = true
val bitmap: Bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bitmapOptions)
val imageBitmap = bitmap.asImageBitmap()
newImages.add(imageBitmap)
} else {
// error reading the bytes from the image url
println("The image that was picked could not be read from the device at this url: $eachImageUrl")
}
}
val currentViewState = _albumViewState.value
val newCopy = currentViewState.copy(
selectedPictures = (currentViewState.selectedPictures + newImages),
tempFileUrl = null
)
_albumViewState.value = newCopy
} else {
// user did not pick anything
}
}
is Intent.OnImageSaved -> {
val tempImageUrl = _albumViewState.value.tempFileUrl
if (tempImageUrl != null) {
val source: ImageDecoder.Source = ImageDecoder.createSource(appContext.contentResolver, tempImageUrl)
val currentPictures = _albumViewState.value.selectedPictures.toMutableList()
currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap())
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null,
selectedPictures = currentPictures)
}
}
is Intent.OnImageSavingCanceled -> {
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null)
}
is Intent.OnFinishPickingImagesWith -> {
// unnecessary in this viewmodel variant
}
is Intent.OnPermissionGrantedWith -> {
// unnecessary in this viewmodel variant
}
is Intent.OnImageSavedWith -> {
// unnecessary in this viewmodel variant
}
}
}
}

View File

@@ -1,28 +1,39 @@
package com.example.snapandsolve.camera
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
/*
Das sind die Funktionen beim Drücken der Knöpfe. Übersichtlicher wäre es sie direkt mit den
Knöpfen in der ViewModel zu platzieren. AlbumEvents als name ist vielleicht unglücklich gewählt.
*/
/** /**
* User generated events that can be triggered from the UI. * Benutzer-Aktionen für das Album/Kamera-System.
*/ */
sealed class Intent { sealed class Intent {
data object OnPermissionGranted: Intent() /**
* Kamera-Permission wurde erteilt.
* @param compositionContext Android Context für Dateizugriff
*/
data class OnPermissionGrantedWith(val compositionContext: Context): Intent() data class OnPermissionGrantedWith(val compositionContext: Context): Intent()
/**
* Kamera-Permission wurde verweigert.
*/
data object OnPermissionDenied: Intent() data object OnPermissionDenied: Intent()
data object OnImageSaved: Intent() /**
* Kamera-Aufnahme wurde gespeichert.
data class OnImageSavedWith (val compositionContext: Context): Intent() * @param compositionContext Android Context für Dateizugriff
*/
data class OnImageSavedWith(val compositionContext: Context): Intent()
/**
* Kamera-Aufnahme wurde abgebrochen.
*/
data object OnImageSavingCanceled: Intent() data object OnImageSavingCanceled: Intent()
data class OnFinishPickingImages(val imageUrls: List<Uri>): Intent() /**
* Bilder aus Galerie wurden ausgewählt.
data class OnFinishPickingImagesWith(val compositionContext: Context, val imageUrls: List<Uri>): Intent() * @param compositionContext Android Context für Dateizugriff
} * @param imageUrls Liste der ausgewählten Bild-URIs
*/
data class OnFinishPickingImagesWith(
val compositionContext: Context,
val imageUrls: List<Uri>
): Intent()
}

View File

@@ -1,6 +1,6 @@
package com.example.snapandsolve.camera package com.example.snapandsolve.camera
import Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.ImageDecoder import android.graphics.ImageDecoder
@@ -10,27 +10,35 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.snapandsolve.BuildConfig import com.example.snapandsolve.BuildConfig
import com.example.snapandsolve.camera.AlbumViewState
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class AlbumViewModel(private val coroutineContext: CoroutineContext /**
): ViewModel() { * ViewModel für Bild-Verwaltung (Kamera + Galerie).
* Verwaltet ausgewählte Bilder und temporäre Kamera-Dateien.
*/
class AlbumViewModel(private val coroutineContext: CoroutineContext) : ViewModel() {
//region View State
private val _albumViewState: MutableStateFlow<AlbumViewState> = MutableStateFlow( private val _albumViewState: MutableStateFlow<AlbumViewState> = MutableStateFlow(
AlbumViewState() AlbumViewState()
) )
/**
* Observable State für UI-Komponenten.
*/
val viewStateFlow: StateFlow<AlbumViewState> val viewStateFlow: StateFlow<AlbumViewState>
get() = _albumViewState get() = _albumViewState
//endregion
// region Intents /**
* Verarbeitet Benutzer-Aktionen.
* @param intent Die zu verarbeitende Aktion
*/
fun onReceive(intent: Intent) = viewModelScope.launch(coroutineContext) { fun onReceive(intent: Intent) = viewModelScope.launch(coroutineContext) {
when(intent) { when (intent) {
is Intent.OnPermissionGrantedWith -> { is Intent.OnPermissionGrantedWith -> {
println("DEBUG: OnPermissionGrantedWith empfangen") println("DEBUG: OnPermissionGrantedWith empfangen")
val tempFile = File.createTempFile( val tempFile = File.createTempFile(
@@ -40,7 +48,8 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext
) )
println("DEBUG: TempFile erstellt: ${tempFile.absolutePath}") println("DEBUG: TempFile erstellt: ${tempFile.absolutePath}")
val uri = FileProvider.getUriForFile(intent.compositionContext, val uri = FileProvider.getUriForFile(
intent.compositionContext,
"${BuildConfig.APPLICATION_ID}.provider", "${BuildConfig.APPLICATION_ID}.provider",
tempFile tempFile
) )
@@ -50,13 +59,11 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext
} }
is Intent.OnPermissionDenied -> { is Intent.OnPermissionDenied -> {
// maybe log the permission denial event
println("User did not grant permission to use the camera") println("User did not grant permission to use the camera")
} }
is Intent.OnFinishPickingImagesWith -> { is Intent.OnFinishPickingImagesWith -> {
if (intent.imageUrls.isNotEmpty()) { if (intent.imageUrls.isNotEmpty()) {
// Handle picked images
val newImages = mutableListOf<ImageBitmap>() val newImages = mutableListOf<ImageBitmap>()
for (eachImageUrl in intent.imageUrls) { for (eachImageUrl in intent.imageUrls) {
val inputStream = intent.compositionContext.contentResolver.openInputStream(eachImageUrl) val inputStream = intent.compositionContext.contentResolver.openInputStream(eachImageUrl)
@@ -69,7 +76,6 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext
val bitmap: Bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bitmapOptions) val bitmap: Bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bitmapOptions)
newImages.add(bitmap.asImageBitmap()) newImages.add(bitmap.asImageBitmap())
} else { } else {
// error reading the bytes from the image url
println("The image that was picked could not be read from the device at this url: $eachImageUrl") println("The image that was picked could not be read from the device at this url: $eachImageUrl")
} }
} }
@@ -80,42 +86,36 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext
tempFileUrl = null tempFileUrl = null
) )
_albumViewState.value = newCopy _albumViewState.value = newCopy
} else {
// user did not pick anything
} }
} }
is Intent.OnImageSavedWith -> { is Intent.OnImageSavedWith -> {
val tempImageUrl = _albumViewState.value.tempFileUrl val tempImageUrl = _albumViewState.value.tempFileUrl
if (tempImageUrl != null) { if (tempImageUrl != null) {
val source = ImageDecoder.createSource(intent.compositionContext.contentResolver, tempImageUrl) val source = ImageDecoder.createSource(
intent.compositionContext.contentResolver,
tempImageUrl
)
val currentPictures = _albumViewState.value.selectedPictures.toMutableList() val currentPictures = _albumViewState.value.selectedPictures.toMutableList()
currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap()) currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap())
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null, _albumViewState.value = _albumViewState.value.copy(
selectedPictures = currentPictures) tempFileUrl = null,
selectedPictures = currentPictures
)
} }
} }
is Intent.OnImageSavingCanceled -> { is Intent.OnImageSavingCanceled -> {
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null) _albumViewState.value = _albumViewState.value.copy(tempFileUrl = null)
} }
is Intent.OnPermissionGranted -> {
// unnecessary in this viewmodel variant
}
is Intent.OnFinishPickingImages -> {
// unnecessary in this viewmodel variant
}
is Intent.OnImageSaved -> {
// unnecessary in this viewmodel variant
}
} }
} }
// endregion
/**
* Löscht alle ausgewählten Bilder aus dem State.
*/
fun clearSelection() { fun clearSelection() {
_albumViewState.value = _albumViewState.value.copy(selectedPictures = emptyList()) _albumViewState.value = _albumViewState.value.copy(selectedPictures = emptyList())
} }

View File

@@ -1,96 +0,0 @@
@file:Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
package com.example.snapandsolve.ui.theme
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun OverlayShell(
title: String,
footer: @Composable RowScope.() -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.25f)),
contentAlignment = Alignment.Center
) {
BoxWithConstraints {
val verticalMargin = 50.dp
val maxCardHeight = maxHeight - verticalMargin * 2
Card(
modifier = Modifier
.fillMaxWidth(0.9f)
.heightIn(min = 400.dp, max = maxCardHeight),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = WidgetColor)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = Color.Black
)
}
Spacer(Modifier.height(12.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f, fill = true)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp),
content = content
)
Spacer(Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
content = footer
)
}
}
}
}
}

View File

@@ -26,7 +26,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.snapandsolve.camera.AlbumViewModel import com.example.snapandsolve.camera.AlbumViewModel
import com.example.snapandsolve.camera.AlbumViewState import com.example.snapandsolve.camera.AlbumViewState
import com.example.snapandsolve.camera.Intent
import com.example.snapandsolve.ui.theme.composable.AppButton import com.example.snapandsolve.ui.theme.composable.AppButton
import com.example.snapandsolve.ui.theme.composable.AppButtonStyle import com.example.snapandsolve.ui.theme.composable.AppButtonStyle
import com.example.snapandsolve.ui.theme.composable.DialogContainer import com.example.snapandsolve.ui.theme.composable.DialogContainer

View File

@@ -0,0 +1,19 @@
## Intent (sealed class)
```kotlin
sealed class Intent
```
**Zweck:** Definition aller möglichen Benutzeraktionen im Album-System.
### Varianten
| Intent | Parameter | Beschreibung |
|--------|-----------|--------------|
| `OnPermissionGrantedWith` | `Context` | Kamera-Permission erteilt |
| `OnImageSavedWith` | `Context` | Kamera-Aufnahme gespeichert |
| `OnFinishPickingImagesWith` | `Context, List<Uri>` | Bilder aus Galerie ausgewählt |
| `OnPermissionDenied` | - | Permission verweigert |
| `OnImageSavingCanceled` | - | Kamera-Aufnahme abgebrochen |
**Deprecated:** `OnPermissionGranted`, `OnImageSaved`, `OnFinishPickingImages` (Varianten ohne Context-Parameter)

View File

@@ -0,0 +1,40 @@
# Album/Kamera-System Dokumentation
## AlbumViewModel
```kotlin
class AlbumViewModel(private val coroutineContext: CoroutineContext) : ViewModel()
```
**Zweck:** Verwaltung von Bildauswahl und Kamera-Aufnahmen für Schadensmeldungen.
### Properties
| Name | Typ | Beschreibung |
|------|-----|--------------|
| `viewStateFlow` | `StateFlow<AlbumViewState>` | Read-only State für UI-Komponenten |
### Methoden
#### `onReceive(intent: Intent)`
Verarbeitet Benutzeraktionen für Bild-Verwaltung.
**Parameter:**
- `intent: Intent` - Benutzeraktion (siehe Intent-Klasse)
**Verwendete Intents:**
- `OnPermissionGrantedWith(Context)` - Erstellt temp. Datei für Kamera
- `OnFinishPickingImagesWith(Context, List<Uri>)` - Lädt Bilder aus Galerie
- `OnImageSavedWith(Context)` - Speichert Kamera-Aufnahme
- `OnImageSavingCanceled` - Verwirft temp. Datei
- `OnPermissionDenied` - Loggt Permission-Verweigerung
**Deprecated Intents:** `OnPermissionGranted`, `OnFinishPickingImages`, `OnImageSaved` (ohne Context)
#### `clearSelection()`
Löscht alle ausgewählten Bilder aus dem State.
---

View File

@@ -0,0 +1,45 @@
## AlbumViewState
```kotlin
data class AlbumViewState(
val tempFileUrl: Uri? = null,
val selectedPictures: List<ImageBitmap> = emptyList()
)
```
**Zweck:** Immutable State-Container für Album-UI.
### Properties
| Name | Typ | Default | Beschreibung |
|------|-----|---------|--------------|
| `tempFileUrl` | `Uri?` | `null` | Temporäre URI für Kamera-Aufnahme |
| `selectedPictures` | `List<ImageBitmap>` | `emptyList()` | Alle ausgewählten/aufgenommenen Bilder |
---
## Verwendungsbeispiel
```kotlin
// Initialisierung
val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) }
// State beobachten
val viewState by albumViewModel.viewStateFlow.collectAsState()
// Kamera öffnen
albumViewModel.onReceive(Intent.OnPermissionGrantedWith(context))
val cameraUri = viewState.tempFileUrl
// Bild gespeichert
albumViewModel.onReceive(Intent.OnImageSavedWith(context))
// Bilder hochladen
viewState.selectedPictures.forEach { bitmap ->
mapViewModel.uploadImageAsAttachment(bitmap)
}
albumViewModel.clearSelection()
```