Attachments hinzufügen
This commit is contained in:
@@ -22,10 +22,22 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2
|
||||||
|
|
||||||
|
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.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.AlbumViewState
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.Intent
|
||||||
|
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() {
|
||||||
|
//region View State
|
||||||
|
private val _albumViewState: MutableStateFlow<AlbumViewState> = MutableStateFlow(AlbumViewState())
|
||||||
|
// exposes the ViewState to the composable view
|
||||||
|
val viewStateFlow: StateFlow<AlbumViewState>
|
||||||
|
get() = _albumViewState
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Intents
|
||||||
|
// receives user generated events and processes them in the provided coroutine context
|
||||||
|
fun onReceive(intent: Intent) = viewModelScope.launch(coroutineContext) {
|
||||||
|
when(intent) {
|
||||||
|
is Intent.OnPermissionGrantedWith -> {
|
||||||
|
// Create an empty image file in the app's cache directory
|
||||||
|
val tempFile = File.createTempFile(
|
||||||
|
"temp_image_file_", /* prefix */
|
||||||
|
".jpg", /* suffix */
|
||||||
|
intent.compositionContext.cacheDir /* cache directory */
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 */
|
||||||
|
tempFile
|
||||||
|
)
|
||||||
|
_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.OnFinishPickingImagesWith -> {
|
||||||
|
if (intent.imageUrls.isNotEmpty()) {
|
||||||
|
// Handle picked images
|
||||||
|
val newImages = mutableListOf<ImageBitmap>()
|
||||||
|
val imageUriList = _albumViewState.value.imageUris.toMutableList()
|
||||||
|
for (eachImageUrl in intent.imageUrls) {
|
||||||
|
val inputStream = intent.compositionContext.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)
|
||||||
|
newImages.add(bitmap.asImageBitmap())
|
||||||
|
imageUriList.add(eachImageUrl)
|
||||||
|
} 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,
|
||||||
|
imageUris = imageUriList
|
||||||
|
)
|
||||||
|
_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 currentPictures = _albumViewState.value.selectedPictures.toMutableList()
|
||||||
|
currentPictures.add(ImageDecoder.decodeBitmap(source).asImageBitmap())
|
||||||
|
|
||||||
|
val photoUriList = _albumViewState.value.imageUris.toMutableList()
|
||||||
|
photoUriList.add(tempImageUrl)
|
||||||
|
|
||||||
|
_albumViewState.value = _albumViewState.value.copy(
|
||||||
|
tempFileUrl = null,
|
||||||
|
selectedPictures = currentPictures,
|
||||||
|
imageUris = photoUriList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Intent.OnImageSavingCanceled -> {
|
||||||
|
_albumViewState.value = _albumViewState.value.copy(tempFileUrl = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import com.arcgismaps.mapping.layers.FeatureLayer
|
|||||||
import com.arcgismaps.mapping.view.ScreenCoordinate
|
import com.arcgismaps.mapping.view.ScreenCoordinate
|
||||||
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
|
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
|
||||||
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
|
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
|
||||||
import de.jadehs.strassenschadenpro2.pages.SettingsPage
|
import de.jadehs.strassenschadenpro2.components.album.PhotoData
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MapViewModel(application: Application): AndroidViewModel(application) {
|
class MapViewModel(application: Application): AndroidViewModel(application) {
|
||||||
@@ -119,7 +119,7 @@ class MapViewModel(application: Application): AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createFeatureWithAttributesAt(screenCoordinate: ScreenCoordinate, beschreibung: String, typ: String) {
|
fun createFeatureWithAttributesAt(screenCoordinate: ScreenCoordinate, beschreibung: String, typ: String,photosFromAlbum: List<PhotoData>) {
|
||||||
// Create the feature.
|
// Create the feature.
|
||||||
val feature = serviceFeatureTable?.createFeature()?.apply {
|
val feature = serviceFeatureTable?.createFeature()?.apply {
|
||||||
// Get the normalized geometry for the tapped location and use it as the feature's geometry.
|
// Get the normalized geometry for the tapped location and use it as the feature's geometry.
|
||||||
@@ -138,13 +138,28 @@ class MapViewModel(application: Application): AndroidViewModel(application) {
|
|||||||
serviceFeatureTable?.applyEdits()
|
serviceFeatureTable?.applyEdits()
|
||||||
// Update the feature to get the updated objectid - a temporary ID is used before the feature is added.
|
// Update the feature to get the updated objectid - a temporary ID is used before the feature is added.
|
||||||
it.refresh()
|
it.refresh()
|
||||||
|
|
||||||
|
var arcgisFeature = it as? ArcGISFeature
|
||||||
|
if (arcgisFeature != null && photosFromAlbum.size>0){
|
||||||
|
for (photoData in photosFromAlbum){
|
||||||
|
arcgisFeature.addAttachment(photoData.name,photoData.contentType,photoData.data)
|
||||||
|
}
|
||||||
|
serviceFeatureTable?.updateFeature(arcgisFeature)
|
||||||
|
serviceFeatureTable?.applyEdits()
|
||||||
|
}
|
||||||
|
|
||||||
// Confirm feature addition.
|
// Confirm feature addition.
|
||||||
snackBarMessage = "Created feature ${it.attributes["objectid"]}"
|
snackBarMessage = "Created feature ${it.attributes["objectid"]}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createFeatureWithAttributesAtPoint(point: Point, beschreibung: String, typ: String) {
|
fun createFeatureWithAttributesAtPoint(
|
||||||
|
point: Point,
|
||||||
|
beschreibung: String,
|
||||||
|
typ: String,
|
||||||
|
photoDataList: List<PhotoData>
|
||||||
|
) {
|
||||||
// Create the feature.
|
// Create the feature.
|
||||||
val feature = serviceFeatureTable?.createFeature()?.apply {
|
val feature = serviceFeatureTable?.createFeature()?.apply {
|
||||||
// Get the normalized geometry for the tapped location and use it as the feature's geometry.
|
// Get the normalized geometry for the tapped location and use it as the feature's geometry.
|
||||||
@@ -163,6 +178,16 @@ class MapViewModel(application: Application): AndroidViewModel(application) {
|
|||||||
serviceFeatureTable?.applyEdits()
|
serviceFeatureTable?.applyEdits()
|
||||||
// Update the feature to get the updated objectid - a temporary ID is used before the feature is added.
|
// Update the feature to get the updated objectid - a temporary ID is used before the feature is added.
|
||||||
it.refresh()
|
it.refresh()
|
||||||
|
|
||||||
|
var arcgisFeature = it as? ArcGISFeature
|
||||||
|
if (arcgisFeature != null && photoDataList.size>0){
|
||||||
|
for (photoData in photoDataList){
|
||||||
|
arcgisFeature.addAttachment(photoData.name,photoData.contentType,photoData.data)
|
||||||
|
}
|
||||||
|
serviceFeatureTable?.updateFeature(arcgisFeature)
|
||||||
|
serviceFeatureTable?.applyEdits()
|
||||||
|
}
|
||||||
|
|
||||||
// Confirm feature addition.
|
// Confirm feature addition.
|
||||||
snackBarMessage = "Created feature ${it.attributes["objectid"]}"
|
snackBarMessage = "Created feature ${it.attributes["objectid"]}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2.components.album
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User generated events that can be triggered from the UI.
|
||||||
|
*/
|
||||||
|
sealed class Intent {
|
||||||
|
data class OnPermissionGrantedWith(val compositionContext: Context): Intent()
|
||||||
|
data object OnPermissionDenied: Intent()
|
||||||
|
data class OnImageSavedWith (val compositionContext: Context): Intent()
|
||||||
|
data object OnImageSavingCanceled: Intent()
|
||||||
|
data class OnFinishPickingImagesWith(val compositionContext: Context, val imageUrls: List<Uri>): Intent()
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2.components.album
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
|
||||||
|
data class AlbumViewState(
|
||||||
|
/**
|
||||||
|
* holds the URL of the temporary file which stores the image taken by the camera.
|
||||||
|
*/
|
||||||
|
val tempFileUrl: Uri? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* holds the list of images taken by camera or selected pictures from the gallery.
|
||||||
|
*/
|
||||||
|
val selectedPictures: List<ImageBitmap> = emptyList(),
|
||||||
|
|
||||||
|
val imageUris: List<Uri> = emptyList()
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package de.jadehs.strassenschadenpro2.components.album
|
||||||
|
|
||||||
|
data class PhotoData(
|
||||||
|
val name: String,
|
||||||
|
val contentType: String,
|
||||||
|
val data: ByteArray)
|
||||||
@@ -1,12 +1,26 @@
|
|||||||
package de.jadehs.strassenschadenpro2.pages
|
package de.jadehs.strassenschadenpro2.pages
|
||||||
|
|
||||||
import android.util.Log
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
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.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.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.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
@@ -14,11 +28,14 @@ import androidx.compose.material3.OutlinedTextField
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -28,8 +45,12 @@ import com.arcgismaps.location.LocationDisplayAutoPanMode
|
|||||||
import com.arcgismaps.toolkit.geoviewcompose.MapView
|
import com.arcgismaps.toolkit.geoviewcompose.MapView
|
||||||
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
|
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
|
import de.jadehs.strassenschadenpro2.AlbumViewModel
|
||||||
import de.jadehs.strassenschadenpro2.MapViewModel
|
import de.jadehs.strassenschadenpro2.MapViewModel
|
||||||
import de.jadehs.strassenschadenpro2.components.DropDownMenuBox
|
import de.jadehs.strassenschadenpro2.components.DropDownMenuBox
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.AlbumViewState
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.Intent
|
||||||
|
import de.jadehs.strassenschadenpro2.components.album.PhotoData
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -43,12 +64,16 @@ fun CreatePage(modifier: Modifier = Modifier, mapViewModel: MapViewModel) {
|
|||||||
|
|
||||||
var enableSaveButton = remember { mutableStateOf(false) }
|
var enableSaveButton = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var albumViewModel = remember { AlbumViewModel(coroutineScope.coroutineContext) }
|
||||||
|
|
||||||
val locationDisplay = rememberLocationDisplay().apply {
|
val locationDisplay = rememberLocationDisplay().apply {
|
||||||
setAutoPanMode(LocationDisplayAutoPanMode.Off)
|
setAutoPanMode(LocationDisplayAutoPanMode.Off)
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
if (checkPermissions(context)) {
|
if (checkPermissions(context)) {
|
||||||
// Permissions are already granted.
|
// Permissions are already granted.
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -68,6 +93,7 @@ fun CreatePage(modifier: Modifier = Modifier, mapViewModel: MapViewModel) {
|
|||||||
val fuesedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
|
val fuesedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxSize()
|
Column(modifier = modifier.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
|
.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally) {
|
horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
@@ -92,6 +118,7 @@ fun CreatePage(modifier: Modifier = Modifier, mapViewModel: MapViewModel) {
|
|||||||
enableSaveButton.value = beschreibungTextFieldValue.value.text.isNotEmpty() && currentDamageType.value.isNotEmpty()
|
enableSaveButton.value = beschreibungTextFieldValue.value.text.isNotEmpty() && currentDamageType.value.isNotEmpty()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
AlbumScreen(modifier= Modifier, viewModel = albumViewModel)
|
||||||
Row(modifier = Modifier.fillMaxWidth(),
|
Row(modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly){
|
horizontalArrangement = Arrangement.SpaceEvenly){
|
||||||
Button(enabled = enableSaveButton.value,
|
Button(enabled = enableSaveButton.value,
|
||||||
@@ -103,7 +130,8 @@ fun CreatePage(modifier: Modifier = Modifier, mapViewModel: MapViewModel) {
|
|||||||
var point = Point(location.longitude,
|
var point = Point(location.longitude,
|
||||||
location.latitude,
|
location.latitude,
|
||||||
SpatialReference.wgs84())
|
SpatialReference.wgs84())
|
||||||
mapViewModel.createFeatureWithAttributesAtPoint(point,beschreibung,typ)
|
val photoDataList = getPhotosFromAlbum(context,albumViewModel)
|
||||||
|
mapViewModel.createFeatureWithAttributesAtPoint(point,beschreibung,typ,photoDataList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}){
|
}){
|
||||||
@@ -133,10 +161,140 @@ fun CreatePage(modifier: Modifier = Modifier, mapViewModel: MapViewModel) {
|
|||||||
val screenCoordinate = singleTapConfirmedEvent.screenCoordinate
|
val screenCoordinate = singleTapConfirmedEvent.screenCoordinate
|
||||||
val beschreibung = beschreibungTextFieldValue.value.text
|
val beschreibung = beschreibungTextFieldValue.value.text
|
||||||
val typ = currentDamageType.value
|
val typ = currentDamageType.value
|
||||||
mapViewModel.createFeatureWithAttributesAt(screenCoordinate,beschreibung,typ)
|
val photoDataList=getPhotosFromAlbum(context,albumViewModel)
|
||||||
|
mapViewModel.createFeatureWithAttributesAt(
|
||||||
|
screenCoordinate,
|
||||||
|
beschreibung,
|
||||||
|
typ,
|
||||||
|
photoDataList
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AlbumScreen(modifier: Modifier = Modifier, viewModel: AlbumViewModel) {
|
||||||
|
// collecting the flow from the view model as a state allows our ViewModel and View
|
||||||
|
// to be in sync with each other.
|
||||||
|
val viewState: AlbumViewState by viewModel.viewStateFlow.collectAsState()
|
||||||
|
|
||||||
|
val currentContext = LocalContext.current
|
||||||
|
|
||||||
|
// launches photo picker
|
||||||
|
val pickImageFromAlbumLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { urls ->
|
||||||
|
viewModel.onReceive(Intent.OnFinishPickingImagesWith(currentContext, urls))
|
||||||
|
}
|
||||||
|
|
||||||
|
// launches camera
|
||||||
|
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { isImageSaved ->
|
||||||
|
if (isImageSaved) {
|
||||||
|
viewModel.onReceive(Intent.OnImageSavedWith(currentContext))
|
||||||
|
} else {
|
||||||
|
// handle image saving error or cancellation
|
||||||
|
viewModel.onReceive(Intent.OnImageSavingCanceled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// launches camera permissions
|
||||||
|
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted ->
|
||||||
|
if (permissionGranted) {
|
||||||
|
viewModel.onReceive(Intent.OnPermissionGrantedWith(currentContext))
|
||||||
|
} else {
|
||||||
|
// handle permission denied such as:
|
||||||
|
viewModel.onReceive(Intent.OnPermissionDenied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this ensures that the camera is launched only once when the url of the temp file changes
|
||||||
|
LaunchedEffect(key1 = viewState.tempFileUrl) {
|
||||||
|
viewState.tempFileUrl?.let {
|
||||||
|
cameraLauncher.launch(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic view that has 2 buttons and a grid for selected pictures
|
||||||
|
Column(modifier = Modifier.padding(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Row {
|
||||||
|
Button(onClick = {
|
||||||
|
// get user's permission first to use camera
|
||||||
|
permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
}) {
|
||||||
|
Text(text = "Take a photo")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Button(onClick = {
|
||||||
|
// Image picker does not require special permissions and can be activated right away
|
||||||
|
val mediaRequest = PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
pickImageFromAlbumLauncher.launch(mediaRequest)
|
||||||
|
}) {
|
||||||
|
Text(text = "Pick a picture")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(text = "Selected Pictures")
|
||||||
|
LazyVerticalGrid(modifier = Modifier.fillMaxWidth().heightIn(0.dp, 1200.dp),
|
||||||
|
columns = GridCells.Adaptive(150.dp),
|
||||||
|
userScrollEnabled = false) {
|
||||||
|
itemsIndexed(viewState.selectedPictures) { index, picture ->
|
||||||
|
Image(modifier = Modifier.padding(8.dp),
|
||||||
|
bitmap = picture,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPhotosFromAlbum(context: Context,albumViewModel: AlbumViewModel): List<PhotoData>{
|
||||||
|
var photoDataList = mutableListOf<PhotoData>()
|
||||||
|
|
||||||
|
albumViewModel.viewStateFlow.value.imageUris.forEach { uri ->
|
||||||
|
context.contentResolver.openInputStream(uri).use { inputStream ->
|
||||||
|
try {
|
||||||
|
val name = "image_${System.currentTimeMillis()}.png"
|
||||||
|
val contentType = "image/png"
|
||||||
|
val byteArray = inputStream?.readBytes()
|
||||||
|
|
||||||
|
if (byteArray!=null) {
|
||||||
|
photoDataList.add(
|
||||||
|
PhotoData(
|
||||||
|
name,
|
||||||
|
contentType,
|
||||||
|
byteArray
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}catch (e: Exception){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return photoDataList
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5
app/src/main/res/xml/file_paths.xml
Normal file
5
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<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>
|
||||||
Reference in New Issue
Block a user