From 5264490f3bcefbea7f2a8b1a4cf8a2e83a14ba01 Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Fri, 4 Apr 2025 09:30:16 +0200 Subject: [PATCH] DROID-3484 Primitives | Create type screen, part 1 (#2229) --- .../anytype/di/common/ComponentManager.kt | 8 + .../di/feature/PrimitivesObjectTypeDI.kt | 58 +++++- .../anytype/di/main/MainComponent.kt | 9 +- .../anytypeio/anytype/navigation/Navigator.kt | 11 + .../ui/allcontent/AllContentFragment.kt | 7 +- .../ui/primitives/CreateTypeFragment.kt | 128 ++++++++++++ app/src/main/res/navigation/graph.xml | 4 + .../anytypeio/anytype/core_models/Command.kt | 6 + .../auth/repo/block/BlockDataRepository.kt | 12 +- .../data/auth/repo/block/BlockRemote.kt | 6 +- .../domain/block/repo/BlockRepository.kt | 6 +- .../anytype/domain/types/CreateObjectType.kt | 27 ++- .../presentation/AllContentViewModel.kt | 39 +--- .../ui/create/CreateNewTypeScreen.kt | 20 ++ .../feature_object_type/ui/create/UiState.kt | 20 ++ .../viewmodel/CreateObjectTypeViewModel.kt | 190 ++++++++++++++++++ localization/src/main/res/values/strings.xml | 5 + .../middleware/block/BlockMiddleware.kt | 10 +- .../middleware/interactor/Middleware.kt | 20 +- .../presentation/navigation/AppNavigation.kt | 2 + .../types/CreateObjectTypeViewModel.kt | 153 -------------- 21 files changed, 500 insertions(+), 241 deletions(-) create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/primitives/CreateTypeFragment.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/create/CreateNewTypeScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/create/UiState.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/CreateObjectTypeViewModel.kt delete mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/types/CreateObjectTypeViewModel.kt diff --git a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt index 74fd0698e1..3abb87ac83 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt @@ -8,6 +8,7 @@ import com.anytypeio.anytype.di.feature.CreateObjectModule import com.anytypeio.anytype.di.feature.DaggerAllContentComponent import com.anytypeio.anytype.di.feature.DaggerAppPreferencesComponent import com.anytypeio.anytype.di.feature.DaggerBacklinkOrAddToObjectComponent +import com.anytypeio.anytype.di.feature.DaggerCreateObjectTypeComponent import com.anytypeio.anytype.di.feature.DaggerDateObjectComponent import com.anytypeio.anytype.di.feature.DaggerEditTypePropertiesComponent import com.anytypeio.anytype.di.feature.DaggerLinkToObjectComponent @@ -106,6 +107,7 @@ import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams import com.anytypeio.anytype.feature_chats.presentation.SelectChatReactionViewModel import com.anytypeio.anytype.feature_date.viewmodel.DateObjectVmParams +import com.anytypeio.anytype.feature_object_type.viewmodel.CreateTypeVmParams import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel import com.anytypeio.anytype.presentation.editor.EditorViewModel @@ -1113,6 +1115,12 @@ class ComponentManager( .create(params, findComponentDependencies()) } + val createObjectTypeComponent = ComponentWithParams { params: CreateTypeVmParams -> + DaggerCreateObjectTypeComponent + .factory() + .create(params, findComponentDependencies()) + } + class Component(private val builder: () -> T) { private var instance: T? = null diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt index 0bcd0f3b0b..0b2f9172ce 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt @@ -26,11 +26,15 @@ import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFie import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.search.SubscriptionEventChannel +import com.anytypeio.anytype.domain.types.CreateObjectType import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams +import com.anytypeio.anytype.feature_object_type.viewmodel.CreateObjectTypeVMFactory +import com.anytypeio.anytype.feature_object_type.viewmodel.CreateTypeVmParams import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeVMFactory import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider import com.anytypeio.anytype.providers.DefaultCoverImageHashProvider +import com.anytypeio.anytype.ui.primitives.CreateTypeFragment import com.anytypeio.anytype.ui.primitives.ObjectTypeFieldsFragment import com.anytypeio.anytype.ui.primitives.ObjectTypeFragment import dagger.Binds @@ -163,4 +167,56 @@ interface ObjectTypeDependencies : ComponentDependencies { fun fieldParser(): FieldParser fun provideEventChannel(): EventChannel fun provideStringResourceProvider(): StringResourceProvider -} \ No newline at end of file +} + +//region Create Type Screen +@Component( + dependencies = [CreateObjectTypeDependencies::class], + modules = [ + CreateObjectTypeModule::class, + CreateObjectTypeModule.Declarations::class + ] +) +@PerScreen +interface CreateObjectTypeComponent { + @Component.Factory + interface Factory { + fun create( + @BindsInstance vmParams: CreateTypeVmParams, + dependencies: CreateObjectTypeDependencies + ) + : CreateObjectTypeComponent + } + + fun inject(fragment: CreateTypeFragment) +} + +@Module +object CreateObjectTypeModule { + + @Provides + @PerScreen + @JvmStatic + fun createObjectType( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): CreateObjectType = CreateObjectType(repo, dispatchers) + + @Module + interface Declarations { + @PerScreen + @Binds + fun bindViewModelFactory( + factory: CreateObjectTypeVMFactory + ): ViewModelProvider.Factory + } +} + +interface CreateObjectTypeDependencies : ComponentDependencies { + fun stringResourceProvider(): StringResourceProvider + fun blockRepository(): BlockRepository + fun analyticsHelper(): AnalyticSpaceHelperDelegate + fun analytics(): Analytics + fun dispatchers(): AppCoroutineDispatchers +} +//endregion \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt b/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt index ddf28af6b6..226036655a 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt @@ -8,6 +8,7 @@ import com.anytypeio.anytype.di.feature.AppPreferencesDependencies import com.anytypeio.anytype.di.feature.BacklinkOrAddToObjectDependencies import com.anytypeio.anytype.di.feature.CreateBookmarkSubComponent import com.anytypeio.anytype.di.feature.CreateObjectSubComponent +import com.anytypeio.anytype.di.feature.CreateObjectTypeDependencies import com.anytypeio.anytype.di.feature.DateObjectDependencies import com.anytypeio.anytype.di.feature.DebugSettingsSubComponent import com.anytypeio.anytype.di.feature.EditTypePropertiesDependencies @@ -137,7 +138,8 @@ interface MainComponent : ChatReactionDependencies, ParticipantComponentDependencies, EditTypePropertiesDependencies, - DebugDependencies + DebugDependencies, + CreateObjectTypeDependencies { fun inject(app: AndroidApplication) @@ -393,4 +395,9 @@ abstract class ComponentDependenciesModule { @IntoMap @ComponentDependenciesKey(EditTypePropertiesDependencies::class) abstract fun provideEditTypePropertiesDependencies(component: MainComponent): ComponentDependencies + + @Binds + @IntoMap + @ComponentDependenciesKey(CreateObjectTypeDependencies::class) + abstract fun provideCreateObjectTypeDependencies(component: MainComponent): ComponentDependencies } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt index db59b0dc8b..fc5cc7f4c0 100644 --- a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt +++ b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt @@ -16,6 +16,7 @@ import com.anytypeio.anytype.ui.date.DateObjectFragment import com.anytypeio.anytype.ui.editor.EditorFragment import com.anytypeio.anytype.ui.editor.EditorModalFragment import com.anytypeio.anytype.ui.multiplayer.ShareSpaceFragment +import com.anytypeio.anytype.ui.primitives.CreateTypeFragment import com.anytypeio.anytype.ui.primitives.ObjectTypeFieldsFragment import com.anytypeio.anytype.ui.profile.ParticipantFragment import com.anytypeio.anytype.ui.primitives.ObjectTypeFragment @@ -294,6 +295,16 @@ class Navigator : AppNavigation { ) } + override fun openCreateObjectTypeScreen(spaceId: Id) { + val args = CreateTypeFragment.args( + spaceId = spaceId + ) + navController?.navigate( + resId = R.id.createObjectTypeScreen, + args = args + ) + } + override fun openDateObject( objectId: Id, space: Id diff --git a/app/src/main/java/com/anytypeio/anytype/ui/allcontent/AllContentFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/allcontent/AllContentFragment.kt index bb51da113b..2cea002f43 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/allcontent/AllContentFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/allcontent/AllContentFragment.kt @@ -186,7 +186,12 @@ class AllContentFragment : BaseComposeFragment(), ObjectTypeSelectionListener { } is AllContentViewModel.Command.OpenTypeCreation -> { - //todo: implement new screen logic + runCatching { + navigation().openCreateObjectTypeScreen(spaceId = command.space) + }.onFailure { + toast("Failed to open type creation screen") + Timber.e(it, "Failed to open type creation screen from all content") + } } is AllContentViewModel.Command.OpenRelationCreation -> { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/primitives/CreateTypeFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/primitives/CreateTypeFragment.kt new file mode 100644 index 0000000000..066357e0d5 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/CreateTypeFragment.kt @@ -0,0 +1,128 @@ +package com.anytypeio.anytype.ui.primitives + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.fragment.compose.content +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavOptions +import androidx.navigation.fragment.findNavController +import androidx.navigation.navOptions +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.feature_object_type.ui.UiIconsPickerState +import com.anytypeio.anytype.feature_object_type.ui.create.SetTypeTitlesAndIconScreen +import com.anytypeio.anytype.feature_object_type.ui.icons.ChangeIconScreen +import com.anytypeio.anytype.feature_object_type.viewmodel.CreateObjectTypeVMFactory +import com.anytypeio.anytype.feature_object_type.viewmodel.CreateObjectTypeViewModel +import com.anytypeio.anytype.feature_object_type.viewmodel.CreateTypeCommand +import com.anytypeio.anytype.feature_object_type.viewmodel.CreateTypeVmParams +import com.anytypeio.anytype.ui.base.navigation +import javax.inject.Inject +import kotlin.getValue +import timber.log.Timber + +class CreateTypeFragment: BaseBottomSheetComposeFragment() { + + @Inject + lateinit var factory: CreateObjectTypeVMFactory + private val vm by viewModels { factory } + + private val space get() = argString(ARG_SPACE_ID) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = content { + MaterialTheme { + SetTypeTitlesAndIconScreen( + uiState = vm.uiState.collectAsStateWithLifecycle().value, + onDismiss = vm::onDismiss, + onTitleChanged = vm::onTypeTitleChanged, + onPluralChanged = vm::onTypePluralChanged, + onIconClicked = vm::onIconClicked, + onButtonClicked = vm::onButtonClicked + ) + IconsPickerScreen() + LaunchedEffect(Unit) { + vm.commands.collect{ command -> + when (command) { + CreateTypeCommand.Dismiss -> { + findNavController().popBackStack() + } + + is CreateTypeCommand.NavigateToObjectType -> { + runCatching { + findNavController().navigate( + resId = R.id.objectTypeScreen, + args = ObjectTypeFragment.args( + objectId = command.id, + space = command.space + ), + navOptions = navOptions { + popUpTo(R.id.createObjectTypeScreen) { + inclusive = true + } + } + ) + }.onFailure { + Timber.e(it, "Failed to open object type object from create type Screen") + } + } + } + } + } + } + } + + @Composable + private fun IconsPickerScreen() { + val uiState = vm.uiIconsPickerScreen.collectAsStateWithLifecycle().value + if (uiState is UiIconsPickerState.Visible) { + ChangeIconScreen( + modifier = Modifier.fillMaxWidth(), + onDismissRequest = vm::onDismissIconPicker, + onIconClicked = { name, color -> + vm.onNewIconPicked( + iconName = name, + color = color + ) + }, + onRemoveIconClicked = vm::onRemoveIcon + ) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + } + + override fun injectDependencies() { + val vmParams = CreateTypeVmParams(spaceId = space) + componentManager().createObjectTypeComponent.get(vmParams).inject(this) + } + + override fun releaseDependencies() { + componentManager().createObjectTypeComponent.release() + } + + companion object { + const val ARG_SPACE_ID = "arg.create_type.space_id" + + fun args(spaceId: Id) = bundleOf( + ARG_SPACE_ID to spaceId + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/navigation/graph.xml b/app/src/main/res/navigation/graph.xml index aa39f269ae..3b7523a3ed 100644 --- a/app/src/main/res/navigation/graph.xml +++ b/app/src/main/res/navigation/graph.xml @@ -691,6 +691,10 @@ android:id="@+id/multiplayerFeatureDialog" android:name="com.anytypeio.anytype.ui.multiplayer.IntroduceSpaceSharingFragment"/> + + diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt index dbfd488ef6..b6eb1052b6 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt @@ -480,6 +480,12 @@ sealed class Command { val shouldApplyEmptyUseCase: Boolean ) + data class CreateObjectType( + val details: Struct, + val spaceId: Id, + val internalFlags: List + ) + data class AddObjectToSpace(val space: Id, val objectId: Id) data class ApplyTemplate(val objectId: Id, val template: Id?) diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt index 59a56a1760..cceb4287de 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -609,15 +609,9 @@ class BlockDataRepository( prefilled = prefilled ) - override suspend fun createType( - space: Id, - name: String, - emojiUnicode: String?, - ): Struct? = remote.createType( - space = space, - name = name, - emojiUnicode = emojiUnicode - ) + override suspend fun createType(command: Command.CreateObjectType): String { + return remote.createType(command) + } override suspend fun createRelationOption( space: Id, diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt index 89d51fa7f8..fed48157d2 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt @@ -260,10 +260,8 @@ interface BlockRemote { ): ObjectWrapper.Relation suspend fun createType( - space: Id, - name: String, - emojiUnicode: String? - ): Struct? + command: Command.CreateObjectType + ): String suspend fun createRelationOption( space: Id, diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt index 9f2e7a254b..b4939aa56d 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt @@ -314,10 +314,8 @@ interface BlockRepository { ) : ObjectWrapper.Relation suspend fun createType( - space: Id, - name: String, - emojiUnicode: String? - ): Struct? + command: Command.CreateObjectType + ): String suspend fun createRelationOption( space: Id, diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/types/CreateObjectType.kt b/domain/src/main/java/com/anytypeio/anytype/domain/types/CreateObjectType.kt index 3fd73bb7cb..1155cd9dcd 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/types/CreateObjectType.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/types/CreateObjectType.kt @@ -1,8 +1,8 @@ package com.anytypeio.anytype.domain.types +import com.anytypeio.anytype.core_models.Command import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.ObjectWrapper -import com.anytypeio.anytype.core_models.ext.mapToObjectWrapperType +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.base.ResultInteractor import com.anytypeio.anytype.domain.block.repo.BlockRepository @@ -10,19 +10,26 @@ import com.anytypeio.anytype.domain.block.repo.BlockRepository class CreateObjectType( private val repo: BlockRepository, dispatchers: AppCoroutineDispatchers -) : ResultInteractor(dispatchers.io) { - override suspend fun doWork(params: Params): ObjectWrapper.Type? { - val result = repo.createType( - space = params.space, - name = params.name, - emojiUnicode = params.emojiUnicode +) : ResultInteractor(dispatchers.io) { + override suspend fun doWork(params: Params): String { + val command = Command.CreateObjectType( + details = mapOf( + Relations.NAME to params.name, + Relations.ICON_NAME to params.iconName, + Relations.PLURAL_NAME to params.pluralName, + Relations.ICON_OPTION to params.iconColor + ), + spaceId = params.space, + internalFlags = listOf() ) - return result?.mapToObjectWrapperType() + return repo.createType(command) } class Params( val space: Id, val name: String, - val emojiUnicode: String? = null + val iconName: String, + val pluralName: String, + val iconColor: Double? ) } \ No newline at end of file diff --git a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModel.kt b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModel.kt index 727f52995c..c66d5afe2d 100644 --- a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModel.kt +++ b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModel.kt @@ -4,12 +4,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.analytics.base.EventsDictionary -import com.anytypeio.anytype.analytics.base.EventsDictionary.libraryCreateType import com.anytypeio.anytype.analytics.base.EventsDictionary.libraryScreenRelation import com.anytypeio.anytype.analytics.base.EventsDictionary.libraryScreenType -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.DVSortType import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectWrapper @@ -838,39 +835,7 @@ class AllContentViewModel( when (item) { UiContentItem.NewType -> { viewModelScope.launch { - createObjectType.execute( - CreateObjectType.Params( - space = vmParams.spaceId.id, - name = "" - ) - ).fold( - onSuccess = { newType -> - val spaceParams = provideParams(vmParams.spaceId.id) - viewModelScope.sendEvent( - analytics = analytics, - eventName = libraryCreateType, - props = Props( - mapOf( - EventsPropertiesKey.permissions to spaceParams.permission, - EventsPropertiesKey.spaceType to spaceParams.spaceType - ) - ) - ) - if (newType == null) { - Timber.e("Error while creating type") - return@fold - } - commands.emit( - NavigateToObjectType( - id = newType.id, - space = vmParams.spaceId.id - ) - ) - }, - onFailure = { - Timber.e(it, "Error while creating type") - } - ) + commands.emit(OpenTypeCreation(space = vmParams.spaceId.id)) } } is UiContentItem.Type -> { @@ -1086,7 +1051,7 @@ class AllContentViewModel( data class ObjectArchived(val name: String) : SendToast() } data class OpenTypeEditing(val item: UiContentItem.Type) : Command() - data object OpenTypeCreation: Command() + data class OpenTypeCreation(val space: Id): Command() data class OpenShareScreen(val space: SpaceId): Command() data class OpenRelationEditing( val typeName: String, diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/create/CreateNewTypeScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/create/CreateNewTypeScreen.kt new file mode 100644 index 0000000000..746033577f --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/create/CreateNewTypeScreen.kt @@ -0,0 +1,20 @@ +package com.anytypeio.anytype.feature_object_type.ui.create + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SetTypeTitlesAndIconScreen( + uiState: UiTypeSetupTitleAndIconState, + modifier: Modifier = Modifier, + onTitleChanged: (String) -> Unit, + onPluralChanged: (String) -> Unit, + onIconClicked: () -> Unit, + onDismiss: () -> Unit, + onButtonClicked: () -> Unit +) { + //next PR +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/create/UiState.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/create/UiState.kt new file mode 100644 index 0000000000..5415e8d3bc --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/create/UiState.kt @@ -0,0 +1,20 @@ +package com.anytypeio.anytype.feature_object_type.ui.create + +import com.anytypeio.anytype.presentation.objects.ObjectIcon + +sealed class UiTypeSetupTitleAndIconState { + + abstract val icon: ObjectIcon.TypeIcon.Default + + data class CreateNewType( + override val icon: ObjectIcon.TypeIcon.Default, + val initialTitle: String = "", + val initialPlural: String = "" + ) : UiTypeSetupTitleAndIconState() + + data class EditType( + override val icon: ObjectIcon.TypeIcon.Default, + val initialTitle: String = "", + val initialPlural: String = "" + ) : UiTypeSetupTitleAndIconState() +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/CreateObjectTypeViewModel.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/CreateObjectTypeViewModel.kt new file mode 100644 index 0000000000..ae58b73591 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/CreateObjectTypeViewModel.kt @@ -0,0 +1,190 @@ +package com.anytypeio.anytype.feature_object_type.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.anytypeio.anytype.analytics.base.Analytics +import com.anytypeio.anytype.analytics.base.EventsDictionary.libraryCreateType +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.Id +import com.anytypeio.anytype.domain.base.fold +import com.anytypeio.anytype.domain.types.CreateObjectType +import com.anytypeio.anytype.feature_object_type.ui.UiIconsPickerState +import com.anytypeio.anytype.feature_object_type.ui.create.UiTypeSetupTitleAndIconState +import com.anytypeio.anytype.feature_object_type.viewmodel.CreateTypeCommand.NavigateToObjectType +import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate +import com.anytypeio.anytype.presentation.objects.ObjectIcon +import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber + +class CreateObjectTypeViewModel( + private val vmParams: CreateTypeVmParams, + private val createObjectType: CreateObjectType, + private val analytics: Analytics, + private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate +) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate { + + private val INIT = + when (vmParams.mode) { + MODE.CREATE -> UiTypeSetupTitleAndIconState.CreateNewType( + icon = ObjectIcon.TypeIcon.Default.DEFAULT + ) + + MODE.EDIT -> UiTypeSetupTitleAndIconState.EditType( + icon = ObjectIcon.TypeIcon.Default.DEFAULT + ) + } + + private val _uiState = MutableStateFlow(INIT) + val uiState = _uiState.asStateFlow() + + //icons picker screen + val uiIconsPickerScreen = MutableStateFlow(UiIconsPickerState.Hidden) + + val commands = MutableSharedFlow(replay = 0) + + private val typeTitle = MutableStateFlow("") + private val typePlural = MutableStateFlow("") + + init { + Timber.d("CreateObjectTypeViewModel initialized") + } + + fun onTypeTitleChanged(title: String) { + typeTitle.value = title + } + + fun onTypePluralChanged(plural: String) { + typePlural.value = plural + } + + fun onDismiss() { + viewModelScope.launch { + commands.emit(CreateTypeCommand.Dismiss) + } + } + + fun onRemoveIcon() { + val defaultIcon = ObjectIcon.TypeIcon.Default.DEFAULT + _uiState.value = when (val currentState = _uiState.value) { + is UiTypeSetupTitleAndIconState.CreateNewType -> currentState.copy(icon = defaultIcon) + is UiTypeSetupTitleAndIconState.EditType -> currentState.copy(icon = defaultIcon) + } + uiIconsPickerScreen.value = UiIconsPickerState.Hidden + } + + fun onNewIconPicked(iconName: String, color: CustomIconColor?) { + val newIcon = ObjectIcon.TypeIcon.Default( + rawValue = iconName, + color = color ?: CustomIconColor.DEFAULT + ) + _uiState.value = when (val currentState = _uiState.value) { + is UiTypeSetupTitleAndIconState.CreateNewType -> currentState.copy(icon = newIcon) + is UiTypeSetupTitleAndIconState.EditType -> currentState.copy(icon = newIcon) + } + uiIconsPickerScreen.value = UiIconsPickerState.Hidden + } + + fun onIconClicked() { + uiIconsPickerScreen.value = UiIconsPickerState.Visible + } + + fun onDismissIconPicker() { + uiIconsPickerScreen.value = UiIconsPickerState.Hidden + } + + fun onButtonClicked() { + when (vmParams.mode) { + MODE.CREATE -> createNewType() + MODE.EDIT -> updateType() + } + } + + private fun createNewType() { + val icon = _uiState.value.icon + val name = typeTitle.value + val plural = typePlural.value + Timber.d("Creating new type with name: $name, plural: $plural, icon: $icon") + viewModelScope.launch { + createObjectType.execute( + CreateObjectType.Params( + space = vmParams.spaceId, + name = name, + pluralName = plural, + iconName = icon.rawValue, + iconColor = icon.color.iconOption.toDouble() + ) + ).fold( + onSuccess = { objTypeId -> + val spaceParams = provideParams(vmParams.spaceId) + viewModelScope.sendEvent( + analytics = analytics, + eventName = libraryCreateType, + props = Props( + mapOf( + EventsPropertiesKey.permissions to spaceParams.permission, + EventsPropertiesKey.spaceType to spaceParams.spaceType + ) + ) + ) + + commands.emit( + NavigateToObjectType( + id = objTypeId, + space = vmParams.spaceId + ) + ) + }, + onFailure = { + Timber.e(it, "Error while creating type") + } + ) + } + + } + + private fun updateType() { + val icon = _uiState.value.icon + val name = typeTitle.value + val plural = typePlural.value + viewModelScope.launch { + } + } +} + +data class CreateTypeVmParams( + val spaceId: Id, + val mode: MODE = MODE.CREATE +) + +enum class MODE { + CREATE, + EDIT +} + +sealed class CreateTypeCommand { + data object Dismiss : CreateTypeCommand() + data class NavigateToObjectType(val id: Id, val space: Id) : CreateTypeCommand() +} + +class CreateObjectTypeVMFactory @Inject constructor( + private val vmParams: CreateTypeVmParams, + private val createObjectType: CreateObjectType, + private val analytics: Analytics, + private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate +) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = CreateObjectTypeViewModel( + vmParams = vmParams, + createObjectType = createObjectType, + analytics = analytics, + analyticSpaceHelperDelegate = analyticSpaceHelperDelegate + ) as T +} \ No newline at end of file diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index fac0f943e3..9e9bb0ccdb 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -2008,6 +2008,11 @@ Please provide specific details of your needs here. Search..." Space invite link copied! + Create new type + Rename type + Type list name + Puzzle + Puzzles Resolve layout conflict This layout differs from the type\'s default. Reset to match? Reset to default diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt index 578f6704d5..55c6349c58 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt @@ -688,14 +688,8 @@ class BlockMiddleware( ) override suspend fun createType( - space: Id, - name: String, - emojiUnicode: String? - ): Struct? = middleware.objectCreateObjectType( - space = space, - name = name, - emojiUnicode = emojiUnicode - ) + command: Command.CreateObjectType + ): String = middleware.objectCreateObjectType(command) override suspend fun createRelationOption( space: Id, diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt index bd1957ff76..092523a4d9 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt @@ -986,23 +986,17 @@ class Middleware @Inject constructor( @Throws(Exception::class) fun objectCreateObjectType( - space: Id, - name: String, - emojiUnicode: String? - ): Struct { + command: Command.CreateObjectType + ): String { val request = Rpc.Object.CreateObjectType.Request( - details = buildMap { - put(Relations.NAME, name) - emojiUnicode?.let { - put(Relations.ICON_EMOJI, it) - } - }, - spaceId = space + details = command.details, + spaceId = command.spaceId, + internalFlags = command.internalFlags.toMiddlewareModel() ) logRequestIfDebug(request) val (response, time) = measureTimedValue { service.objectCreateObjectType(request) } - logResponseIfDebug(response) - return response.details ?: throw IllegalStateException("Null object type struct") + logResponseIfDebug(response, time) + return response.objectId } @Throws(Exception::class) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/AppNavigation.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/AppNavigation.kt index 54fe6882ff..3b6fddf1d0 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/AppNavigation.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/AppNavigation.kt @@ -72,6 +72,8 @@ interface AppNavigation { fun openAllContent(space: Id) fun openRelationCreationScreen(id: Id, name: String, space: Id) + fun openCreateObjectTypeScreen(spaceId: Id) + sealed class Command { data object Exit : Command() diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/types/CreateObjectTypeViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/types/CreateObjectTypeViewModel.kt deleted file mode 100644 index 57051edfa8..0000000000 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/types/CreateObjectTypeViewModel.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.anytypeio.anytype.presentation.types - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import com.anytypeio.anytype.analytics.base.Analytics -import com.anytypeio.anytype.analytics.base.EventsDictionary.libraryCreateType -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.Id -import com.anytypeio.anytype.core_models.ObjectType -import com.anytypeio.anytype.core_models.ObjectWrapper -import com.anytypeio.anytype.core_models.Relations -import com.anytypeio.anytype.core_utils.ext.orNull -import com.anytypeio.anytype.domain.base.fold -import com.anytypeio.anytype.domain.misc.UrlBuilder -import com.anytypeio.anytype.domain.types.CreateObjectType -import com.anytypeio.anytype.domain.workspace.SpaceManager -import com.anytypeio.anytype.emojifier.data.EmojiProvider -import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate -import com.anytypeio.anytype.presentation.mapper.objectIcon -import com.anytypeio.anytype.presentation.navigation.NavigationViewModel -import com.anytypeio.anytype.presentation.objects.ObjectIcon -import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import timber.log.Timber - -@Deprecated("Before Primitives, to delete") -class CreateObjectTypeViewModel( - private val createObjectType: CreateObjectType, - private val urlBuilder: UrlBuilder, - private val emojiProvider: EmojiProvider, - private val analytics: Analytics, - private val spaceManager: SpaceManager, - private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate -) : NavigationViewModel(), - AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate { - - private val unicodeIconFlow = MutableStateFlow("") - - val uiState: StateFlow = unicodeIconFlow.map { icon -> - val objectIcon = icon.orNull()?.let { - ObjectWrapper.Basic( - mapOf( - Relations.ICON_EMOJI to icon, - Relations.LAYOUT to ObjectType.Layout.OBJECT_TYPE.code.toDouble() - ) - ).objectIcon(urlBuilder) - } - TypeCreationIconState( - emojiUnicode = icon, - objectIcon = objectIcon ?: ObjectIcon.None - ) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(STOP_SUBSCRIPTION_TIMEOUT), - TypeCreationIconState() - ) - - fun createType(name: String) { - if (name.isEmpty()) { - sendToast("Name should not be empty") - return - } - viewModelScope.launch { - createObjectType.execute( - CreateObjectType.Params( - space = spaceManager.get(), - name = name, - emojiUnicode = uiState.value.emojiUnicode.orNull() - ) - ).fold( - onSuccess = { - val spaceParams = provideParams(spaceManager.get()) - viewModelScope.sendEvent( - analytics = analytics, - eventName = libraryCreateType, - props = Props( - mapOf( - EventsPropertiesKey.permissions to spaceParams.permission, - EventsPropertiesKey.spaceType to spaceParams.spaceType - ) - ) - ) - navigate(Navigation.BackWithCreatedType) - }, - onFailure = { - Timber.e(it, "Error while creating type $name") - sendToast("Something went wrong. Please, try again later.") - } - ) - } - } - - fun openEmojiPicker() { - navigate(Navigation.SelectEmoji(unicodeIconFlow.value.isNotEmpty())) - } - - fun setEmoji(emojiUnicode: String) { - unicodeIconFlow.value = emojiUnicode - } - - fun removeEmoji() { - unicodeIconFlow.value = "" - } - - fun onPreparedString(preparedName: Id) { - if (preparedName.isNotEmpty()) { - // todo: not random but random - unicodeIconFlow.value = emojiProvider.emojis.random().random() - } - } - - sealed class Navigation { - object BackWithCreatedType: Navigation() - class SelectEmoji(val showRemove: Boolean) : Navigation() - } - - class Factory @Inject constructor( - private val createObjectType: CreateObjectType, - private val urlBuilder: UrlBuilder, - private val emojiProvider: EmojiProvider, - private val analytics: Analytics, - private val spaceManager: SpaceManager, - private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return CreateObjectTypeViewModel( - createObjectType = createObjectType, - urlBuilder = urlBuilder, - emojiProvider = emojiProvider, - analytics = analytics, - spaceManager = spaceManager, - analyticSpaceHelperDelegate = analyticSpaceHelperDelegate - ) as T - } - } - -} - -data class TypeCreationIconState( - val emojiUnicode: String? = null, - val objectIcon: ObjectIcon = ObjectIcon.None -) - -private const val STOP_SUBSCRIPTION_TIMEOUT = 1_000L \ No newline at end of file