From fcdc737a816ed63891cd9ce01c8a6bfb5d6958e9 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 9 May 2023 18:08:45 +0200 Subject: [PATCH] WIP pick avatar image --- features/createroom/impl/build.gradle.kts | 1 + .../impl/configureroom/ConfigureRoomEvents.kt | 2 - .../configureroom/ConfigureRoomPresenter.kt | 32 ++++- .../impl/configureroom/ConfigureRoomState.kt | 2 + .../ConfigureRoomStateProvider.kt | 9 +- .../impl/configureroom/ConfigureRoomView.kt | 33 ++++- .../impl/configureroom/avatar/AvatarAction.kt | 34 +++++ .../avatar/AvatarActionListEvents.kt | 21 +++ .../avatar/AvatarActionListState.kt | 26 ++++ .../avatar/AvatarActionListStateProvider.kt | 30 +++++ .../avatar/AvatarActionListView.kt | 121 ++++++++++++++++++ .../libraries/mediapickers/api/PickerType.kt | 7 + .../mediapickers/impl/PickerProviderImpl.kt | 14 ++ 13 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListEvents.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListState.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 0515bbbfc7..6a9b0c28ff 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.features.userlist.api) api(projects.features.createroom.api) implementation(libs.coil.compose) // FIXME temp + implementation(projects.libraries.mediapickers) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index f2c454656c..58cdae613c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -16,14 +16,12 @@ package io.element.android.features.createroom.impl.configureroom -import android.net.Uri import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface ConfigureRoomEvents { data class RoomNameChanged(val name: String) : ConfigureRoomEvents data class TopicChanged(val topic: String) : ConfigureRoomEvents - data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents data class CreateRoom(val config: CreateRoomConfig) : ConfigureRoomEvents diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index fa61c7f643..e38b9c06a7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -26,6 +26,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListEvents +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.execute @@ -34,6 +37,8 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility +import io.element.android.libraries.mediapickers.PickerProvider +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -41,6 +46,7 @@ import javax.inject.Inject class ConfigureRoomPresenter @Inject constructor( private val dataStore: CreateRoomDataStore, private val matrixClient: MatrixClient, + private val mediaPickerProvider: PickerProvider, ) : Presenter { @Composable @@ -52,6 +58,13 @@ class ConfigureRoomPresenter @Inject constructor( } } + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri -> + if (uri != null) dataStore.setAvatarUrl(uri.toString()) + }) + val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(onResult = { uri -> + if (uri != null) dataStore.setAvatarUrl(uri.toString()) + }) + val localCoroutineScope = rememberCoroutineScope() val createRoomAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } @@ -62,7 +75,6 @@ class ConfigureRoomPresenter @Inject constructor( fun handleEvents(event: ConfigureRoomEvents) { when (event) { - is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString()) is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name) is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic) is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy) @@ -72,9 +84,27 @@ class ConfigureRoomPresenter @Inject constructor( } } + fun handleAvatarEvents(event: AvatarActionListEvents) { + when (event) { + is AvatarActionListEvents.HandleAction -> when (event.action) { + AvatarAction.ChoosePhoto -> galleryImagePicker.launch() + AvatarAction.TakePhoto -> cameraPhotoPicker.launch() + } + } + } + + val avatarActionListState = AvatarActionListState( + actions = persistentListOf( + AvatarAction.ChoosePhoto, + AvatarAction.TakePhoto, + ), + eventSink = ::handleAvatarEvents, + ) + return ConfigureRoomState( config = createRoomConfig.value, isCreateButtonEnabled = isCreateButtonEnabled, + avatarActionListState = avatarActionListState, createRoomAction = createRoomAction.value, eventSink = ::handleEvents, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index b5f804fcde..1b01c3a642 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -17,12 +17,14 @@ package io.element.android.features.createroom.impl.configureroom import io.element.android.features.createroom.impl.CreateRoomConfig +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId data class ConfigureRoomState( val config: CreateRoomConfig, val isCreateButtonEnabled: Boolean, + val avatarActionListState: AvatarActionListState, val createRoomAction: Async, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 67e0b91bba..3383b13b2a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -18,8 +18,11 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.createroom.impl.CreateRoomConfig +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction +import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListState import io.element.android.features.userlist.api.aListOfSelectedUsers import io.element.android.libraries.architecture.Async +import kotlinx.collections.immutable.persistentListOf open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -40,6 +43,10 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider Unit = {}, onRoomCreated: (RoomId) -> Unit = {}, ) { + val coroutineScope = rememberCoroutineScope() + val itemActionsBottomSheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + ) + if (state.createRoomAction is Async.Success) { LaunchedEffect(state.createRoomAction) { onRoomCreated(state.createRoomAction.state) } } - val context = LocalContext.current + fun onAvatarClicked() { + coroutineScope.launch { + itemActionsBottomSheetState.show() + } + } + Scaffold( modifier = modifier, topBar = { @@ -92,7 +107,7 @@ fun ConfigureRoomView( modifier = Modifier.padding(horizontal = 16.dp), avatarUri = state.config.avatarUrl?.toUri(), roomName = state.config.roomName.orEmpty(), - onAvatarClick = { Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show() }, + onAvatarClick = ::onAvatarClicked, onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) }, ) RoomTopic( @@ -114,10 +129,17 @@ fun ConfigureRoomView( } } + AvatarActionListView( + state = state.avatarActionListState, + modalBottomSheetState = itemActionsBottomSheetState, + onActionSelected = { state.avatarActionListState.eventSink(AvatarActionListEvents.HandleAction(it)) } + ) + when (state.createRoomAction) { is Async.Loading -> { ProgressDialog(text = stringResource(StringR.string.common_creating_room)) } + is Async.Failure -> { RetryDialog( content = stringResource(R.string.screen_create_room_error_creating_room), @@ -125,6 +147,7 @@ fun ConfigureRoomView( onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) }, ) } + else -> Unit } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt new file mode 100644 index 0000000000..029732f173 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom.avatar + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material.icons.filled.PhotoLibrary +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.vector.ImageVector +import io.element.android.libraries.ui.strings.R + +@Immutable +sealed class AvatarAction( + @StringRes val titleResId: Int, + val icon: ImageVector, +) { + object TakePhoto : AvatarAction(titleResId = R.string.action_take_photo, icon = Icons.Default.PhotoCamera) + object ChoosePhoto : AvatarAction(titleResId = R.string.action_choose_photo, icon = Icons.Default.PhotoLibrary) +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListEvents.kt new file mode 100644 index 0000000000..2e81ea6500 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListEvents.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom.avatar + +sealed interface AvatarActionListEvents { + data class HandleAction(val action: AvatarAction) : AvatarActionListEvents +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListState.kt new file mode 100644 index 0000000000..35a34b20a9 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom.avatar + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +data class AvatarActionListState( + val actions: ImmutableList, + val eventSink: (AvatarActionListEvents) -> Unit, +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt new file mode 100644 index 0000000000..6cb6ce1361 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListStateProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom.avatar + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import kotlinx.collections.immutable.persistentListOf + +open class ActionListStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(anActionListState()) +} + +fun anActionListState() = AvatarActionListState( + actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto), + eventSink = {} +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt new file mode 100644 index 0000000000..515faee0f5 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialApi::class) + +package io.element.android.features.createroom.impl.configureroom.avatar + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ListItem +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout +import kotlinx.coroutines.launch + +@Composable +fun AvatarActionListView( + state: AvatarActionListState, + modalBottomSheetState: ModalBottomSheetState, + modifier: Modifier = Modifier, + onActionSelected: (action: AvatarAction) -> Unit = {}, +) { + val coroutineScope = rememberCoroutineScope() + fun onItemActionClicked(itemAction: AvatarAction) { + onActionSelected(itemAction) + coroutineScope.launch { + modalBottomSheetState.hide() + } + } + + ModalBottomSheetLayout( + modifier = modifier, + sheetState = modalBottomSheetState, + sheetContent = { + SheetContent( + state = state, + onActionClicked = ::onItemActionClicked, + modifier = Modifier + .navigationBarsPadding() + .imePadding() + ) + } + ) +} + +@Composable +private fun SheetContent( + state: AvatarActionListState, + modifier: Modifier = Modifier, + onActionClicked: (AvatarAction) -> Unit = { }, +) { + val actions = state.actions + LazyColumn( + modifier = modifier.fillMaxWidth() + ) { + items( + items = actions, + ) { action -> + ListItem( + modifier = Modifier.clickable { onActionClicked(action) }, + text = { + Text(text = stringResource(action.titleResId)) + }, + icon = { + Icon( + imageVector = action.icon, + contentDescription = stringResource(action.titleResId), + ) + } + ) + } + } +} + +@Preview +@Composable +fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: AvatarActionListState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: AvatarActionListState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: AvatarActionListState) { + AvatarActionListView( + state = state, + modalBottomSheetState = ModalBottomSheetState( + initialValue = ModalBottomSheetValue.Expanded + ), + ) +} diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt index ad89175ddf..7c86009d34 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerType.kt @@ -26,6 +26,13 @@ sealed interface PickerType { fun getContract(): ActivityResultContract fun getDefaultRequest(): Input + object Image : PickerType { + override fun getContract() = ActivityResultContracts.PickVisualMedia() + override fun getDefaultRequest(): PickVisualMediaRequest { + return PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + } + } + object ImageAndVideo : PickerType { override fun getContract() = ActivityResultContracts.PickVisualMedia() override fun getDefaultRequest(): PickVisualMediaRequest { diff --git a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt index b7e9bad420..11f172fd4c 100644 --- a/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt +++ b/libraries/mediapickers/impl/src/main/kotlin/io/element/android/libraries/mediapickers/impl/PickerProviderImpl.kt @@ -59,6 +59,20 @@ class PickerProviderImpl constructor(private val isInTest: Boolean) : PickerProv } } + /** + * Remembers and returns a [PickerLauncher] for a gallery picture. + * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected. + */ + @Composable + fun registerGalleryImagePicker(onResult: (Uri?) -> Unit): PickerLauncher { + // Tests and UI preview can't handle Contexts, so we might as well disable the whole picker + return if (LocalInspectionMode.current || isInTest) { + NoOpPickerLauncher { onResult(null) } + } else { + rememberPickerLauncher(type = PickerType.Image) { uri -> onResult(uri) } + } + } + /** * Remembers and returns a [PickerLauncher] for a gallery item, either a picture or a video. * [onResult] will be called with either the selected file's [Uri] or `null` if nothing was selected.