From f1ae1856f033a4e452e2bee21f90be3ff2dbd683 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Wed, 26 Jun 2024 11:09:04 +0200 Subject: [PATCH] DROID-2398 Multiplayer | Enhancement | Allow picking an image from device as space icon (#1323) --- .../settings/space/SpaceSettingsFragment.kt | 12 ++- .../domain/icon/SetDocumentImageIcon.kt | 3 +- localization/src/main/res/values/strings.xml | 1 + .../spaces/SpaceSettingsViewModel.kt | 53 ++++++++++++- ui-settings/build.gradle | 1 + .../ui_settings/main/MainSettingScreen.kt | 79 +++++++++++++++---- .../anytype/ui_settings/space/Settings.kt | 11 ++- 7 files changed, 136 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt index e5293ad85e..4ba828e805 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt @@ -19,6 +19,7 @@ import com.anytypeio.anytype.core_ui.common.ComposeDialogView import com.anytypeio.anytype.core_ui.extensions.throttledClick import com.anytypeio.anytype.core_utils.clipboard.copyPlainTextToClipboard import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.parseImagePath import com.anytypeio.anytype.core_utils.ext.shareFile import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment @@ -101,7 +102,16 @@ class SpaceSettingsFragment : BaseBottomSheetComposeFragment() { onRandomGradientClicked = vm::onRandomSpaceGradientClicked, onManageSharedSpaceClicked = vm::onManageSharedSpaceClicked, onSharePrivateSpaceClicked = vm::onSharePrivateSpaceClicked, - onAddMoreSpacesClicked = vm::onAddMoreSpacesClicked + onAddMoreSpacesClicked = vm::onAddMoreSpacesClicked, + onSpaceImagePicked = { uri -> + runCatching { + vm.onSpaceImagePicked( + path = uri.parseImagePath(requireContext()) + ) + }.onFailure { + toast(getString(R.string.error_while_loading_picture)) + } + } ) LaunchedEffect(Unit) { vm.toasts.collect { toast(it) } } LaunchedEffect(Unit) { diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/icon/SetDocumentImageIcon.kt b/domain/src/main/java/com/anytypeio/anytype/domain/icon/SetDocumentImageIcon.kt index 10024677ca..ea7381234e 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/icon/SetDocumentImageIcon.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/icon/SetDocumentImageIcon.kt @@ -4,8 +4,9 @@ import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Command import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.domain.block.repo.BlockRepository +import javax.inject.Inject -class SetDocumentImageIcon( +class SetDocumentImageIcon @Inject constructor( private val repo: BlockRepository ) : SetImageIcon() { diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 7ae408e598..20b958fb20 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -60,6 +60,7 @@ Space name Entry space Apply random gradient + Upload image Delete space You can store up to %1$s of your files on our encrypted backup node for free. If you reach the limit, files will be stored only locally. In order to save space on your local device, you can offload all your files to our encrypted backup node. The files will be loaded back when you open them. diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt index c936e1ffdc..05ac84696d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt @@ -9,6 +9,7 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary.screenLeaveSpace import com.anytypeio.anytype.analytics.base.EventsPropertiesKey import com.anytypeio.anytype.analytics.base.sendEvent import com.anytypeio.anytype.analytics.props.Props +import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Filepath import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectWrapper @@ -27,6 +28,9 @@ import com.anytypeio.anytype.core_utils.ui.ViewState import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.config.ConfigStorage import com.anytypeio.anytype.domain.debugging.DebugSpaceShareDownloader +import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon +import com.anytypeio.anytype.domain.icon.SetImageIcon +import com.anytypeio.anytype.domain.media.UploadFile import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer @@ -58,7 +62,8 @@ class SpaceSettingsViewModel( private val userPermissionProvider: UserPermissionProvider, private val spaceViewContainer: SpaceViewSubscriptionContainer, private val activeSpaceMemberSubscriptionContainer: ActiveSpaceMemberSubscriptionContainer, - private val getMembership: GetMembershipStatus + private val getMembership: GetMembershipStatus, + private val uploadFile: UploadFile ): BaseViewModel() { val commands = MutableSharedFlow() @@ -325,6 +330,46 @@ class SpaceSettingsViewModel( } } + fun onSpaceImagePicked(path: String) { + Timber.d("onSpaceImageClicked: $path") + viewModelScope.launch { + uploadFile.async( + params = UploadFile.Params( + path = path, + space = params.space, + type = Block.Content.File.Type.IMAGE + ) + ).fold( + onSuccess = { file -> + proceedWithSettingSpaceIconImage(file) + }, + onFailure = { + Timber.e(it, "Error while uploading image as space icon") + } + ) + } + } + + private suspend fun proceedWithSettingSpaceIconImage(file: ObjectWrapper.File) { + setSpaceDetails.async( + SetSpaceDetails.Params( + space = params.space, + details = mapOf( + Relations.ICON_OPTION to null, + Relations.ICON_IMAGE to file.id, + Relations.ICON_EMOJI to null + ) + ) + ).fold( + onSuccess = { + Timber.d("Successfully set image as space icon.") + }, + onFailure = { e -> + Timber.e(e, "Error while setting image as space icon") + } + ) + } + data class SpaceData( val spaceId: Id?, val createdDateInMillis: Long?, @@ -369,7 +414,8 @@ class SpaceSettingsViewModel( private val spaceGradientProvider: SpaceGradientProvider, private val userPermissionProvider: UserPermissionProvider, private val activeSpaceMemberSubscriptionContainer: ActiveSpaceMemberSubscriptionContainer, - private val getMembership: GetMembershipStatus + private val getMembership: GetMembershipStatus, + private val uploadFile: UploadFile ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( @@ -388,7 +434,8 @@ class SpaceSettingsViewModel( params = params, userPermissionProvider = userPermissionProvider, activeSpaceMemberSubscriptionContainer = activeSpaceMemberSubscriptionContainer, - getMembership = getMembership + getMembership = getMembership, + uploadFile = uploadFile ) as T } diff --git a/ui-settings/build.gradle b/ui-settings/build.gradle index e4e4a9b656..6b4fca4502 100644 --- a/ui-settings/build.gradle +++ b/ui-settings/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation libs.composeFoundation implementation libs.composeMaterial implementation libs.composeToolingPreview + implementation libs.activityCompose implementation libs.coilCompose diff --git a/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/main/MainSettingScreen.kt b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/main/MainSettingScreen.kt index 556479945b..ea80c49452 100644 --- a/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/main/MainSettingScreen.kt +++ b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/main/MainSettingScreen.kt @@ -1,12 +1,19 @@ package com.anytypeio.anytype.ui_settings.main +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -15,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp @@ -26,6 +34,7 @@ import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.views.BodyRegular import com.anytypeio.anytype.presentation.spaces.SpaceIconView import com.anytypeio.anytype.ui_settings.R +import timber.log.Timber @Composable fun SpaceHeader( @@ -34,8 +43,20 @@ fun SpaceHeader( modifier: Modifier = Modifier, onNameSet: (String) -> Unit, onRandomGradientClicked: () -> Unit, - isEditEnabled: Boolean + isEditEnabled: Boolean, + onSpaceImagePicked: (Uri) -> Unit ) { + val context = LocalContext.current + val singlePhotoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> + if (uri != null) { + onSpaceImagePicked(uri) + } else { + Timber.w("Uri was null after picking image") + } + } + ) val isSpaceIconMenuExpanded = remember { mutableStateOf(false) } @@ -56,24 +77,50 @@ fun SpaceHeader( }, gradientCornerRadius = 4.dp ) - DropdownMenu( - expanded = isSpaceIconMenuExpanded.value, - offset = DpOffset(x = 0.dp, y = 6.dp), - onDismissRequest = { - isSpaceIconMenuExpanded.value = false - } + MaterialTheme( + shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(16.dp)) ) { - DropdownMenuItem( - onClick = { - onRandomGradientClicked() + DropdownMenu( + expanded = isSpaceIconMenuExpanded.value, + offset = DpOffset(x = 0.dp, y = 6.dp), + onDismissRequest = { isSpaceIconMenuExpanded.value = false - }, + } ) { - Text( - text = stringResource(R.string.space_settings_apply_random_gradient), - style = BodyRegular, - color = colorResource(id = R.color.text_primary) - ) + DropdownMenuItem( + onClick = { + onRandomGradientClicked() + isSpaceIconMenuExpanded.value = false + }, + ) { + Text( + text = stringResource(R.string.space_settings_apply_random_gradient), + style = BodyRegular, + color = colorResource(id = R.color.text_primary) + ) + } + if (ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(context)) { + Divider( + thickness = 0.5.dp, + color = colorResource(id = R.color.shape_primary) + ) + DropdownMenuItem( + onClick = { + singlePhotoPickerLauncher.launch( + PickVisualMediaRequest( + ActivityResultContracts.PickVisualMedia.ImageOnly + ) + ) + isSpaceIconMenuExpanded.value = false + }, + ) { + Text( + text = stringResource(R.string.space_settings_apply_upload_image), + style = BodyRegular, + color = colorResource(id = R.color.text_primary) + ) + } + } } } } diff --git a/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/Settings.kt b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/Settings.kt index 9858dc6cab..6f9740c4ad 100644 --- a/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/Settings.kt +++ b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/Settings.kt @@ -1,5 +1,6 @@ package com.anytypeio.anytype.ui_settings.space +import android.net.Uri import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,6 +18,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource @@ -68,6 +70,7 @@ fun SpaceSettingsScreen( onSharePrivateSpaceClicked: () -> Unit, onManageSharedSpaceClicked: () -> Unit, onAddMoreSpacesClicked: () -> Unit, + onSpaceImagePicked: (Uri) -> Unit ) { LazyColumn( modifier = Modifier @@ -95,7 +98,8 @@ fun SpaceSettingsScreen( ViewState.Init -> false ViewState.Loading -> false is ViewState.Success -> state.data.permissions.isOwnerOrEditor() - } + }, + onSpaceImagePicked = onSpaceImagePicked ) } item { Divider() } @@ -311,7 +315,7 @@ fun SpaceSettingsScreenPreview() { shareLimitReached = SpaceSettingsViewModel.ShareLimitsState( shareLimitReached = false, sharedSpacesLimit = 0 - ), + ) ) ), onNameSet = {}, @@ -325,7 +329,8 @@ fun SpaceSettingsScreenPreview() { onRandomGradientClicked = {}, onManageSharedSpaceClicked = {}, onSharePrivateSpaceClicked = {}, - onAddMoreSpacesClicked = {} + onAddMoreSpacesClicked = {}, + onSpaceImagePicked = {} ) }