diff --git a/app/src/main/java/com/example/snapandsolve/MainActivity.kt b/app/src/main/java/com/example/snapandsolve/MainActivity.kt index d39d8a3..9dd3cab 100644 --- a/app/src/main/java/com/example/snapandsolve/MainActivity.kt +++ b/app/src/main/java/com/example/snapandsolve/MainActivity.kt @@ -1,5 +1,6 @@ package com.example.snapandsolve + import MainScreen import android.os.Bundle import androidx.activity.ComponentActivity diff --git a/app/src/main/java/com/example/snapandsolve/camera/AlbumAndroidViewModel.kt b/app/src/main/java/com/example/snapandsolve/camera/AlbumAndroidViewModel.kt deleted file mode 100644 index 541cb74..0000000 --- a/app/src/main/java/com/example/snapandsolve/camera/AlbumAndroidViewModel.kt +++ /dev/null @@ -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 = MutableStateFlow( - AlbumViewState() - ) - val viewStateFlow: StateFlow - 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() - 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 - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/snapandsolve/camera/AlbumEvents.kt b/app/src/main/java/com/example/snapandsolve/camera/AlbumEvents.kt index 49c4d6b..ef6e9ad 100644 --- a/app/src/main/java/com/example/snapandsolve/camera/AlbumEvents.kt +++ b/app/src/main/java/com/example/snapandsolve/camera/AlbumEvents.kt @@ -1,28 +1,39 @@ -package com.example.snapandsolve.camera - import android.content.Context 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 { - data object OnPermissionGranted: Intent() - + /** + * Kamera-Permission wurde erteilt. + * @param compositionContext Android Context für Dateizugriff + */ data class OnPermissionGrantedWith(val compositionContext: Context): Intent() + /** + * Kamera-Permission wurde verweigert. + */ data object OnPermissionDenied: Intent() - data object OnImageSaved: Intent() - - data class OnImageSavedWith (val compositionContext: Context): Intent() + /** + * Kamera-Aufnahme wurde gespeichert. + * @param compositionContext Android Context für Dateizugriff + */ + data class OnImageSavedWith(val compositionContext: Context): Intent() + /** + * Kamera-Aufnahme wurde abgebrochen. + */ data object OnImageSavingCanceled: Intent() - data class OnFinishPickingImages(val imageUrls: List): Intent() - - data class OnFinishPickingImagesWith(val compositionContext: Context, val imageUrls: List): Intent() -} \ No newline at end of file + /** + * Bilder aus Galerie wurden ausgewählt. + * @param compositionContext Android Context für Dateizugriff + * @param imageUrls Liste der ausgewählten Bild-URIs + */ + data class OnFinishPickingImagesWith( + val compositionContext: Context, + val imageUrls: List + ): Intent() +} diff --git a/app/src/main/java/com/example/snapandsolve/camera/AlbumViewModel.kt b/app/src/main/java/com/example/snapandsolve/camera/AlbumViewModel.kt index b241dee..e64d6bb 100644 --- a/app/src/main/java/com/example/snapandsolve/camera/AlbumViewModel.kt +++ b/app/src/main/java/com/example/snapandsolve/camera/AlbumViewModel.kt @@ -1,6 +1,6 @@ package com.example.snapandsolve.camera - +import Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.ImageDecoder @@ -10,27 +10,35 @@ import androidx.core.content.FileProvider import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.snapandsolve.BuildConfig - +import com.example.snapandsolve.camera.AlbumViewState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import java.io.File 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 = MutableStateFlow( AlbumViewState() ) + + /** + * Observable State für UI-Komponenten. + */ val viewStateFlow: StateFlow get() = _albumViewState - //endregion - // region Intents + /** + * Verarbeitet Benutzer-Aktionen. + * @param intent Die zu verarbeitende Aktion + */ fun onReceive(intent: Intent) = viewModelScope.launch(coroutineContext) { - when(intent) { + when (intent) { is Intent.OnPermissionGrantedWith -> { println("DEBUG: OnPermissionGrantedWith empfangen") val tempFile = File.createTempFile( @@ -40,7 +48,8 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext ) println("DEBUG: TempFile erstellt: ${tempFile.absolutePath}") - val uri = FileProvider.getUriForFile(intent.compositionContext, + val uri = FileProvider.getUriForFile( + intent.compositionContext, "${BuildConfig.APPLICATION_ID}.provider", tempFile ) @@ -50,13 +59,11 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext } is Intent.OnPermissionDenied -> { - // maybe log the permission denial event println("User did not grant permission to use the camera") } is Intent.OnFinishPickingImagesWith -> { if (intent.imageUrls.isNotEmpty()) { - // Handle picked images val newImages = mutableListOf() for (eachImageUrl in intent.imageUrls) { 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) newImages.add(bitmap.asImageBitmap()) } 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") } } @@ -80,42 +86,36 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext tempFileUrl = null ) _albumViewState.value = newCopy - } else { - // user did not pick anything } } is Intent.OnImageSavedWith -> { val tempImageUrl = _albumViewState.value.tempFileUrl 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() currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap()) - _albumViewState.value = _albumViewState.value.copy(tempFileUrl = null, - selectedPictures = currentPictures) + _albumViewState.value = _albumViewState.value.copy( + tempFileUrl = null, + selectedPictures = currentPictures + ) } } is Intent.OnImageSavingCanceled -> { _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() { _albumViewState.value = _albumViewState.value.copy(selectedPictures = emptyList()) } diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/Widget.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/Widget.kt deleted file mode 100644 index 53c2e3c..0000000 --- a/app/src/main/java/com/example/snapandsolve/ui/theme/Widget.kt +++ /dev/null @@ -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 - ) - } - } - } - } -} - diff --git a/app/src/main/java/com/example/snapandsolve/view/ReportDialog.kt b/app/src/main/java/com/example/snapandsolve/view/ReportDialog.kt index 7242166..8b436c6 100644 --- a/app/src/main/java/com/example/snapandsolve/view/ReportDialog.kt +++ b/app/src/main/java/com/example/snapandsolve/view/ReportDialog.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.example.snapandsolve.camera.AlbumViewModel 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.AppButtonStyle import com.example.snapandsolve.ui.theme.composable.DialogContainer diff --git a/docs/AlbumAndroidViewModel.md b/docs/AlbumAndroidViewModel.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/AlbumEvents.md b/docs/AlbumEvents.md index e69de29..7a5b90c 100644 --- a/docs/AlbumEvents.md +++ b/docs/AlbumEvents.md @@ -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` | Bilder aus Galerie ausgewählt | +| `OnPermissionDenied` | - | Permission verweigert | +| `OnImageSavingCanceled` | - | Kamera-Aufnahme abgebrochen | + +**Deprecated:** `OnPermissionGranted`, `OnImageSaved`, `OnFinishPickingImages` (Varianten ohne Context-Parameter) \ No newline at end of file diff --git a/docs/AlbumViewModel.md b/docs/AlbumViewModel.md index e69de29..db1f741 100644 --- a/docs/AlbumViewModel.md +++ b/docs/AlbumViewModel.md @@ -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` | 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)` - 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. + +--- + + + diff --git a/docs/AlbumViewState.md b/docs/AlbumViewState.md index e69de29..673994e 100644 --- a/docs/AlbumViewState.md +++ b/docs/AlbumViewState.md @@ -0,0 +1,45 @@ +## AlbumViewState + +```kotlin +data class AlbumViewState( + val tempFileUrl: Uri? = null, + val selectedPictures: List = 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` | `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() +``` + + +