Kamera funktionalität

-AlbumAndroidViewModel
-AlbumEvents
-AlbumViewModel
-AlbumViewState
-file_patchs
SelectPictureScree.kt wurde gelöscht. Die Funktionanlität  AlbumScreen() wurde in ReportOverlay neu aufgebaut.
This commit is contained in:
2025-12-17 22:26:26 +01:00
parent 9048d31413
commit c644361ab8
12 changed files with 431 additions and 34 deletions

1
.idea/gradle.xml generated
View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

View File

@@ -18,6 +18,13 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
val properties = org.jetbrains.kotlin.konan.properties.Properties()
val propertiesFile = rootProject.file("local.properties")
if (propertiesFile.exists()){
propertiesFile.inputStream().use { properties.load(it) }
}
buildConfigField("String", "ARCGIS_TOKEN","\"${properties.getProperty("arcgis.token","")}\"")
}
buildTypes {
@@ -38,6 +45,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}

View File

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Kamera Zugriffsberechtigung -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -12,7 +14,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.SnapAndSolve">
<activity
android:name=".MainActivity"
android:name="com.example.snapandsolve.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SnapAndSolve">
@@ -22,6 +24,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -7,41 +7,26 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.snapandsolve.camera.AlbumViewModel
import com.example.snapandsolve.ui.theme.SnapAndSolveTheme
import kotlinx.coroutines.Dispatchers
class MainActivity : ComponentActivity() {
private lateinit var viewModel: AlbumViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = AlbumViewModel(coroutineContext = Dispatchers.Default)
enableEdgeToEdge()
setContent {
SnapAndSolveTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
MainScreen(application=application)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
SnapAndSolveTheme {
Greeting("Android")
}
}

View File

@@ -0,0 +1,336 @@
package com.example.snapandsolve
import android.Manifest
import android.app.Application
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeFloatingActionButton
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
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.AppColor
import com.example.snapandsolve.ui.theme.ButtonColor
import com.example.snapandsolve.ui.theme.WidgetColor
import kotlinx.coroutines.Dispatchers
@Composable
fun MainScreen(modifier: Modifier = Modifier, application: Application) {
var showReport by rememberSaveable { mutableStateOf(false) }
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
AppTopBar()
},
bottomBar = {
BottomAppBar(
modifier = Modifier.height(120.dp),
containerColor = AppColor,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = {
/*TODO*/
},
modifier = Modifier.padding(bottom = 8.dp)
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu",
)
}
Spacer(Modifier.weight(1f))
}
}
},
floatingActionButton = {
LargeFloatingActionButton(
onClick = {
showReport = true
},
modifier = Modifier.offset(y = 64.dp),
containerColor = ButtonColor
) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
},
floatingActionButtonPosition = FabPosition.Center,
) { innerPadding ->
ContentScreen(
modifier = Modifier.padding(innerPadding),
application,
showReport = showReport,
onDismissReport = { showReport = false })
}
}
@Composable
fun ContentScreen(
modifier: Modifier = Modifier,
application: Application,
showReport: Boolean,
onDismissReport: () -> Unit
) {
val mapViewModel = remember { MapViewModel(application) }
// ViewModel für die Kamera-Funktionalität
val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) }
Box(modifier = modifier.fillMaxSize()) {
if (showReport) {
ReportOverlay(
onCancel = onDismissReport,
onAdd = { /* später */ },
viewModel = albumViewModel
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTopBar(
modifier: Modifier = Modifier
) {
MediumTopAppBar(
title = {
Text("Scan And Solve")
},
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = AppColor,
titleContentColor = Color.White
)
)
}
@Composable
fun ReportOverlay(
onCancel: () -> Unit,
onAdd: () -> Unit,
viewModel: AlbumViewModel
) {
val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
val currentContext = LocalContext.current
// Launcher für Bildauswahl aus Galerie
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(20)
) { urls ->
viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
}
// Launcher für Kamera
val cameraLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.TakePicture()
) { isImageSaved ->
if (isImageSaved) {
viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
} else {
viewModel.onReceive(Intent.OnImageSavingCanceled)
}
}
// Launcher für Kamera-Berechtigung
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { permissionGranted ->
if (permissionGranted) {
viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
} else {
viewModel.onReceive(Intent.OnPermissionDenied)
}
}
// Funktion zum Starten der Kamera (prüft Berechtigung)
fun startCamera() {
println("DEBUG: startCamera() aufgerufen")
val hasPermission = currentContext.checkSelfPermission(Manifest.permission.CAMERA) ==
android.content.pm.PackageManager.PERMISSION_GRANTED
println("DEBUG: Hat Berechtigung? $hasPermission")
if (hasPermission) {
println("DEBUG: Erstelle tempFileUrl")
// Berechtigung bereits erteilt -> direkt tempFileUrl erstellen
viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
} else {
println("DEBUG: Frage Berechtigung an")
// Berechtigung anfragen
permissionLauncher.launch(Manifest.permission.CAMERA)
}
}
// Kamera starten, wenn tempFileUrl gesetzt ist
LaunchedEffect(key1 = viewState.tempFileUrl) {
viewState.tempFileUrl?.let {
cameraLauncher.launch(it)
}
}
// leichter Dim-Hintergrund
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.25f)),
contentAlignment = Alignment.Center
) {
Card(
modifier = Modifier
.fillMaxWidth(0.9f)
.heightIn(min = 400.dp),
shape = RoundedCornerShape(24.dp),
colors = CardColors(
containerColor = WidgetColor,
contentColor = ButtonColor,
disabledContainerColor = Color.White,
disabledContentColor = Color.White
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
"Schadensbeschreibung:",
color = Color.Black
)
// Deine Kamera-Buttons (über der weißen Box)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = {
startCamera()
},
modifier = Modifier.weight(1f)
) {
Text(text = "Foto aufnehmen")
}
Button(
onClick = {
pickImageFromAlbumLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
},
modifier = Modifier.weight(1f)
) {
Text(text = "Aus Galerie")
}
}
// Die weiße Box vom Kommilitonen (Textfeld-Platzhalter)
Box(
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.background(Color.White, RoundedCornerShape(12.dp))
)
// Grid für ausgewählte Bilder (außerhalb der Box, aber in der Card)
if (viewState.selectedPictures.isNotEmpty()) {
Text(
text = "Ausgewählte Bilder (${viewState.selectedPictures.size})",
color = Color.Black
)
LazyVerticalGrid(
columns = GridCells.Fixed(3),
userScrollEnabled = false,
modifier = Modifier
.fillMaxWidth()
.heightIn(0.dp, 200.dp)
) {
itemsIndexed(viewState.selectedPictures) { index, picture ->
Image(
modifier = Modifier.padding(4.dp),
bitmap = picture,
contentDescription = "Bild ${index + 1}",
contentScale = ContentScale.Crop
)
}
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedButton(
onClick = onCancel,
colors = ButtonColors(
containerColor = ButtonColor,
contentColor = Color.White,
disabledContainerColor = Color.White,
disabledContentColor = Color.White
)
) {
Text(
"Abbrechen",
color = Color.Black
)
}
Button(onClick = onAdd) { Text("Hinzufügen") }
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
package com.example.snapandsolve
import android.app.Application
import androidx.lifecycle.AndroidViewModel
class MapViewModel(application: Application ): AndroidViewModel(application) {
}

View File

@@ -32,19 +32,21 @@ class AlbumViewModel(private val coroutineContext: CoroutineContext
fun onReceive(intent: Intent) = viewModelScope.launch(coroutineContext) {
when(intent) {
is Intent.OnPermissionGrantedWith -> {
// Create an empty image file in the app's cache directory
println("DEBUG: OnPermissionGrantedWith empfangen")
val tempFile = File.createTempFile(
"temp_image_file_", /* prefix */
".jpg", /* suffix */
intent.compositionContext.cacheDir /* cache directory */
"temp_image_file_",
".jpg",
intent.compositionContext.cacheDir
)
println("DEBUG: TempFile erstellt: ${tempFile.absolutePath}")
// Create sandboxed url for this temp file - needed for the camera API
val uri = FileProvider.getUriForFile(intent.compositionContext,
"${BuildConfig.APPLICATION_ID}.provider", /* needs to match the provider information in the manifest */
"${BuildConfig.APPLICATION_ID}.provider",
tempFile
)
println("DEBUG: URI erstellt: $uri")
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = uri)
println("DEBUG: tempFileUrl gesetzt in ViewState")
}
is Intent.OnPermissionDenied -> {

View File

@@ -1,7 +1,11 @@
package com.example.snapandsolve.ui.theme
import androidx.compose.material3.CardColors
import androidx.compose.ui.graphics.Color
val AppColor = Color(0xFF7CBE64)
val ButtonColor = Color(0xFFE6E6E6)
val WidgetColor: Color = Color(0xFFDBCCCC)
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)

View File

@@ -0,0 +1,4 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- creates a reference to the cache folder that the system maintains -->
<cache-path name="temporary_camera_images" path="/" />
</paths>

38
docs/architecture.md Normal file
View File

@@ -0,0 +1,38 @@
# Architektur
## Überblick
## Struktur
## Verantwortlichkeiten
## Prozessablauf
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
## Process Flow (Architecture)
```mermaid
flowchart LR
UI[UI: Screen / Fragment / Compose] -->|user action| VM[ViewModel]
VM -->|invoke| UC[Use Case]
UC -->|calls| R[Repository]
R -->|read/write| LDS[Local Data Source\nDB / DataStore]
R -->|fetch| RDS[Remote Data Source\nREST / GraphQL]
RDS -->|DTOs| MAP[Mapper]
LDS -->|Entities| MAP
MAP -->|Domain Model| UC
UC -->|Result| VM
VM -->|StateFlow / LiveData| UI
subgraph Data
R
LDS
RDS
MAP
end

View File

@@ -16,6 +16,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://esri.jfrog.io/artifactory/arcgis") } // <-- NEU
}
}