From 4bc1e060f3d450f1d31f8e5e184f72a3ef9d96f1 Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Fri, 28 Feb 2025 20:47:43 +0100 Subject: [PATCH] DROID-2905 Primitives | Epic | Foundation for primitives (#2098) Co-authored-by: Evgenii Kozlov --- .../analytics/base/EventsDictionary.kt | 8 + app/build.gradle | 3 +- .../anytype/features/editor/LayoutTesting.kt | 15 +- .../anytype/features/editor/ProfileTesting.kt | 5 +- .../anytype/di/common/ComponentManager.kt | 8 + .../anytype/di/feature/ObjectMenuDI.kt | 47 +- .../di/feature/ObjectRelationListDI.kt | 21 + .../di/feature/PrimitivesObjectTypeDI.kt | 189 +++ .../anytype/di/main/MainComponent.kt | 7 + .../anytypeio/anytype/navigation/Navigator.kt | 28 + .../ui/allcontent/AllContentFragment.kt | 8 +- .../anytype/ui/editor/EditorFragment.kt | 10 +- .../ui/editor/layout/ObjectLayoutFragment.kt | 1 + .../editor/sheets/ObjectMenuBaseFragment.kt | 40 +- .../ui/primitives/ObjectFieldsFragment.kt | 313 ++++ .../ui/primitives/ObjectTypeFieldsFragment.kt | 93 ++ .../ui/primitives/ObjectTypeFragment.kt | 331 ++++ .../relations/ObjectRelationListFragment.kt | 5 + .../main/res/layout/fragment_object_menu.xml | 118 +- app/src/main/res/navigation/graph.xml | 21 +- .../main/res/navigation/nav_editor_modal.xml | 2 +- .../res/navigation/nav_templates_modal.xml | 2 +- .../anytypeio/anytype/core_models/Command.kt | 14 + .../anytype/core_models/ObjectWrapper.kt | 41 +- .../anytype/core_models/Relations.kt | 123 +- .../anytypeio/anytype/core_models/Response.kt | 7 +- .../anytype/core_models/SupportedLayouts.kt | 3 +- .../permissions/ObjectPermissions.kt | 48 +- .../core_models/primitives/FieldValues.kt | 10 + core-ui/build.gradle | 2 +- .../anytype/core_ui/common/ComposePreview.kt | 14 +- .../anytype/core_ui/common/HapticExt.kt | 52 + .../core_ui/features/fields/DropDownMenu.kt | 60 + .../core_ui/features/fields/FieldEmpty.kt | 94 +- .../features/fields/FieldTypeCheckbox.kt | 36 +- .../core_ui/features/fields/FieldTypeDate.kt | 36 +- .../core_ui/features/fields/FieldTypeFile.kt | 47 +- .../features/fields/FieldTypeMultiselect.kt | 32 +- .../features/fields/FieldTypeObjects.kt | 46 +- .../features/fields/FieldTypeSelect.kt | 39 +- .../core_ui/features/fields/FieldTypeText.kt | 39 +- .../features/fields/FieldsListScreen.kt | 272 +++- .../features/fields/MultiSelectPreviews.kt | 42 +- .../relations/DocumentRelationAdapter.kt | 52 +- .../anytype/core_ui/foundation/Foundation.kt | 8 +- .../anytype/core_ui/foundation/SearchBar.kt | 162 ++ .../core_ui/lists/objects/ObjectsListItem.kt | 137 ++ .../lists/objects/ObjectsListScreen.kt | 165 ++ .../core_ui/lists/objects/ObjectsModel.kt | 27 + .../lists/objects/SwipeToDismissItem.kt | 89 ++ .../core_ui/lists/objects/menu/SortingMenu.kt | 212 +++ .../core_ui/lists/objects/stubs/Stubs.kt | 42 + .../core_ui/widgets/ObjectIconCompose.kt | 8 +- .../widgets/ObjectMenuDescriptionItem.kt | 24 + .../core_ui/widgets/ObjectMenuItemWidget.kt | 1 - .../core_ui/widgets/TemplatesPreview.kt | 72 + .../core_ui/widgets/TypeTemplatesWidget.kt | 292 ++-- .../res/drawable/ic_obj_settings_cover_24.xml | 10 + .../ic_obj_settings_description_24.xml | 17 + .../drawable/ic_obj_settings_fields_24.xml | 33 + .../drawable/ic_obj_settings_history_24.xml | 23 + .../res/drawable/ic_obj_settings_icon_24.xml | 20 + .../drawable/ic_obj_settings_layout_24.xml | 20 + .../res/drawable/ic_section_local_fields.xml | 16 + .../src/main/res/drawable/ic_settings_24.xml | 10 + .../drawable/ic_type_layout_basic_icon.xml | 20 + .../res/drawable/ic_type_layout_todo_icon.xml | 14 + .../layout/widget_object_menu_description.xml | 44 + .../res/layout/widget_object_menu_item.xml | 23 +- core-ui/src/main/res/values-night/colors.xml | 3 + core-ui/src/main/res/values/colors.xml | 3 + .../auth/repo/block/BlockDataRepository.kt | 27 +- .../data/auth/repo/block/BlockRemote.kt | 9 +- .../block/interactor/sets/CreateObjectSet.kt | 13 +- .../domain/block/repo/BlockRepository.kt | 9 +- .../anytype/domain/objects/ObjectWatcher.kt | 36 +- .../domain/objects/StoreOfObjectTypes.kt | 22 + .../domain/objects/StoreOfRelations.kt | 6 + .../anytype/domain/primitives/FieldParser.kt | 134 +- .../GetObjectTypeConflictingFields.kt | 27 + .../SetObjectTypeHeaderRecommendedFields.kt | 28 + .../SetObjectTypeRecommendedFields.kt | 29 + .../resources/StringResourceProvider.kt | 1 + .../search/ObjectTypesSubscriptionManager.kt | 10 +- .../models/AllContentModels.kt | 72 +- .../models/AllContentSearchParams.kt | 39 +- .../presentation/AllContentViewModel.kt | 50 +- .../feature_allcontent/ui/AllContentMenu.kt | 230 +-- .../feature_allcontent/ui/AllContentScreen.kt | 19 +- .../ui/AllContentTopToolbar.kt | 156 +- .../mapping/DateObjectModelsExt.kt | 40 - .../anytype/feature_date/ui/MainScreen.kt | 20 +- .../anytype/feature_date/ui/ObjectsScreen.kt | 337 ---- .../feature_date/ui/models/DateEvent.kt | 2 +- .../feature_date/ui/models/PreviewStubs.kt | 40 - .../feature_date/viewmodel/DateModels.kt | 45 - .../viewmodel/DateObjectViewModel.kt | 9 +- feature-object-type/build.gradle | 61 + .../src/main/AndroidManifest.xml | 2 + .../feature_object_type/fields/UiEvent.kt | 52 + .../feature_object_type/fields/UiState.kt | 203 +++ .../fields/ui/AddScreen.kt | 170 ++ .../fields/ui/EditScreen.kt | 386 +++++ .../fields/ui/InfoScreen.kt | 113 ++ .../fields/ui/ListScreen.kt | 775 +++++++++ .../anytype/feature_object_type/ui/Stubs.kt | 39 + .../feature_object_type/ui/TypeScreen.kt | 462 ++++++ .../anytype/feature_object_type/ui/UiEvent.kt | 62 + .../anytype/feature_object_type/ui/UiState.kt | 211 +++ .../feature_object_type/ui/UiStateExt.kt | 254 +++ .../ui/alerts/AlertsScreen.kt | 80 + .../ui/header/HeaderScreen.kt | 163 ++ .../ui/header/HorizontalButtons.kt | 117 ++ .../ui/header/TopToolbar.kt | 196 +++ .../ui/layouts/LayoutsScreen.kt | 341 ++++ .../ui/objects/ObjectsHeaderScreen.kt | 210 +++ .../ui/templates/TemplatesList.kt | 193 +++ .../ui/templates/TemplatesScreen.kt | 121 ++ .../viewmodel/ObjectTypeViewModel.kt | 1419 +++++++++++++++++ .../viewmodel/SearchParams.kt | 126 ++ .../viewmodel/VmFactory.kt | 77 + .../feature_object_type/TestFieldsMappping.kt | 241 +++ gradle/libs.versions.toml | 6 +- localization/src/main/res/values/strings.xml | 40 +- .../middleware/block/BlockMiddleware.kt | 19 +- .../middleware/interactor/Middleware.kt | 45 +- .../middleware/service/MiddlewareService.kt | 9 + .../MiddlewareServiceImplementation.kt | 39 + .../editor/render/DefaultBlockViewRenderer.kt | 36 +- .../presentation/extension/AnalyticsExt.kt | 17 + .../presentation/mapper/ObjectIconMapper.kt | 20 + .../presentation/navigation/AppNavigation.kt | 10 + .../anytype/presentation/objects/Models.kt | 190 +++ .../objects/menu/ObjectMenuOptionsProvider.kt | 12 +- .../menu/ObjectMenuOptionsProviderImpl.kt | 56 +- .../objects/menu/ObjectMenuViewModel.kt | 113 +- .../objects/menu/ObjectMenuViewModelBase.kt | 11 +- .../objects/menu/ObjectSetMenuViewModel.kt | 73 +- .../ObjectRelationListViewModelFactory.kt | 13 +- .../relations/RelationListViewModel.kt | 223 ++- .../search/ObjectSearchConstants.kt | 3 + .../util/StringResourceProviderImpl.kt | 4 + .../presentation/MockRelationFactory.kt | 3 +- .../editor/DefaultBlockViewRendererTest.kt | 67 +- .../editor/EditorFeaturedRelationsTest.kt | 10 +- .../EditorObjectTypeChangeWidgetTest.kt | 2 +- .../mapper/ObjectWrapperExtensionsKtTest.kt | 26 +- .../menu/ObjectMenuOptionsProviderImplTest.kt | 55 +- sample/build.gradle | 2 +- settings.gradle | 1 + .../anytypeio/anytype/core_models/Block.kt | 10 - .../anytypeio/anytype/core_models/Object.kt | 13 +- versioning.gradle | 2 +- 153 files changed, 10877 insertions(+), 1616 deletions(-) create mode 100644 app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectFieldsFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFieldsFragment.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFragment.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/HapticExt.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/DropDownMenu.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/SearchBar.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsListItem.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsListScreen.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsModel.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/SwipeToDismissItem.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/menu/SortingMenu.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/stubs/Stubs.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectMenuDescriptionItem.kt create mode 100644 core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/TemplatesPreview.kt create mode 100644 core-ui/src/main/res/drawable/ic_obj_settings_cover_24.xml create mode 100644 core-ui/src/main/res/drawable/ic_obj_settings_description_24.xml create mode 100644 core-ui/src/main/res/drawable/ic_obj_settings_fields_24.xml create mode 100644 core-ui/src/main/res/drawable/ic_obj_settings_history_24.xml create mode 100644 core-ui/src/main/res/drawable/ic_obj_settings_icon_24.xml create mode 100644 core-ui/src/main/res/drawable/ic_obj_settings_layout_24.xml create mode 100644 core-ui/src/main/res/drawable/ic_section_local_fields.xml create mode 100644 core-ui/src/main/res/drawable/ic_settings_24.xml create mode 100644 core-ui/src/main/res/drawable/ic_type_layout_basic_icon.xml create mode 100644 core-ui/src/main/res/drawable/ic_type_layout_todo_icon.xml create mode 100644 core-ui/src/main/res/layout/widget_object_menu_description.xml create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/primitives/GetObjectTypeConflictingFields.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/primitives/SetObjectTypeHeaderRecommendedFields.kt create mode 100644 domain/src/main/java/com/anytypeio/anytype/domain/primitives/SetObjectTypeRecommendedFields.kt delete mode 100644 feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/ObjectsScreen.kt create mode 100644 feature-object-type/build.gradle create mode 100644 feature-object-type/src/main/AndroidManifest.xml create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/UiEvent.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/UiState.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/AddScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/EditScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/InfoScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/ListScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/Stubs.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/TypeScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiEvent.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiState.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiStateExt.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/alerts/AlertsScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/HeaderScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/HorizontalButtons.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/TopToolbar.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/layouts/LayoutsScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/objects/ObjectsHeaderScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/templates/TemplatesList.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/templates/TemplatesScreen.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/SearchParams.kt create mode 100644 feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/VmFactory.kt create mode 100644 feature-object-type/src/test/java/com/anytypeio/anytype/feature_object_type/TestFieldsMappping.kt create mode 100644 presentation/src/main/java/com/anytypeio/anytype/presentation/objects/Models.kt diff --git a/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt b/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt index e32421e7f4..03e2939466 100644 --- a/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt +++ b/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt @@ -234,6 +234,14 @@ object EventsDictionary { const val clickDateCalendarView = "ClickDateCalendarView" const val objectListSort = "ObjectListSort" + //ObjectType + const val screenObjectType = "ScreenType" + const val editType = "EditType" + const val changeRecommendedLayout = "ChangeRecommendedLayout" + const val changeTypeSort = "ChangeTypeSort" + const val screenTemplate = "ScreenTemplate" + + const val searchBacklink = "SearchBacklink" object SharingSpacesTypes { diff --git a/app/build.gradle b/app/build.gradle index 8d00f0b706..ef4576f685 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -176,6 +176,7 @@ dependencies { implementation project(':gallery-experience') implementation project(':feature-all-content') implementation project(':feature-date') + implementation project(':feature-object-type') //Compile time dependencies ksp libs.daggerCompiler @@ -223,7 +224,7 @@ dependencies { implementation libs.composeAccompanistNavigation implementation libs.preference implementation libs.activityCompose - implementation libs.composeReorderable + implementation libs.composeReorderableLegacy implementation libs.room implementation libs.appUpdater diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/LayoutTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/LayoutTesting.kt index 602a433f33..5997f37aa2 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/LayoutTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/LayoutTesting.kt @@ -14,6 +14,7 @@ import com.anytypeio.anytype.presentation.MockBlockContentFactory import com.anytypeio.anytype.presentation.MockBlockFactory import com.anytypeio.anytype.presentation.editor.cover.CoverColor import com.anytypeio.anytype.core_models.ObjectViewDetails +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.test_utils.MockDataFactory import com.anytypeio.anytype.test_utils.utils.checkHasText import com.anytypeio.anytype.test_utils.utils.checkIsDisplayed @@ -85,7 +86,7 @@ class LayoutTesting : EditorTestSetup() { root to mapOf( "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), - "layout" to ObjectType.Layout.TODO.code.toDouble() + Relations.LAYOUT to ObjectType.Layout.TODO.code.toDouble() ) ) ) @@ -135,7 +136,7 @@ class LayoutTesting : EditorTestSetup() { root to mapOf( "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), - "layout" to ObjectType.Layout.TODO.code.toDouble() + Relations.LAYOUT to ObjectType.Layout.TODO.code.toDouble() ) ) ) @@ -180,7 +181,7 @@ class LayoutTesting : EditorTestSetup() { root to mapOf( "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), - "layout" to ObjectType.Layout.TODO.code.toDouble(), + Relations.LAYOUT to ObjectType.Layout.TODO.code.toDouble(), "coverType" to CoverType.COLOR.code.toDouble(), "coverId" to CoverColor.BLUE.code, ) @@ -226,7 +227,7 @@ class LayoutTesting : EditorTestSetup() { root to mapOf( "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), - "layout" to ObjectType.Layout.PROFILE.code.toDouble() + Relations.LAYOUT to ObjectType.Layout.PROFILE.code.toDouble() ) ) ) @@ -270,7 +271,7 @@ class LayoutTesting : EditorTestSetup() { root to mapOf( "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), - "layout" to ObjectType.Layout.PROFILE.code.toDouble(), + Relations.LAYOUT to ObjectType.Layout.PROFILE.code.toDouble(), "coverType" to CoverType.COLOR.code.toDouble(), "coverId" to CoverColor.BLUE.code, ) @@ -316,7 +317,7 @@ class LayoutTesting : EditorTestSetup() { root to mapOf( "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), - "layout" to ObjectType.Layout.BASIC.code.toDouble() + Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble() ) ) ) @@ -360,7 +361,7 @@ class LayoutTesting : EditorTestSetup() { root to mapOf( "iconEmoji" to DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random(), - "layout" to ObjectType.Layout.BASIC.code.toDouble(), + Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble(), "coverType" to CoverType.COLOR.code.toDouble(), "coverId" to CoverColor.BLUE.code, ) diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ProfileTesting.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ProfileTesting.kt index ce5e2e3a7d..391bf35c05 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ProfileTesting.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/ProfileTesting.kt @@ -14,6 +14,7 @@ import com.anytypeio.anytype.features.editor.base.EditorTestSetup import com.anytypeio.anytype.presentation.MockBlockContentFactory.StubTextContent import com.anytypeio.anytype.presentation.editor.cover.CoverColor import com.anytypeio.anytype.core_models.ObjectViewDetails +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.test_utils.MockDataFactory import com.anytypeio.anytype.test_utils.utils.checkHasText import com.anytypeio.anytype.test_utils.utils.checkIsDisplayed @@ -182,7 +183,7 @@ class ProfileTesting : EditorTestSetup() { root to mapOf( "iconImage" to "anyimage", - "layout" to ObjectType.Layout.PROFILE.code.toDouble(), + Relations.LAYOUT to ObjectType.Layout.PROFILE.code.toDouble(), "coverType" to CoverType.COLOR.code.toDouble(), "coverId" to CoverColor.BLUE.code, ) @@ -195,7 +196,7 @@ class ProfileTesting : EditorTestSetup() { mapOf( root to mapOf( - "layout" to ObjectType.Layout.PROFILE.code.toDouble(), + Relations.LAYOUT to ObjectType.Layout.PROFILE.code.toDouble(), "coverType" to CoverType.COLOR.code.toDouble(), "coverId" to CoverColor.BLUE.code, ) 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 2cc7528514..1f11c73d44 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 @@ -11,6 +11,7 @@ import com.anytypeio.anytype.di.feature.DaggerBacklinkOrAddToObjectComponent import com.anytypeio.anytype.di.feature.DaggerDateObjectComponent import com.anytypeio.anytype.di.feature.DaggerLinkToObjectComponent import com.anytypeio.anytype.di.feature.DaggerMoveToComponent +import com.anytypeio.anytype.di.feature.DaggerObjectTypeComponent import com.anytypeio.anytype.di.feature.DaggerSplashComponent import com.anytypeio.anytype.di.feature.DebugSettingsModule import com.anytypeio.anytype.di.feature.DefaultComponentParam @@ -105,6 +106,7 @@ import com.anytypeio.anytype.di.main.MainComponent import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel import com.anytypeio.anytype.feature_chats.presentation.ChatReactionViewModel 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.gallery_experience.viewmodel.GalleryInstallationViewModel @@ -1133,6 +1135,12 @@ class ComponentManager( .create(findComponentDependencies()) } + val objectTypeComponent = ComponentWithParams { params: ObjectTypeVmParams -> + DaggerObjectTypeComponent + .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/ObjectMenuDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt index 100a3caece..72b11cb683 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectMenuDI.kt @@ -16,6 +16,7 @@ import com.anytypeio.anytype.domain.misc.DeepLinkResolver import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.GetSpaceInviteLink import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.`object`.DuplicateObject import com.anytypeio.anytype.domain.`object`.SetObjectDetails import com.anytypeio.anytype.domain.objects.SetObjectListIsArchived @@ -23,6 +24,8 @@ import com.anytypeio.anytype.domain.page.AddBackLinkToObject import com.anytypeio.anytype.domain.page.CloseBlock import com.anytypeio.anytype.domain.page.OpenPage import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations +import com.anytypeio.anytype.domain.relations.RemoveFromFeaturedRelations import com.anytypeio.anytype.domain.templates.CreateTemplateFromObject import com.anytypeio.anytype.domain.widgets.CreateWidget import com.anytypeio.anytype.domain.workspace.SpaceManager @@ -122,7 +125,10 @@ object ObjectMenuModule { setObjectIsArchived: SetObjectListIsArchived, fieldParser: FieldParser, spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, - getSpaceInviteLink: GetSpaceInviteLink + getSpaceInviteLink: GetSpaceInviteLink, + addToFeaturedRelations: AddToFeaturedRelations, + removeFromFeaturedRelations: RemoveFromFeaturedRelations, + userPermissionProvider: UserPermissionProvider ): ObjectMenuViewModel.Factory = ObjectMenuViewModel.Factory( setObjectIsArchived = setObjectIsArchived, duplicateObject = duplicateObject, @@ -147,7 +153,10 @@ object ObjectMenuModule { setObjectListIsFavorite = setObjectListIsFavorite, fieldParser = fieldParser, getSpaceInviteLink = getSpaceInviteLink, - spaceViewSubscriptionContainer = spaceViewSubscriptionContainer + spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, + addToFeaturedRelations = addToFeaturedRelations, + removeFromFeaturedRelations = removeFromFeaturedRelations, + userPermissionProvider = userPermissionProvider ) @JvmStatic @@ -214,6 +223,18 @@ object ObjectMenuModule { repo: BlockRepository, dispatchers: AppCoroutineDispatchers ): SetObjectListIsArchived = SetObjectListIsArchived(repo = repo, dispatchers = dispatchers) + + @JvmStatic + @Provides + @PerDialog + fun addToFeaturedRelations(repo: BlockRepository): AddToFeaturedRelations = + AddToFeaturedRelations(repo) + + @JvmStatic + @Provides + @PerDialog + fun removeFromFeaturedRelations(repo: BlockRepository): RemoveFromFeaturedRelations = + RemoveFromFeaturedRelations(repo) } @Module @@ -242,7 +263,10 @@ object ObjectSetMenuModule { setObjectIsArchived: SetObjectListIsArchived, fieldParser: FieldParser, getSpaceInviteLink: GetSpaceInviteLink, - spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer + spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, + addToFeaturedRelations: AddToFeaturedRelations, + removeFromFeaturedRelations: RemoveFromFeaturedRelations, + userPermissionProvider: UserPermissionProvider ): ObjectSetMenuViewModel.Factory = ObjectSetMenuViewModel.Factory( setObjectListIsArchived = setObjectIsArchived, addBackLinkToObject = addBackLinkToObject, @@ -263,7 +287,10 @@ object ObjectSetMenuModule { setObjectListIsFavorite = setObjectListIsFavorite, fieldParser = fieldParser, getSpaceInviteLink = getSpaceInviteLink, - spaceViewSubscriptionContainer = spaceViewSubscriptionContainer + spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, + addToFeaturedRelations = addToFeaturedRelations, + removeFromFeaturedRelations = removeFromFeaturedRelations, + userPermissionProvider = userPermissionProvider ) @JvmStatic @@ -333,4 +360,16 @@ object ObjectSetMenuModule { ) } } + + @JvmStatic + @Provides + @PerDialog + fun addToFeaturedRelations(repo: BlockRepository): AddToFeaturedRelations = + AddToFeaturedRelations(repo) + + @JvmStatic + @Provides + @PerDialog + fun removeFromFeaturedRelations(repo: BlockRepository): RemoveFromFeaturedRelations = + RemoveFromFeaturedRelations(repo) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationListDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationListDI.kt index 444fc471df..83f28f6106 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationListDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/ObjectRelationListDI.kt @@ -3,11 +3,16 @@ package com.anytypeio.anytype.di.feature import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields import com.anytypeio.anytype.domain.relations.AddRelationToObject import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations import com.anytypeio.anytype.domain.relations.DeleteRelationFromObject @@ -18,6 +23,7 @@ import com.anytypeio.anytype.presentation.relations.ObjectRelationListViewModelF import com.anytypeio.anytype.presentation.relations.RelationListViewModel import com.anytypeio.anytype.presentation.relations.providers.ObjectRelationListProvider import com.anytypeio.anytype.presentation.util.Dispatcher +import com.anytypeio.anytype.ui.primitives.ObjectFieldsFragment import com.anytypeio.anytype.ui.relations.ObjectRelationListFragment import dagger.BindsInstance import dagger.Module @@ -37,6 +43,7 @@ interface ObjectRelationListComponent { } fun inject(fragment: ObjectRelationListFragment) + fun inject(fragment: ObjectFieldsFragment) } @Module @@ -56,9 +63,12 @@ object ObjectRelationListModule { deleteRelationFromObject: DeleteRelationFromObject, analytics: Analytics, storeOfRelations: StoreOfRelations, + storeOfObjectTypes: StoreOfObjectTypes, addRelationToObject: AddRelationToObject, analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, fieldParser: FieldParser, + userPermissionProvider: UserPermissionProvider, + setObjectTypeRecommendedFields: SetObjectTypeRecommendedFields ): ObjectRelationListViewModelFactory { return ObjectRelationListViewModelFactory( vmParams = vmParams, @@ -72,9 +82,12 @@ object ObjectRelationListModule { deleteRelationFromObject = deleteRelationFromObject, analytics = analytics, storeOfRelations = storeOfRelations, + storeOfObjectTypes = storeOfObjectTypes, addRelationToObject = addRelationToObject, analyticSpaceHelperDelegate = analyticSpaceHelperDelegate, fieldParser = fieldParser, + userPermissionProvider = userPermissionProvider, + setObjectTypeRecommendedFields = setObjectTypeRecommendedFields ) } @@ -95,4 +108,12 @@ object ObjectRelationListModule { @PerModal fun deleteRelationFromObject(repo: BlockRepository): DeleteRelationFromObject = DeleteRelationFromObject(repo) + + @JvmStatic + @Provides + @PerModal + fun provideTypeSetRecommendedFields( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): SetObjectTypeRecommendedFields = SetObjectTypeRecommendedFields(repo, dispatchers) } \ No newline at end of file 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 new file mode 100644 index 0000000000..7fcb7d0078 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt @@ -0,0 +1,189 @@ +package com.anytypeio.anytype.di.feature + +import androidx.lifecycle.ViewModelProvider +import com.anytypeio.anytype.analytics.base.Analytics +import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.di.common.ComponentDependencies +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.config.ConfigStorage +import com.anytypeio.anytype.domain.config.UserSettingsRepository +import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.event.interactor.EventChannel +import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider +import com.anytypeio.anytype.domain.launch.GetDefaultObjectType +import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer +import com.anytypeio.anytype.domain.misc.LocaleProvider +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.`object`.DuplicateObjects +import com.anytypeio.anytype.domain.`object`.SetObjectDetails +import com.anytypeio.anytype.domain.objects.DeleteObjects +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelations +import com.anytypeio.anytype.domain.page.CreateObject +import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields +import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields +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.feature_object_type.viewmodel.ObjectTypeVMFactory +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams +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.ObjectTypeFieldsFragment +import com.anytypeio.anytype.ui.primitives.ObjectTypeFragment +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides + +@Component( + dependencies = [ObjectTypeDependencies::class], + modules = [ + ObjectTypeModule::class, + ObjectTypeModule.Declarations::class + ] +) +@PerScreen +interface ObjectTypeComponent { + @Component.Factory + interface Factory { + fun create( + @BindsInstance vmParams: ObjectTypeVmParams, + dependencies: ObjectTypeDependencies + ): ObjectTypeComponent + } + + fun inject(fragment: ObjectTypeFragment) + fun inject(fragment: ObjectTypeFieldsFragment) +} + +@Module +object ObjectTypeModule { + + @JvmStatic + @Provides + @PerScreen + fun provideStoreLessSubscriptionContainer( + repo: BlockRepository, + channel: SubscriptionEventChannel, + dispatchers: AppCoroutineDispatchers, + logger: Logger + ): StorelessSubscriptionContainer = StorelessSubscriptionContainer.Impl( + repo = repo, + channel = channel, + dispatchers = dispatchers, + logger = logger + ) + + @JvmStatic + @Provides + @PerScreen + fun createObject( + repo: BlockRepository, + getDefaultObjectType: GetDefaultObjectType, + dispatchers: AppCoroutineDispatchers, + ): CreateObject = CreateObject( + repo = repo, + getDefaultObjectType = getDefaultObjectType, + dispatchers = dispatchers + ) + + @JvmStatic + @Provides + @PerScreen + fun provideUpdateDetailUseCase( + repository: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): SetObjectDetails = SetObjectDetails(repository, dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun coverHashProvider(): CoverImageHashProvider = DefaultCoverImageHashProvider() + + @JvmStatic + @PerScreen + @Provides + fun getDeleteObjects( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): DeleteObjects = DeleteObjects(repo, dispatchers) + + @JvmStatic + @PerScreen + @Provides + fun getObjectTypeConflictingFields( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): GetObjectTypeConflictingFields = GetObjectTypeConflictingFields(repo, dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun provideCreateObjectSetUseCase( + repo: BlockRepository + ): CreateObjectSet = CreateObjectSet(repo = repo) + + @JvmStatic + @Provides + @PerScreen + fun provideDuplicateObjectsListUseCase( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): DuplicateObjects = DuplicateObjects( + repo = repo, + dispatchers = dispatchers + ) + + @JvmStatic + @Provides + @PerScreen + fun provideTypeSetRecommendedFields( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): SetObjectTypeRecommendedFields = SetObjectTypeRecommendedFields(repo, dispatchers) + + @JvmStatic + @Provides + @PerScreen + fun provideTypeSetHeaderRecommendedFields( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): SetObjectTypeHeaderRecommendedFields = + SetObjectTypeHeaderRecommendedFields(repo, dispatchers) + + @Module + interface Declarations { + @PerScreen + @Binds + fun bindViewModelFactory( + factory: ObjectTypeVMFactory + ): ViewModelProvider.Factory + } +} + +interface ObjectTypeDependencies : ComponentDependencies { + fun blockRepository(): BlockRepository + fun analytics(): Analytics + fun urlBuilder(): UrlBuilder + fun dispatchers(): AppCoroutineDispatchers + fun storeOfObjectTypes(): StoreOfObjectTypes + fun analyticsHelper(): AnalyticSpaceHelperDelegate + fun subEventChannel(): SubscriptionEventChannel + fun logger(): Logger + fun localeProvider(): LocaleProvider + fun config(): ConfigStorage + fun userPermissionProvider(): UserPermissionProvider + fun provideStoreOfRelations(): StoreOfRelations + fun provideSpaceSyncAndP2PStatusProvider(): SpaceSyncAndP2PStatusProvider + fun provideUserSettingsRepository(): UserSettingsRepository + fun fieldParser(): FieldParser + fun provideEventChannel(): EventChannel + fun provideStringResourceProvider(): StringResourceProvider +} \ 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 8306646e00..342446f878 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 @@ -17,6 +17,7 @@ import com.anytypeio.anytype.di.feature.MainEntrySubComponent import com.anytypeio.anytype.di.feature.MoveToDependencies import com.anytypeio.anytype.di.feature.ObjectSetSubComponent import com.anytypeio.anytype.di.feature.ObjectTypeChangeSubComponent +import com.anytypeio.anytype.di.feature.ObjectTypeDependencies import com.anytypeio.anytype.di.feature.PersonalizationSettingsSubComponent import com.anytypeio.anytype.di.feature.SplashDependencies import com.anytypeio.anytype.di.feature.auth.DeletedAccountDependencies @@ -138,6 +139,7 @@ interface MainComponent : LinkToObjectDependencies, MoveToDependencies, DateObjectDependencies, + ObjectTypeDependencies, SelectChatReactionDependencies, ChatReactionDependencies, ParticipantComponentDependencies, @@ -388,6 +390,11 @@ abstract class ComponentDependenciesModule { @ComponentDependenciesKey(DateObjectDependencies::class) abstract fun provideDateObjectDependencies(component: MainComponent): ComponentDependencies + @Binds + @IntoMap + @ComponentDependenciesKey(ObjectTypeDependencies::class) + abstract fun provideObjectTypeDependencies(component: MainComponent): ComponentDependencies + @Binds @IntoMap @ComponentDependenciesKey(SelectChatReactionDependencies::class) 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 0fd5654a97..3252c2d59b 100644 --- a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt +++ b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt @@ -16,7 +16,9 @@ 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.ObjectTypeFieldsFragment import com.anytypeio.anytype.ui.profile.ParticipantFragment +import com.anytypeio.anytype.ui.primitives.ObjectTypeFragment import com.anytypeio.anytype.ui.relations.RelationCreateFromScratchForObjectFragment import com.anytypeio.anytype.ui.relations.RelationEditFragment import com.anytypeio.anytype.ui.search.GlobalSearchFragment @@ -357,4 +359,30 @@ class Navigator : AppNavigation { ) ) } + + override fun openObjectType( + objectId: Id, + space: Id + ) { + navController?.navigate( + resId = R.id.objectTypeScreen, + args = ObjectTypeFragment.args( + objectId = objectId, + space = space + ) + ) + } + + override fun openCurrentObjectTypeFields( + objectId: Id, + space: Id + ) { + navController?.navigate( + resId = R.id.objectTypeFieldsScreen, + args = ObjectTypeFieldsFragment.args( + objectId = objectId, + space = space + ) + ) + } } 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 16f1f9879b..2add470436 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 @@ -207,11 +207,9 @@ class AllContentFragment : BaseComposeFragment(), ObjectTypeSelectionListener { is AllContentViewModel.Command.OpenTypeEditing -> { runCatching { - navigation().openTypeEditingScreen( - id = command.item.id, - name = command.item.name, - icon = (command.item.icon as? ObjectIcon.Basic.Emoji)?.unicode ?: "", - readOnly = command.item.readOnly + navigation().openObjectType( + objectId = command.item.id, + space = space ) }.onFailure { toast("Failed to open type editing screen") diff --git a/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt index 7612faa832..7094206448 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt @@ -167,7 +167,7 @@ import com.anytypeio.anytype.ui.objects.creation.ObjectTypeSelectionFragment import com.anytypeio.anytype.ui.objects.creation.ObjectTypeUpdateFragment import com.anytypeio.anytype.ui.objects.types.pickers.ObjectTypeSelectionListener import com.anytypeio.anytype.ui.objects.types.pickers.ObjectTypeUpdateListener -import com.anytypeio.anytype.ui.relations.ObjectRelationListFragment +import com.anytypeio.anytype.ui.primitives.ObjectFieldsFragment import com.anytypeio.anytype.ui.relations.RelationAddToObjectBlockFragment import com.anytypeio.anytype.ui.relations.RelationDateValueFragment import com.anytypeio.anytype.ui.relations.RelationTextValueFragment @@ -1074,10 +1074,10 @@ open class EditorFragment : NavigationFragment(R.layout.f R.id.pageScreen, R.id.objectRelationListScreen, bundleOf( - ObjectRelationListFragment.ARG_CTX to command.ctx, - ObjectRelationListFragment.ARG_SPACE to space, - ObjectRelationListFragment.ARG_TARGET to command.target, - ObjectRelationListFragment.ARG_LOCKED to command.isLocked, + ObjectFieldsFragment.ARG_CTX to command.ctx, + ObjectFieldsFragment.ARG_SPACE to space, + ObjectFieldsFragment.ARG_TARGET to command.target, + ObjectFieldsFragment.ARG_LOCKED to command.isLocked, ) ) } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/editor/layout/ObjectLayoutFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/layout/ObjectLayoutFragment.kt index 64a0a81fe6..f53ac8396b 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/editor/layout/ObjectLayoutFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/layout/ObjectLayoutFragment.kt @@ -25,6 +25,7 @@ import com.anytypeio.anytype.presentation.editor.layout.ObjectLayoutViewModel import com.anytypeio.anytype.presentation.objects.ObjectLayoutView import javax.inject.Inject +@Deprecated("epic Primitives") class ObjectLayoutFragment : BaseBottomSheetFragment() { private val ctx: String get() = argString(CONTEXT_ID_KEY) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuBaseFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuBaseFragment.kt index c9a2caf324..1ec22c6450 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuBaseFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/sheets/ObjectMenuBaseFragment.kt @@ -31,13 +31,12 @@ import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuViewModelBase import com.anytypeio.anytype.ui.base.navigation import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectFragment import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectSetFragment -import com.anytypeio.anytype.ui.editor.layout.ObjectLayoutFragment import com.anytypeio.anytype.ui.editor.modals.IconPickerFragmentBase import com.anytypeio.anytype.ui.history.VersionHistoryFragment import com.anytypeio.anytype.ui.linking.BacklinkAction import com.anytypeio.anytype.ui.linking.BacklinkOrAddToObjectFragment import com.anytypeio.anytype.ui.moving.OnMoveToAction -import com.anytypeio.anytype.ui.relations.ObjectRelationListFragment +import com.anytypeio.anytype.ui.primitives.ObjectFieldsFragment import com.google.android.material.snackbar.Snackbar import timber.log.Timber @@ -84,7 +83,7 @@ abstract class ObjectMenuBaseFragment : override fun onStart() { click(binding.objectDiagnostics) { vm.onDiagnosticsClicked(ctx = ctx) } click(binding.optionHistory) { vm.onHistoryClicked(ctx = ctx, space = space) } - click(binding.optionLayout) { vm.onLayoutClicked(ctx = ctx, space = space) } + click(binding.optionDescription) { vm.onDescriptionClicked(ctx = ctx, space = space) } click(binding.optionIcon) { vm.onIconClicked(ctx = ctx, space = space) } click(binding.optionRelations) { vm.onRelationsClicked() } click(binding.optionCover) { vm.onCoverClicked(ctx = ctx, space = space) } @@ -116,19 +115,22 @@ abstract class ObjectMenuBaseFragment : private fun renderOptions(options: ObjectMenuOptionsProvider.Options) { val iconVisibility = options.hasIcon.toVisibility() val coverVisibility = options.hasCover.toVisibility() - val layoutVisibility = options.hasLayout.toVisibility() val relationsVisibility = options.hasRelations.toVisibility() val historyVisibility = options.hasHistory.toVisibility() val objectDiagnosticsVisibility = options.hasDiagnosticsVisibility.toVisibility() + if (options.hasDescriptionShow) { + binding.optionDescription.setAction(setAsHide = false) + } else { + binding.optionDescription.setAction(setAsHide = true) + } + binding.optionIcon.visibility = iconVisibility binding.optionCover.visibility = coverVisibility - binding.optionLayout.visibility = layoutVisibility binding.optionRelations.visibility = relationsVisibility binding.optionHistory.visibility = historyVisibility binding.iconDivider.visibility = iconVisibility binding.coverDivider.visibility = coverVisibility - binding.layoutDivider.visibility = layoutVisibility binding.relationsDivider.visibility = relationsVisibility binding.historyDivider.visibility = historyVisibility binding.objectDiagnostics.visibility = objectDiagnosticsVisibility @@ -139,7 +141,6 @@ abstract class ObjectMenuBaseFragment : when (command) { ObjectMenuViewModelBase.Command.OpenObjectCover -> openObjectCover() ObjectMenuViewModelBase.Command.OpenObjectIcons -> openObjectIcons() - ObjectMenuViewModelBase.Command.OpenObjectLayout -> openObjectLayout() ObjectMenuViewModelBase.Command.OpenObjectRelations -> openObjectRelations() ObjectMenuViewModelBase.Command.OpenSetCover -> openSetCover() ObjectMenuViewModelBase.Command.OpenSetIcons -> openSetIcons() @@ -218,20 +219,15 @@ abstract class ObjectMenuBaseFragment : ) } - private fun openObjectLayout() { - val fr = ObjectLayoutFragment.new(ctx = ctx, space = space) - fr.showChildFragment() - } - private fun openObjectRelations() { findNavController().navigate( R.id.objectRelationListScreen, bundleOf( - ObjectRelationListFragment.ARG_CTX to ctx, - ObjectRelationListFragment.ARG_SPACE to space, - ObjectRelationListFragment.ARG_TARGET to null, - ObjectRelationListFragment.ARG_LOCKED to isLocked, - ObjectRelationListFragment.ARG_SET_FLOW to false + ObjectFieldsFragment.ARG_CTX to ctx, + ObjectFieldsFragment.ARG_SPACE to space, + ObjectFieldsFragment.ARG_TARGET to null, + ObjectFieldsFragment.ARG_LOCKED to isLocked, + ObjectFieldsFragment.ARG_SET_FLOW to false ) ) } @@ -240,11 +236,11 @@ abstract class ObjectMenuBaseFragment : findNavController().navigate( R.id.objectRelationListScreen, bundleOf( - ObjectRelationListFragment.ARG_CTX to ctx, - ObjectRelationListFragment.ARG_SPACE to space, - ObjectRelationListFragment.ARG_TARGET to null, - ObjectRelationListFragment.ARG_LOCKED to isLocked, - ObjectRelationListFragment.ARG_SET_FLOW to true + ObjectFieldsFragment.ARG_CTX to ctx, + ObjectFieldsFragment.ARG_SPACE to space, + ObjectFieldsFragment.ARG_TARGET to null, + ObjectFieldsFragment.ARG_LOCKED to isLocked, + ObjectFieldsFragment.ARG_SET_FLOW to true ) ) } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectFieldsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectFieldsFragment.kt new file mode 100644 index 0000000000..b5857dcd1b --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectFieldsFragment.kt @@ -0,0 +1,313 @@ +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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.rememberModalBottomSheetState +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.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.TimeInMillis +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.core_ui.features.fields.FieldListScreen +import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.argStringOrNull +import com.anytypeio.anytype.core_utils.ext.safeNavigate +import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.toast +import com.anytypeio.anytype.core_utils.ext.withParent +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.di.feature.DefaultComponentParam +import com.anytypeio.anytype.feature_object_type.fields.ui.LocalInfoScreen +import com.anytypeio.anytype.presentation.relations.ObjectRelationListViewModelFactory +import com.anytypeio.anytype.presentation.relations.RelationListViewModel +import com.anytypeio.anytype.presentation.relations.RelationListViewModel.Command +import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationContext +import com.anytypeio.anytype.ui.base.navigation +import com.anytypeio.anytype.ui.editor.OnFragmentInteractionListener +import com.anytypeio.anytype.ui.relations.RelationDateValueFragment +import com.anytypeio.anytype.ui.relations.RelationTextValueFragment +import com.anytypeio.anytype.ui.relations.value.ObjectValueFragment +import com.anytypeio.anytype.ui.relations.value.TagOrStatusValueFragment +import javax.inject.Inject +import kotlin.getValue +import timber.log.Timber + +class ObjectFieldsFragment : BaseBottomSheetComposeFragment(), + RelationTextValueFragment.TextValueEditReceiver, + RelationDateValueFragment.DateValueEditReceiver { + + private val vm by viewModels { factory } + + private val ctx: String get() = argString(ARG_CTX) + private val space: String get() = argString(ARG_SPACE) + private val target: String? get() = argStringOrNull(ARG_TARGET) + private val isSetFlow: Boolean get() = arg(ARG_SET_FLOW) + + @Inject + lateinit var factory: ObjectRelationListViewModelFactory + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = + content { + MaterialTheme { + FieldListScreen( + state = vm.views.collectAsStateWithLifecycle().value, + onRelationClicked = { + vm.onRelationClicked( + ctx = ctx, + target = target, + view = it.view + ) + }, + onLocalInfoIconClicked = { + vm.onShowLocalInfo() + }, + onTypeIconClicked = { + vm.onTypeIconClicked() + }, + onRemoveFromObjectClicked = vm::onRemoveFromObjectClicked, + onAddToTypeClicked = vm::onAddToTypeClicked + ) + val showLocalFieldExplanationScreen = vm.showLocalInfo.collectAsStateWithLifecycle().value + if (showLocalFieldExplanationScreen) { + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + LocalInfoScreen( + modifier = Modifier.fillMaxWidth(), + bottomSheetState = bottomSheetState, + onDismiss = { vm.onDismissLocalInfo() } + ) + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupBottomSheetBehavior(DEFAULT_PADDING_TOP) + } + + private fun execute(command: Command) { + when (command) { + is Command.EditTextRelationValue -> { + runCatching { + val fr = RelationTextValueFragment.new( + ctx = ctx, + relationKey = command.relationKey, + objectId = command.target, + isLocked = command.isLocked, + flow = if (isSetFlow) + RelationTextValueFragment.FLOW_DATAVIEW + else + RelationTextValueFragment.FLOW_DEFAULT, + space = space + ) + fr.showChildFragment() + }.onFailure { + Timber.e(it, "Error while opening relation text value from relation list") + } + } + + is Command.EditDateRelationValue -> { + val fr = RelationDateValueFragment.new( + ctx = ctx, + space = space, + relationKey = command.relationKey, + objectId = command.target, + flow = if (isSetFlow) { + RelationDateValueFragment.FLOW_SET_OR_COLLECTION + } else { + RelationDateValueFragment.FLOW_DEFAULT + }, + isLocked = command.isLocked + ) + fr.showChildFragment() + } + + is Command.EditFileObjectRelationValue -> { + val relationContext = + if (isSetFlow) RelationContext.OBJECT_SET else RelationContext.OBJECT + findNavController().safeNavigate( + R.id.objectRelationListScreen, + R.id.objectValueScreen, + ObjectValueFragment.args( + ctx = command.ctx, + space = space, + obj = command.target, + relation = command.relationKey, + isLocked = command.isLocked, + relationContext = relationContext + ) + ) + } + + is Command.SetRelationKey -> { + withParent { + onSetRelationKeyClicked( + blockId = command.blockId, + key = command.key + ) + } + dismiss() + } + + is Command.EditTagOrStatusRelationValue -> { + val relationContext = + if (isSetFlow) RelationContext.OBJECT_SET else RelationContext.OBJECT + val bundle = TagOrStatusValueFragment.args( + ctx = command.ctx, + space = space, + obj = command.target, + relation = command.relationKey, + isLocked = command.isLocked, + context = relationContext + ) + findNavController().safeNavigate( + R.id.objectRelationListScreen, + R.id.nav_relations, + bundle + ) + } + + is Command.NavigateToDateObject -> { + runCatching { + navigation().openDateObject( + objectId = command.objectId, + space = space + ) + }.onFailure { + Timber.e(it, "Error while opening date object from relation list") + } + } + + is Command.NavigateToObjectType -> { + runCatching { + navigation().openCurrentObjectTypeFields( + objectId = command.objectTypeId, + space = space + ) + }.onFailure { + Timber.e(it, "Error while opening object type fields from object fields list") + } + } + } + } + + override fun onStart() { + jobs += lifecycleScope.subscribe(vm.commands) { command -> execute(command) } + jobs += lifecycleScope.subscribe(vm.toasts) { toast(it) } + super.onStart() + vm.onStartListMode(ctx) + } + + override fun onStop() { + super.onStop() + vm.onStop() + } + + override fun onTextValueChanged(ctx: Id, text: String, objectId: Id, relationKey: Key) { + vm.onRelationTextValueChanged( + ctx = ctx, + relationKey = relationKey, + value = text, + isValueEmpty = text.isEmpty() + ) + } + + override fun onNumberValueChanged(ctx: Id, number: Double?, objectId: Id, relationKey: Key) { + vm.onRelationTextValueChanged( + ctx = ctx, + relationKey = relationKey, + value = number, + isValueEmpty = number == null + ) + } + + override fun onDateValueChanged( + ctx: Id, + timeInSeconds: Number?, + objectId: Id, + relationKey: Key + ) { + vm.onRelationTextValueChanged( + ctx = ctx, + relationKey = relationKey, + value = timeInSeconds, + isValueEmpty = timeInSeconds == null + ) + } + + override fun onOpenDateObject(timeInMillis: TimeInMillis) { + vm.onOpenDateObjectByTimeInMillis(timeInMillis) + } + + override fun injectDependencies() { + val param = DefaultComponentParam( + ctx = ctx, + space = SpaceId(space) + ) + if (isSetFlow) { + componentManager().objectSetRelationListComponent.get(param).inject(this) + } else { + componentManager().objectRelationListComponent.get(param).inject(this) + } + } + + override fun releaseDependencies() { + if (isSetFlow) { + componentManager().objectSetRelationListComponent.release() + } else { + componentManager().objectRelationListComponent.release() + } + } + + /** + * This screen should be started from Objects with Editor Layouts + * or from objects with Set or Collection Layouts + * @param isSetFlow - true if started from Set or Collection + */ + companion object { + fun new( + ctx: Id, + space: Id, + target: String?, + locked: Boolean = false, + isSetFlow: Boolean = false, + ) = ObjectFieldsFragment().apply { + arguments = bundleOf( + ARG_CTX to ctx, + ARG_SPACE to space, + ARG_TARGET to target, + ARG_LOCKED to locked, + ARG_SET_FLOW to isSetFlow + ) + } + + const val ARG_CTX = "arg.document-relation.ctx" + const val ARG_SPACE = "arg.document-relation.space" + const val ARG_TARGET = "arg.document-relation.target" + const val ARG_LOCKED = "arg.document-relation.locked" + const val ARG_SET_FLOW = "arg.document-relation.set-flow" + + const val DEFAULT_PADDING_TOP = 10 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFieldsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFieldsFragment.kt new file mode 100644 index 0000000000..5be422fcd3 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFieldsFragment.kt @@ -0,0 +1,93 @@ +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.material.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.fragment.compose.content +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.feature_object_type.fields.ui.FieldsMainScreen +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams +import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeVMFactory +import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeViewModel +import javax.inject.Inject +import kotlin.getValue + +class ObjectTypeFieldsFragment : BaseBottomSheetComposeFragment() { + + @Inject + lateinit var factory: ObjectTypeVMFactory + + private val vm by viewModels { factory } + + private val space get() = argString(ARG_SPACE) + private val typeId get() = argString(ARG_OBJECT_ID) + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = content { + MaterialTheme { + FieldsMainScreen( + uiFieldsListState = vm.uiFieldsListState.collectAsStateWithLifecycle().value, + uiTitleState = vm.uiTitleState.collectAsStateWithLifecycle().value, + uiIconState = vm.uiIconState.collectAsStateWithLifecycle().value, + uiFieldEditOrNewState = vm.uiFieldEditOrNewState.collectAsStateWithLifecycle().value, + uiFieldLocalInfoState = vm.uiFieldLocalInfoState.collectAsStateWithLifecycle().value, + uiAddFieldsScreenState = vm.uiAddFieldsState.collectAsStateWithLifecycle().value, + fieldEvent = vm::onFieldEvent + ) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupBottomSheetBehavior(DEFAULT_PADDING_TOP) + } + + override fun onStart() { + super.onStart() + vm.onStart() + } + + override fun onStop() { + super.onStop() + vm.onStop() + } + + override fun injectDependencies() { + val params = ObjectTypeVmParams( + spaceId = SpaceId(space), + objectId = typeId, + withSubscriptions = false, + showHiddenFields = true + ) + componentManager().objectTypeComponent.get(params).inject(this) + } + + override fun releaseDependencies() { + componentManager().objectTypeComponent.release() + } + + companion object { + const val ARG_SPACE = "arg.object.type.space" + const val ARG_OBJECT_ID = "arg.object.type.object_id" + + fun args(space: Id, objectId: Id) = bundleOf( + ARG_SPACE to space, + ARG_OBJECT_ID to objectId + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFragment.kt new file mode 100644 index 0000000000..a3938fefe8 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFragment.kt @@ -0,0 +1,331 @@ +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.material.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.res.stringResource +import androidx.core.os.bundleOf +import androidx.fragment.app.setFragmentResultListener +import androidx.fragment.app.viewModels +import androidx.fragment.compose.content +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.fragment.findNavController +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.core_ui.views.BaseAlertDialog +import com.anytypeio.anytype.core_utils.ext.argString +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ext.toast +import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeMainScreen +import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeVMFactory +import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeViewModel +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams +import com.anytypeio.anytype.feature_object_type.ui.UiErrorState +import com.anytypeio.anytype.feature_object_type.fields.ui.FieldsMainScreen +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand +import com.anytypeio.anytype.presentation.home.OpenObjectNavigation +import com.anytypeio.anytype.ui.chats.ChatFragment +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.profile.ParticipantFragment +import com.anytypeio.anytype.ui.relations.RelationAddToObjectFragment +import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.TYPE_TEMPLATE_EDIT +import com.anytypeio.anytype.ui.types.picker.REQUEST_KEY_PICK_EMOJI +import com.anytypeio.anytype.ui.types.picker.REQUEST_KEY_REMOVE_EMOJI +import com.anytypeio.anytype.ui.types.picker.RESULT_EMOJI_UNICODE +import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi +import com.google.accompanist.navigation.material.rememberBottomSheetNavigator +import javax.inject.Inject +import kotlin.getValue +import timber.log.Timber + +class ObjectTypeFragment : BaseComposeFragment() { + @Inject + lateinit var factory: ObjectTypeVMFactory + + private val vm by viewModels { factory } + private lateinit var navComposeController: NavHostController + + private val space get() = argString(ARG_SPACE) + private val objectId get() = argString(ARG_OBJECT_ID) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setFragmentResultListener(REQUEST_KEY_PICK_EMOJI) { _, bundle -> + val res = requireNotNull(bundle.getString(RESULT_EMOJI_UNICODE)) + vm.updateIcon(res) + } + setFragmentResultListener(REQUEST_KEY_REMOVE_EMOJI) { _, _ -> + vm.removeIcon() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = content { + MaterialTheme { + ObjectTypeScreen() + ErrorScreen() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + subscribe(vm.commands) { command -> + Timber.d("Received command: $command") + when (command) { + ObjectTypeCommand.Back -> { + runCatching { + findNavController().popBackStack() + }.onFailure { e -> + Timber.e(e, "Error while exiting back from object type screen") + } + } + ObjectTypeCommand.OpenEmojiPicker -> { + runCatching { + findNavController().navigate(R.id.openEmojiPicker) + }.onFailure { + Timber.w("Error while opening emoji picker") + } + } + + is ObjectTypeCommand.OpenTemplate -> { + findNavController().navigate( + R.id.nav_editor_modal, + bundleOf( + EditorModalFragment.ARG_TEMPLATE_ID to command.templateId, + EditorModalFragment.ARG_TEMPLATE_TYPE_ID to command.typeId, + EditorModalFragment.ARG_TEMPLATE_TYPE_KEY to command.typeKey, + EditorModalFragment.ARG_SCREEN_TYPE to TYPE_TEMPLATE_EDIT, + EditorModalFragment.ARG_SPACE_ID to command.spaceId + ) + ) + } + + ObjectTypeCommand.OpenFieldsScreen -> { + navComposeController.navigate(OBJ_TYPE_FIELDS) + } + + is ObjectTypeCommand.OpenAddFieldScreen -> { + RelationAddToObjectFragment.new( + ctx = command.typeId, + space = command.space, + isSetOrCollection = command.isSet + ).showChildFragment() + } + } + } + } + + override fun onStart() { + super.onStart() + vm.onStart() + } + + override fun onStop() { + super.onStop() + vm.onStop() + } + + @OptIn(ExperimentalMaterialNavigationApi::class) + @Composable + fun ObjectTypeScreen() { + val bottomSheetNavigator = rememberBottomSheetNavigator() + navComposeController = rememberNavController(bottomSheetNavigator) + NavHost( + navController = navComposeController, + startDestination = OBJ_TYPE_MAIN + ) { + composable(route = OBJ_TYPE_MAIN) { + ObjectTypeMainScreen( + uiSyncStatusBadgeState = vm.uiSyncStatusBadgeState.collectAsStateWithLifecycle().value, + uiSyncStatusState = vm.uiSyncStatusWidgetState.collectAsStateWithLifecycle().value, + uiTitleState = vm.uiTitleState.collectAsStateWithLifecycle().value, + uiIconState = vm.uiIconState.collectAsStateWithLifecycle().value, + uiFieldsButtonState = vm.uiFieldsButtonState.collectAsStateWithLifecycle().value, + uiLayoutButtonState = vm.uiLayoutButtonState.collectAsStateWithLifecycle().value, + uiTemplatesHeaderState = vm.uiTemplatesHeaderState.collectAsStateWithLifecycle().value, + uiTemplatesAddIconState = vm.uiTemplatesAddIconState.collectAsStateWithLifecycle().value, + uiTemplatesListState = vm.uiTemplatesListState.collectAsStateWithLifecycle().value, + uiObjectsHeaderState = vm.uiObjectsHeaderState.collectAsStateWithLifecycle().value, + uiObjectsAddIconState = vm.uiObjectsAddIconState.collectAsStateWithLifecycle().value, + uiObjectsSettingsIconState = vm.uiObjectsSettingsIconState.collectAsStateWithLifecycle().value, + uiObjectsMenuState = vm.uiMenuState.collectAsStateWithLifecycle().value, + uiObjectsListState = vm.uiObjectsListState.collectAsStateWithLifecycle().value, + uiContentState = vm.uiContentState.collectAsStateWithLifecycle().value, + uiDeleteAlertState = vm.uiAlertState.collectAsStateWithLifecycle().value, + uiEditButtonState = vm.uiEditButtonState.collectAsStateWithLifecycle().value, + uiLayoutTypeState = vm.uiTypeLayoutsState.collectAsStateWithLifecycle().value, + onTypeEvent = vm::onTypeEvent + ) + } + composable(route = OBJ_TYPE_FIELDS) { + FieldsMainScreen( + uiFieldsListState = vm.uiFieldsListState.collectAsStateWithLifecycle().value, + uiTitleState = vm.uiTitleState.collectAsStateWithLifecycle().value, + uiIconState = vm.uiIconState.collectAsStateWithLifecycle().value, + uiFieldEditOrNewState = vm.uiFieldEditOrNewState.collectAsStateWithLifecycle().value, + uiFieldLocalInfoState = vm.uiFieldLocalInfoState.collectAsStateWithLifecycle().value, + uiAddFieldsScreenState = vm.uiAddFieldsState.collectAsStateWithLifecycle().value, + fieldEvent = vm::onFieldEvent + ) + } + } + LaunchedEffect(Unit) { + vm.navigation.collect { nav -> + when (nav) { + is OpenObjectNavigation.OpenEditor -> { + findNavController().navigate( + R.id.objectNavigation, + EditorFragment.args( + ctx = nav.target, + space = nav.space + ) + ) + } + + is OpenObjectNavigation.OpenDataView -> { + findNavController().navigate( + R.id.dataViewNavigation, + ObjectSetFragment.args( + ctx = nav.target, + space = nav.space + ) + ) + } + + is OpenObjectNavigation.OpenParticipant -> { + runCatching { + findNavController().navigate( + R.id.participantScreen, + ParticipantFragment.args( + objectId = nav.target, + space = nav.space + ) + ) + }.onFailure { + Timber.w("Error while opening participant screen") + } + } + + is OpenObjectNavigation.OpenChat -> { + findNavController().navigate( + R.id.chatScreen, + ChatFragment.args( + ctx = nav.target, + space = nav.space + ) + ) + } + + OpenObjectNavigation.NonValidObject -> { + toast(getString(R.string.error_non_valid_object)) + } + + is OpenObjectNavigation.OpenDateObject -> { + runCatching { + findNavController().navigate( + R.id.dateObjectScreen, + DateObjectFragment.args( + objectId = nav.target, + space = nav.space + ) + ) + }.onFailure { + Timber.e(it, "Failed to navigate to date object screen") + } + } + + is OpenObjectNavigation.UnexpectedLayoutError -> { + toast(getString(R.string.error_unexpected_layout)) + } + + else -> { + // Do nothing. + } + } + } + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun ErrorScreen() { + val errorStateScreen = vm.errorState.collectAsStateWithLifecycle().value + if (errorStateScreen is UiErrorState.Show) { + when (val r = errorStateScreen.reason) { + is UiErrorState.Reason.ErrorGettingObjects -> { + BaseAlertDialog( + dialogText = "${stringResource(R.string.object_type_open_type_error)}:\n${r.msg}", + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::closeObject, + onDismissRequest = vm::closeObject + ) + } + is UiErrorState.Reason.Other -> { + BaseAlertDialog( + dialogText = r.msg, + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::hideError, + onDismissRequest = vm::hideError + ) + } + } + } + when (val state = errorStateScreen) { + UiErrorState.Hidden -> { + + } + + is UiErrorState.Show -> { + + } + } + } + + override fun injectDependencies() { + val params = ObjectTypeVmParams( + spaceId = SpaceId(space), + objectId = objectId, + withSubscriptions = true, + showHiddenFields = true + ) + componentManager().objectTypeComponent.get(params).inject(this) + } + + override fun releaseDependencies() { + componentManager().objectTypeComponent.release() + } + + override fun onApplyWindowRootInsets(view: View) { + // Skipping this, since window insets will be applied by compose code. + } + + companion object { + private const val OBJ_TYPE_MAIN = "obj_type_main" + private const val OBJ_TYPE_FIELDS = "obj_fields" + const val ARG_SPACE = "arg.object.type.space" + const val ARG_OBJECT_ID = "arg.object.type.object_id" + + fun args(space: Id, objectId: Id) = bundleOf( + ARG_SPACE to space, + ARG_OBJECT_ID to objectId + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationListFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationListFragment.kt index 58e47b4c06..962320c399 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationListFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/relations/ObjectRelationListFragment.kt @@ -41,6 +41,7 @@ import com.anytypeio.anytype.ui.relations.value.TagOrStatusValueFragment import javax.inject.Inject import timber.log.Timber +@Deprecated("Legacy, epic Primitives, use ObjectFieldsFragment instead") open class ObjectRelationListFragment : BaseBottomSheetFragment(), RelationTextValueFragment.TextValueEditReceiver, RelationDateValueFragment.DateValueEditReceiver { @@ -194,6 +195,10 @@ open class ObjectRelationListFragment : BaseBottomSheetFragment { + //do nothing + } } } diff --git a/app/src/main/res/layout/fragment_object_menu.xml b/app/src/main/res/layout/fragment_object_menu.xml index 662b6eb124..7bf4498f39 100644 --- a/app/src/main/res/layout/fragment_object_menu.xml +++ b/app/src/main/res/layout/fragment_object_menu.xml @@ -28,24 +28,28 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - + @@ -61,28 +65,28 @@ android:id="@+id/coverDivider" android:layout_width="match_parent" android:layout_height="0.5dp" - android:layout_marginStart="72dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="16dp" + android:layout_marginStart="20dp" + android:layout_marginEnd="20dp" android:background="@color/shape_primary" /> - + + app:icon="@drawable/ic_obj_settings_fields_24" + app:title="@string/fields" /> - + + tools:visibility="visible" /> diff --git a/app/src/main/res/navigation/graph.xml b/app/src/main/res/navigation/graph.xml index 060abc2806..1528bb4e06 100644 --- a/app/src/main/res/navigation/graph.xml +++ b/app/src/main/res/navigation/graph.xml @@ -34,7 +34,7 @@ android:label="Object-Menu-Screen" /> + + + @@ -109,9 +114,10 @@ app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popExitAnim="@anim/nav_default_pop_exit_anim" /> + { + android:label="Date Object"> + + + + diff --git a/app/src/main/res/navigation/nav_editor_modal.xml b/app/src/main/res/navigation/nav_editor_modal.xml index 03aceae9b1..7441b2cee3 100644 --- a/app/src/main/res/navigation/nav_editor_modal.xml +++ b/app/src/main/res/navigation/nav_editor_modal.xml @@ -32,7 +32,7 @@ + ) : Command() + + data class ObjectTypeSetRecommendedFields( + val objectTypeId: String, + val fields: List + ) : Command() } \ No newline at end of file diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt index 135307c067..1946cedfdc 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt @@ -91,10 +91,10 @@ sealed class ObjectWrapper { val restrictions: List get() = when (val value = map[Relations.RESTRICTIONS]) { is Double -> buildList { - ObjectRestriction.values().firstOrNull { it.code == value.toInt() } + ObjectRestriction.entries.firstOrNull { it.code == value.toInt() } } is List<*> -> value.typeOf().mapNotNull { code -> - ObjectRestriction.values().firstOrNull { it.code == code.toInt() } + ObjectRestriction.entries.firstOrNull { it.code == code.toInt() } } else -> emptyList() } @@ -168,14 +168,37 @@ sealed class ObjectWrapper { val iconEmoji: String? by default val isDeleted: Boolean? by default val recommendedRelations: List get() = getValues(Relations.RECOMMENDED_RELATIONS) + val recommendedFeaturedRelations: List get() = getValues(Relations.RECOMMENDED_FEATURED_RELATIONS) + val recommendedHiddenRelations: List get() = getValues(Relations.RECOMMENDED_HIDDEN_RELATIONS) + val recommendedFileRelations: List get() = getValues(Relations.RECOMMENDED_FILE_RELATIONS) val recommendedLayout: ObjectType.Layout? get() = when (val value = map[Relations.RECOMMENDED_LAYOUT]) { - is Double -> ObjectType.Layout.values().singleOrNull { layout -> + is Double -> ObjectType.Layout.entries.singleOrNull { layout -> layout.code == value.toInt() } else -> ObjectType.Layout.BASIC } + val layout: ObjectType.Layout? + get() = when (val value = map[Relations.LAYOUT]) { + is Double -> ObjectType.Layout.entries.singleOrNull { layout -> + layout.code == value.toInt() + } + else -> null + } val defaultTemplateId: Id? by default + + val restrictions: List + get() = when (val value = map[Relations.RESTRICTIONS]) { + is Double -> buildList { + ObjectRestriction.entries.firstOrNull { it.code == value.toInt() } + } + + is List<*> -> value.typeOf().mapNotNull { code -> + ObjectRestriction.entries.firstOrNull { it.code == code.toInt() } + } + + else -> emptyList() + } } data class Relation(override val map: Struct) : ObjectWrapper() { @@ -188,7 +211,7 @@ sealed class ObjectWrapper { get() { val value = map[Relations.RELATION_FORMAT] return if (value is Double) { - RelationFormat.values().firstOrNull { f -> + RelationFormat.entries.firstOrNull { f -> f.code == value.toInt() } ?: RelationFormat.UNDEFINED } else { @@ -214,10 +237,10 @@ sealed class ObjectWrapper { val restrictions: List get() = when (val value = map[Relations.RESTRICTIONS]) { is Double -> buildList { - ObjectRestriction.values().firstOrNull { it.code == value.toInt() } + ObjectRestriction.entries.firstOrNull { it.code == value.toInt() } } is List<*> -> value.typeOf().mapNotNull { code -> - ObjectRestriction.values().firstOrNull { it.code == code.toInt() } + ObjectRestriction.entries.firstOrNull { it.code == code.toInt() } } else -> emptyList() } @@ -226,7 +249,11 @@ sealed class ObjectWrapper { val type: List get() = getValues(Relations.TYPE) - val isValid get() = map.containsKey(Relations.RELATION_KEY) && map.containsKey(Relations.ID) + val isValid get() = + map.containsKey(Relations.RELATION_KEY) && map.containsKey(Relations.ID) + + val isValidToUse get() = isValid && isDeleted != true && isArchived != true && isHidden != true + } data class Option(override val map: Struct) : ObjectWrapper() { diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt index fa8dbebeae..7e4e05088f 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt @@ -12,7 +12,8 @@ object Relations { const val COVER_TYPE = "coverType" const val COVER_ID = "coverId" const val DESCRIPTION = "description" - const val LAYOUT = "layout" + const val LAYOUT = "resolvedLayout" + const val LEGACY_LAYOUT = "layout" const val NAME = "name" const val ICON_EMOJI = "iconEmoji" const val ICON_OPTION = "iconOption" @@ -69,6 +70,9 @@ object Relations { const val RECOMMENDED_LAYOUT = "recommendedLayout" const val RECOMMENDED_RELATIONS = "recommendedRelations" + const val RECOMMENDED_FEATURED_RELATIONS = "recommendedFeaturedRelations" + const val RECOMMENDED_HIDDEN_RELATIONS = "recommendedHiddenRelations" + const val RECOMMENDED_FILE_RELATIONS = "recommendedFileRelations" const val DEFAULT_TEMPLATE_ID = "defaultTemplateId" const val UNIQUE_KEY = "uniqueKey" @@ -103,59 +107,98 @@ object Relations { "name", "description", "snippet", + "iconEmoji", + "iconImage", "type", + "layout", + "layoutAlign", + "coverId", + "coverScale", + "coverType", + "coverX", + "coverY", + "createdDate", + "creator", + "lastModifiedDate", + "lastModifiedBy", + "lastOpenedDate", "featuredRelations", + "isFavorite", "workspaceId", - "done", + "spaceId", "links", "internalFlags", "restrictions", - + "addedDate", "source", "sourceObject", - "setOf", + "relationFormat", + "relationKey", + "relationReadonlyValue", + "relationDefaultValue", + "relationMaxCount", + "relationOptionColor", + "relationFormatObjectTypes", + "isReadonly", + "isDeleted", + "isHidden", + "spaceShareableStatus", + "isAclShared", + "isHiddenDiscovery", + "done", + "isArchived", + "templateIsBundled", "smartblockTypes", "targetObjectType", - "recommendedRelations", "recommendedLayout", - "templateIsBundled", - - "layout", - "layoutAlign", - - "creator", - "createdDate", - "lastOpenedDate", - "lastModifiedBy", - "lastModifiedDate", - "addedDate", - - "iconEmoji", - "iconImage", - - "coverId", - "coverType", - "coverScale", - "coverX", - "coverY", - "fileExt", "fileMimeType", "sizeInBytes", - - "isHidden", - "isArchived", - "isFavorite", - "isReadonly", - - "relationKey", - "relationFormat", - "relationMaxCount", - "relationReadonlyValue", - "relationDefaultValue", - "relationFormatObjectTypes", - "relationOptionColor", - "sharedSpacesLimit" + "oldAnytypeID", + "spaceDashboardId", + "recommendedRelations", + "iconOption", + "widthInPixels", + "heightInPixels", + "sourceFilePath", + "fileSyncStatus", + "defaultTemplateId", + "uniqueKey", + "backlinks", + "profileOwnerIdentity", + "fileBackupStatus", + "fileId", + "fileIndexingStatus", + "origin", + "revision", + "imageKind", + "importType", + "spaceAccessType", + "spaceInviteFileCid", + "spaceInviteFileKey", + "readersLimit", + "writersLimit", + "sharedSpacesLimit", + "participantPermissions", + "participantStatus", + "latestAclHeadId", + "identity", + "globalName", + "syncDate", + "syncStatus", + "syncError", + "lastUsedDate", + "mentions", + "chatId", + "hasChat", + "timestamp", + "recommendedFeaturedRelations", + "recommendedHiddenRelations", + "recommendedFileRelations", + "layoutWidth", + "defaultViewType", + "defaultTypeId", + "resolvedLayout" ) } \ No newline at end of file diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Response.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Response.kt index 3a82a666a3..9fa774eeec 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Response.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Response.kt @@ -38,10 +38,9 @@ sealed class Response { sealed class Set : Response() { data class Create( - @Deprecated("legacy param") - val blockId: Id?, - val targetId: Id, - val payload: Payload + val objectId: Id, + val payload: Payload, + val details: Struct ) } diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/SupportedLayouts.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/SupportedLayouts.kt index 0fff11d579..29acacbbc9 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/SupportedLayouts.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/SupportedLayouts.kt @@ -14,7 +14,8 @@ object SupportedLayouts { ObjectType.Layout.NOTE, ObjectType.Layout.BOOKMARK, ObjectType.Layout.AUDIO, - ObjectType.Layout.PDF + ObjectType.Layout.PDF, + ObjectType.Layout.OBJECT_TYPE, ) val editorLayouts = listOf( ObjectType.Layout.BASIC, diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/permissions/ObjectPermissions.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/permissions/ObjectPermissions.kt index 90c84423c9..747b143de5 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/permissions/ObjectPermissions.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/permissions/ObjectPermissions.kt @@ -4,10 +4,13 @@ import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectTypeIds import com.anytypeio.anytype.core_models.ObjectView +import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.SupportedLayouts import com.anytypeio.anytype.core_models.getSingleValue import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction +import kotlin.collections.contains +import kotlin.collections.get /** * Represents a set of user permissions for a given object. @@ -52,8 +55,11 @@ data class ObjectPermissions( val canEditRelationsList: Boolean = false, val canEditBlocks: Boolean = false, val canEditDetails: Boolean = false, - val editBlocks: EditBlocksPermission, - val canCreateObjectThisType: Boolean = false + val editBlocks: EditBlocksPermission = EditBlocksPermission.ReadOnly, + val canCreateObjectThisType: Boolean = false, + val canChangeRecommendedLayoutForThisType: Boolean = false, + val canCreateTemplatesForThisType: Boolean = false, + val participantCanEdit: Boolean = false, ) /** @@ -104,7 +110,7 @@ fun ObjectView.toObjectPermissions( return ObjectPermissions( canArchive = participantCanEdit && !objectRestrictions.contains(ObjectRestriction.DELETE) && !isArchived, - canDelete = participantCanEdit && !objectRestrictions.contains(ObjectRestriction.DELETE), + canDelete = participantCanEdit && !objectRestrictions.contains(ObjectRestriction.DELETE), canChangeType = canEdit && !isTemplateObject && !objectRestrictions.contains(ObjectRestriction.TYPE_CHANGE), @@ -134,7 +140,34 @@ fun ObjectView.toObjectPermissions( canEditBlocks = (editBlocksPermission == EditBlocksPermission.Edit), canEditDetails = canEditDetails && canEdit, editBlocks = editBlocksPermission, - canCreateObjectThisType = !objectRestrictions.contains(ObjectRestriction.CREATE_OBJECT_OF_THIS_TYPE) && canApplyUneditableActions + canCreateObjectThisType = !objectRestrictions.contains(ObjectRestriction.CREATE_OBJECT_OF_THIS_TYPE) && canApplyUneditableActions, + participantCanEdit = participantCanEdit + ) +} + +fun ObjectWrapper.Type.toObjectPermissionsForTypes( + participantCanEdit: Boolean +): ObjectPermissions { + + val isArchived = getSingleValue(Relations.IS_ARCHIVED) == true + val canEdit = !isArchived && participantCanEdit + + val canEditDetails = !restrictions.contains(ObjectRestriction.DETAILS) + + val canCreateTemplatesForObjectsThisType = layoutsWithTemplates.contains(recommendedLayout) + && uniqueKey != ObjectTypeIds.TEMPLATE + + val canChangeRecommendedLayoutForObjectsThisType = participantCanEdit + && possibleToChangeLayoutLayouts.contains(recommendedLayout) + && uniqueKey != ObjectTypeIds.TEMPLATE + + return ObjectPermissions( + canDelete = participantCanEdit && !restrictions.contains(ObjectRestriction.DELETE), + canEditDetails = canEditDetails && canEdit, + canCreateTemplatesForThisType = canCreateTemplatesForObjectsThisType, + canCreateObjectThisType = !restrictions.contains(ObjectRestriction.CREATE_OBJECT_OF_THIS_TYPE) && participantCanEdit, + canChangeRecommendedLayoutForThisType = canChangeRecommendedLayoutForObjectsThisType, + participantCanEdit = canEdit ) } @@ -185,3 +218,10 @@ private val possibleToChangeLayoutLayouts = listOf( ObjectType.Layout.TODO, ObjectType.Layout.NOTE ) + +private val layoutsWithTemplates = listOf( + ObjectType.Layout.BASIC, + ObjectType.Layout.NOTE, + ObjectType.Layout.PROFILE, + ObjectType.Layout.TODO, +) diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/primitives/FieldValues.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/primitives/FieldValues.kt index 4219b6832e..f987ac9942 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/primitives/FieldValues.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/primitives/FieldValues.kt @@ -1,5 +1,6 @@ package com.anytypeio.anytype.core_models.primitives +import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.RelativeDate sealed class Value { @@ -20,4 +21,13 @@ sealed class Field(open val value: Value) { data class FieldDateValue( val timestamp: TimestampInSeconds, val relativeDate: RelativeDate +) + +data class ParsedFields( + val header: List = emptyList(), + val sidebar: List = emptyList(), + val hidden: List = emptyList(), + val localWithoutSystem: List = emptyList(), + val localSystem: List = emptyList(), + val file: List = emptyList(), ) \ No newline at end of file diff --git a/core-ui/build.gradle b/core-ui/build.gradle index 1cca3fdf68..cc3e30ac72 100644 --- a/core-ui/build.gradle +++ b/core-ui/build.gradle @@ -57,7 +57,7 @@ dependencies { debugImplementation libs.composeTooling implementation libs.coilCompose implementation libs.composeConstraintLayout - implementation libs.composeReorderable + implementation libs.composeReorderableLegacy testImplementation libs.fragmentTesting testImplementation project(':test:android-utils') diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/ComposePreview.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/ComposePreview.kt index 2100808988..563daab107 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/ComposePreview.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/ComposePreview.kt @@ -2,6 +2,7 @@ package com.anytypeio.anytype.core_ui.common import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @Preview( @@ -20,4 +21,15 @@ import androidx.compose.ui.tooling.preview.Preview apiLevel = 34, showSystemUi = true ) -annotation class DefaultPreviews \ No newline at end of file +annotation class DefaultPreviews + +@Preview( + backgroundColor = 0xFFFFFFFF, + showBackground = true, + uiMode = UI_MODE_NIGHT_NO, + name = "Light Mode", + apiLevel = 28, + showSystemUi = true, + device = Devices.NEXUS_5 +) +annotation class OldDevicesPreview \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/HapticExt.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/HapticExt.kt new file mode 100644 index 0000000000..71a5bf8412 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/HapticExt.kt @@ -0,0 +1,52 @@ +package com.anytypeio.anytype.core_ui.common + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalView +import androidx.core.view.HapticFeedbackConstantsCompat +import androidx.core.view.ViewCompat + +@Composable +fun rememberReorderHapticFeedback(): ReorderHapticFeedback { + val view = LocalView.current + + val reorderHapticFeedback = remember { + object : ReorderHapticFeedback() { + override fun performHapticFeedback(type: ReorderHapticFeedbackType) { + when (type) { + ReorderHapticFeedbackType.START -> + ViewCompat.performHapticFeedback( + view, + HapticFeedbackConstantsCompat.GESTURE_START + ) + + ReorderHapticFeedbackType.MOVE -> + ViewCompat.performHapticFeedback( + view, + HapticFeedbackConstantsCompat.SEGMENT_FREQUENT_TICK + ) + + ReorderHapticFeedbackType.END -> + ViewCompat.performHapticFeedback( + view, + HapticFeedbackConstantsCompat.GESTURE_END + ) + } + } + } + } + + return reorderHapticFeedback +} + +enum class ReorderHapticFeedbackType { + START, + MOVE, + END, +} + +open class ReorderHapticFeedback { + open fun performHapticFeedback(type: ReorderHapticFeedbackType) { + // no-op + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/DropDownMenu.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/DropDownMenu.kt new file mode 100644 index 0000000000..9b7a973065 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/DropDownMenu.kt @@ -0,0 +1,60 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular +import com.anytypeio.anytype.core_ui.R + +@Composable +fun FieldItemDropDownMenu( + showMenu: Boolean, + onDismissRequest: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, +) { + DropdownMenu( + modifier = Modifier + .width(244.dp), + expanded = showMenu, + offset = DpOffset(x = 0.dp, y = 0.dp), + onDismissRequest = { + onDismissRequest() + }, + shape = RoundedCornerShape(10.dp), + containerColor = colorResource(id = R.color.background_secondary), + ) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.field_menu_add_to_type), + style = BodyCalloutRegular, + color = colorResource(id = R.color.text_primary) + ) + }, + onClick = { + onAddToCurrentTypeClick() + }, + ) + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.field_menu_remove_from_object), + style = BodyCalloutRegular, + color = colorResource(id = R.color.palette_system_red) + ) + }, + onClick = { + onRemoveFromObjectClick() + }, + ) + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldEmpty.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldEmpty.kt index 8c3b58924f..ae050de3cd 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldEmpty.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldEmpty.kt @@ -1,6 +1,8 @@ package com.anytypeio.anytype.core_ui.features.fields +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -14,6 +16,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.colorResource @@ -26,8 +30,17 @@ import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.views.Relations1 +@OptIn(ExperimentalFoundationApi::class) @Composable -fun FieldEmpty(modifier: Modifier = Modifier, title: String, fieldFormat: RelationFormat) { +fun FieldEmpty( + modifier: Modifier = Modifier, + title: String, + fieldFormat: RelationFormat, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, +) { val defaultModifier = modifier .fillMaxWidth() .border( @@ -43,7 +56,11 @@ fun FieldEmpty(modifier: Modifier = Modifier, title: String, fieldFormat: Relati FieldVerticalEmpty( modifier = defaultModifier, title = title, - emptyState = emptyState + emptyState = emptyState, + isLocal = isLocal, + onFieldClick = onFieldClick, + onAddToCurrentTypeClick = onAddToCurrentTypeClick, + onRemoveFromObjectClick = onRemoveFromObjectClick ) } @@ -52,20 +69,37 @@ fun FieldEmpty(modifier: Modifier = Modifier, title: String, fieldFormat: Relati FieldHorizontalEmpty( modifier = defaultModifier, title = title, - emptyState = emptyState + emptyState = emptyState, + isLocal = isLocal, + onFieldClick = onFieldClick, + onAddToCurrentTypeClick = onAddToCurrentTypeClick, + onRemoveFromObjectClick = onRemoveFromObjectClick ) } } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun FieldVerticalEmpty( modifier: Modifier = Modifier, title: String, emptyState: String, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, ) { + val isMenuExpanded = remember { mutableStateOf(false) } Column( - modifier = modifier.padding(horizontal = 16.dp, vertical = 16.dp) + modifier = modifier + .combinedClickable( + onClick = { onFieldClick()}, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) + .padding(horizontal = 16.dp, vertical = 16.dp) ) { Text( modifier = Modifier.fillMaxWidth(), @@ -84,21 +118,47 @@ private fun FieldVerticalEmpty( maxLines = 1, overflow = TextOverflow.Ellipsis ) + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun FieldHorizontalEmpty( modifier: Modifier = Modifier, title: String, emptyState: String, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, ) { + val isMenuExpanded = remember { mutableStateOf(false) } val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp val halfScreenWidth = screenWidth / 2 - 32.dp Row( modifier = modifier + .combinedClickable( + onClick = onFieldClick, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) .padding(horizontal = 16.dp, vertical = 16.dp) ) { Text( @@ -116,6 +176,20 @@ private fun FieldHorizontalEmpty( style = Relations1, color = colorResource(id = R.color.text_tertiary) ) + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } @@ -149,13 +223,21 @@ fun PreviewField() { item { FieldEmpty( title = "Description", - fieldFormat = Relation.Format.LONG_TEXT + fieldFormat = Relation.Format.LONG_TEXT, + isLocal = true, + onFieldClick = {}, + onAddToCurrentTypeClick = {}, + onRemoveFromObjectClick = {} ) } item { FieldEmpty( title = "Some Number, very long long long long long fields name", - fieldFormat = Relation.Format.NUMBER + fieldFormat = Relation.Format.NUMBER, + isLocal = true, + onFieldClick = {}, + onAddToCurrentTypeClick = {}, + onRemoveFromObjectClick = {} ) } item { diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeCheckbox.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeCheckbox.kt index effcb16890..3b8272e5b1 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeCheckbox.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeCheckbox.kt @@ -1,7 +1,9 @@ package com.anytypeio.anytype.core_ui.features.fields +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -14,6 +16,8 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration @@ -25,13 +29,25 @@ import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.views.Relations1 +@OptIn(ExperimentalFoundationApi::class) @Composable fun FieldTypeCheckbox( modifier: Modifier = Modifier, title: String, isCheck: Boolean, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, ) { + val isMenuExpanded = remember { mutableStateOf(false) } val defaultModifier = modifier + .combinedClickable( + onClick = { onFieldClick()}, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) .fillMaxWidth() .border( width = 1.dp, @@ -81,6 +97,20 @@ fun FieldTypeCheckbox( ) } } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } @@ -90,6 +120,10 @@ fun FieldTypeCheckbox( fun FieldTypeCheckboxPreview() { FieldTypeCheckbox( title = "Creation date", - isCheck = false + isCheck = false, + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeDate.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeDate.kt index cf76d59870..ba8fcdb7cf 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeDate.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeDate.kt @@ -1,6 +1,8 @@ package com.anytypeio.anytype.core_ui.features.fields +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -12,6 +14,8 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration @@ -26,13 +30,25 @@ import com.anytypeio.anytype.core_ui.extensions.getPrettyName import com.anytypeio.anytype.core_ui.views.BodyCallout import com.anytypeio.anytype.core_ui.views.Relations1 +@OptIn(ExperimentalFoundationApi::class) @Composable fun FieldTypeDate( modifier: Modifier = Modifier, title: String, - relativeDate: RelativeDate + relativeDate: RelativeDate, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, ) { + val isMenuExpanded = remember { mutableStateOf(false) } val defaultModifier = modifier + .combinedClickable( + onClick = onFieldClick, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) .fillMaxWidth() .border( width = 1.dp, @@ -77,6 +93,20 @@ fun FieldTypeDate( overflow = TextOverflow.Ellipsis ) } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } @@ -90,5 +120,9 @@ fun FieldTypeDatePreview() { initialTimeInMillis = System.currentTimeMillis(), dayOfWeek = DayOfWeekCustom.THURSDAY ), + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeFile.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeFile.kt index 437589b953..ed4cc5688c 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeFile.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeFile.kt @@ -1,7 +1,9 @@ package com.anytypeio.anytype.core_ui.features.fields +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -16,6 +18,8 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.SubcomposeLayout @@ -34,16 +38,28 @@ import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon import com.anytypeio.anytype.presentation.relations.ObjectRelationView import com.anytypeio.anytype.presentation.sets.model.FileView +@OptIn(ExperimentalFoundationApi::class) @Composable fun FieldTypeFile( modifier: Modifier = Modifier, - fieldObject: ObjectRelationView.File + fieldObject: ObjectRelationView.File, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, ) { + val isMenuExpanded = remember { mutableStateOf(false) } val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp val halfScreenWidth = screenWidth / 2 - 32.dp val defaultModifier = modifier + .combinedClickable( + onClick = onFieldClick, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) .fillMaxWidth() .border( width = 1.dp, @@ -52,6 +68,7 @@ fun FieldTypeFile( ) .padding(vertical = 16.dp) .padding(horizontal = 16.dp) + if (fieldObject.files.size == 1) { // If there is only one item, display the title and the item in one row. val singleItem = fieldObject.files.first() @@ -82,6 +99,20 @@ fun FieldTypeFile( objView = singleItem ) } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } else { Column( @@ -156,6 +187,20 @@ fun FieldTypeFile( } } } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeMultiselect.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeMultiselect.kt index de33ff9c6d..19ac70f4f7 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeMultiselect.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeMultiselect.kt @@ -1,7 +1,9 @@ package com.anytypeio.anytype.core_ui.features.fields +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -15,6 +17,8 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -35,13 +39,25 @@ import com.anytypeio.anytype.core_ui.views.Relations1 import com.anytypeio.anytype.core_ui.views.Relations2 import com.anytypeio.anytype.presentation.sets.model.TagView +@OptIn(ExperimentalFoundationApi::class) @Composable fun FieldTypeMultiSelect( modifier: Modifier = Modifier, title: String, - tags: List + tags: List, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, ) { + val isMenuExpanded = remember { mutableStateOf(false) } val defaultModifier = modifier + .combinedClickable( + onClick = onFieldClick, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) .fillMaxWidth() .border( width = 1.dp, @@ -79,6 +95,20 @@ fun FieldTypeMultiSelect( modifier = Modifier.fillMaxWidth(), textStyle = Relations1 ) + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeObjects.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeObjects.kt index 4bbb96c351..b829ea54c8 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeObjects.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeObjects.kt @@ -1,11 +1,15 @@ package com.anytypeio.anytype.core_ui.features.fields +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.SubcomposeLayout @@ -35,16 +39,28 @@ import com.anytypeio.anytype.presentation.sets.model.ObjectView * (where n = total number of items minus two) immediately following its text. * If the text of the second item is long, it is truncated so that the suffix is always visible. */ +@OptIn(ExperimentalFoundationApi::class) @Composable fun FieldTypeObject( modifier: Modifier = Modifier, - fieldObject: ObjectRelationView.Object + fieldObject: ObjectRelationView.Object, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, ) { + val isMenuExpanded = remember { mutableStateOf(false) } val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp val halfScreenWidth = screenWidth / 2 - 32.dp val defaultModifier = modifier + .combinedClickable( + onClick = onFieldClick, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) .fillMaxWidth() .border( width = 1.dp, @@ -83,6 +99,20 @@ fun FieldTypeObject( objView = singleItem ) } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } else { Column( @@ -156,6 +186,20 @@ fun FieldTypeObject( } } } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeSelect.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeSelect.kt index e3cb4f385b..6ac0f9d94f 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeSelect.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeSelect.kt @@ -1,6 +1,8 @@ package com.anytypeio.anytype.core_ui.features.fields +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -12,6 +14,8 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration @@ -25,13 +29,26 @@ import com.anytypeio.anytype.core_ui.extensions.dark import com.anytypeio.anytype.core_ui.views.Relations1 import com.anytypeio.anytype.presentation.sets.model.StatusView +@OptIn(ExperimentalFoundationApi::class) @Composable fun FieldTypeSelect( modifier: Modifier = Modifier, title: String, - status: StatusView + status: StatusView, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, ) { + val isMenuExpanded = remember { mutableStateOf(false) } + val defaultModifier = modifier + .combinedClickable( + onClick = onFieldClick, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) .fillMaxWidth() .border( width = 1.dp, @@ -71,6 +88,20 @@ fun FieldTypeSelect( maxLines = 1, overflow = TextOverflow.Ellipsis ) + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } @@ -83,6 +114,10 @@ fun FieldTypeSelectPreview() { id = "1", status = "In Progress", color = ThemeColor.TEAL.code - ) + ), + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeText.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeText.kt index c6bfab077c..50bc4fce2d 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeText.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeText.kt @@ -1,6 +1,8 @@ package com.anytypeio.anytype.core_ui.features.fields +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -9,6 +11,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.style.TextOverflow @@ -17,13 +21,26 @@ import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.views.Relations1 +@OptIn(ExperimentalFoundationApi::class) @Composable fun FieldTypeText( modifier: Modifier = Modifier, title: String, - text: String + text: String, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, ) { + val isMenuExpanded = remember { mutableStateOf(false) } + val defaultModifier = modifier + .combinedClickable( + onClick = onFieldClick, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) .fillMaxWidth() .border( width = 1.dp, @@ -55,6 +72,20 @@ fun FieldTypeText( maxLines = 3, overflow = TextOverflow.Ellipsis ) + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) } } @@ -65,6 +96,10 @@ fun FieldTypeTextPreview() { title = "Description", text = "Upon creating your profile, you’ll receive your very own 12 word mnemonic ‘Recovery’ phrase to protect your account. This phrase is generated on-device and represents your master key generated upon signup, similar to a Bitcoin wallet. It also prevents anyone - including Anytype - from accessing your account and decrypting your data.\n" + "\n" + - "All data you create will be stored locally (on-device) first. We use zero-knowledge encryption, meaning that your data is encrypted before it leaves your device to sync with other devices or backup nodes." + "All data you create will be stored locally (on-device) first. We use zero-knowledge encryption, meaning that your data is encrypted before it leaves your device to sync with other devices or backup nodes.", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldsListScreen.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldsListScreen.kt index 9eaedc63f8..6d7d305d5c 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldsListScreen.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldsListScreen.kt @@ -1,12 +1,19 @@ package com.anytypeio.anytype.core_ui.features.fields +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -14,27 +21,39 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.RelationFormat import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.features.editor.holders.relations.resRelationOrigin import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium import com.anytypeio.anytype.core_ui.views.Title1 import com.anytypeio.anytype.presentation.relations.ObjectRelationView import com.anytypeio.anytype.presentation.relations.RelationListViewModel.Model import timber.log.Timber +@OptIn(ExperimentalFoundationApi::class) @Composable fun FieldListScreen( state: List, - onRelationClicked: (Model.Item) -> Unit + onRelationClicked: (Model.Item) -> Unit, + onTypeIconClicked: () -> Unit, + onLocalInfoIconClicked: () -> Unit, + onAddToTypeClicked: (Model.Item) -> Unit, + onRemoveFromObjectClicked: (Model.Item) -> Unit ) { LazyColumn( modifier = Modifier .fillMaxSize() + .background( + color = colorResource(id = R.color.widget_background), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + ) .nestedScroll(rememberNestedScrollInteropConnection()) .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -47,7 +66,9 @@ fun FieldListScreen( } item { Box( - modifier = Modifier.height(48.dp) + modifier = Modifier + .fillMaxWidth() + .height(48.dp) ) { Text( modifier = Modifier.align(Alignment.Center), @@ -55,6 +76,23 @@ fun FieldListScreen( style = Title1, color = colorResource(id = R.color.text_primary), ) + + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .width(56.dp) + .height(48.dp) + .noRippleThrottledClickable { + onTypeIconClicked() + }, + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.wrapContentSize(), + painter = painterResource(R.drawable.ic_settings_24), + contentDescription = "Open object's type" + ) + } } } items( @@ -68,11 +106,13 @@ fun FieldListScreen( when (field) { is ObjectRelationView.Checkbox -> { FieldTypeCheckbox( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - isCheck = field.isChecked + isCheck = field.isChecked, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } @@ -80,19 +120,23 @@ fun FieldListScreen( val relativeDate = field.relativeDate if (relativeDate != null) { FieldTypeDate( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - relativeDate = relativeDate + relativeDate = relativeDate, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } else { FieldEmpty( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - fieldFormat = RelationFormat.DATE + fieldFormat = RelationFormat.DATE, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } } @@ -102,28 +146,34 @@ fun FieldListScreen( if (field.key == Relations.ORIGIN) { val code = textValue?.toInt() ?: -1 FieldTypeText( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - text = stringResource(code.resRelationOrigin()) + text = stringResource(code.resRelationOrigin()), + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } else { if (textValue.isNullOrEmpty() == true) { FieldEmpty( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - fieldFormat = RelationFormat.LONG_TEXT + fieldFormat = RelationFormat.LONG_TEXT, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } else { FieldTypeText( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - text = textValue + text = textValue, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } } @@ -132,18 +182,22 @@ fun FieldListScreen( is ObjectRelationView.File -> { if (field.files.isEmpty()) { FieldEmpty( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - fieldFormat = RelationFormat.FILE + fieldFormat = RelationFormat.FILE, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } else { FieldTypeFile( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, - fieldObject = field + modifier = Modifier, + fieldObject = field, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } } @@ -151,18 +205,22 @@ fun FieldListScreen( is ObjectRelationView.Object -> { if (field.objects.isEmpty()) { FieldEmpty( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - fieldFormat = RelationFormat.OBJECT + fieldFormat = RelationFormat.OBJECT, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } else { FieldTypeObject( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, - fieldObject = field + modifier = Modifier, + fieldObject = field, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } } @@ -170,19 +228,23 @@ fun FieldListScreen( is ObjectRelationView.Status -> { if (field.status.isEmpty()) { FieldEmpty( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - fieldFormat = RelationFormat.STATUS + fieldFormat = RelationFormat.STATUS, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } else { FieldTypeSelect( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - status = field.status.first() + status = field.status.first(), + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } } @@ -190,22 +252,27 @@ fun FieldListScreen( is ObjectRelationView.Tags -> { if (field.tags.isEmpty()) { FieldEmpty( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - fieldFormat = RelationFormat.TAG + fieldFormat = RelationFormat.TAG, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } else { FieldTypeMultiSelect( - modifier = Modifier.noRippleThrottledClickable { - onRelationClicked(item) - }, + modifier = Modifier, title = field.name, - tags = field.tags + tags = field.tags, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } ) } } + is ObjectRelationView.Links.Backlinks, is ObjectRelationView.Links.From, is ObjectRelationView.ObjectType.Base, @@ -216,16 +283,14 @@ fun FieldListScreen( } } - Model.Section.Featured -> { - //TODO: Implement + is Model.Section.Header -> { + Section(item) } - - Model.Section.Other -> { - //TODO: Implement + is Model.Section.SideBar -> { + Section(item) } - - is Model.Section.TypeFrom -> { - //TODO: Implement + Model.Section.Local -> { + SectionLocal(onLocalInfoIconClicked) } } } @@ -234,4 +299,77 @@ fun FieldListScreen( Spacer(modifier = Modifier.height(64.dp)) } } -} \ No newline at end of file +} + +@Composable +private fun SectionLocal( + onLocalInfoIconClicked: () -> Unit = {} +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + ) { + Text( + modifier = Modifier + .padding(bottom = 7.dp, start = 20.dp) + .align(Alignment.BottomStart), + text = stringResource(id = R.string.object_type_fields_section_local_fields), + style = BodyCalloutMedium, + color = colorResource(R.color.text_primary), + ) + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .height(37.dp) + .width(44.dp) + .noRippleThrottledClickable { + onLocalInfoIconClicked() + } + ) { + Image( + modifier = Modifier + .padding(bottom = 9.dp, end = 20.dp) + .wrapContentSize() + .align(Alignment.BottomEnd), + painter = painterResource(R.drawable.ic_section_local_fields), + contentDescription = "Section local fields info" + ) + } + } +} + +@Composable +private fun Section(item: Model.Section) { + val text = when (item) { + Model.Section.Header -> stringResource(id = R.string.object_type_fields_section_header) + Model.Section.SideBar -> stringResource(id = R.string.object_type_fields_section_fields_menu) + else -> "" + } + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = text, + style = BodyCalloutMedium, + color = colorResource(id = R.color.text_secondary), + modifier = Modifier + .padding(vertical = 17.dp) + .padding(start = 16.dp) + ) + } +} + +@DefaultPreviews +@Composable +fun FieldListScreenPreview() { + FieldListScreen( + state = listOf(Model.Section.Local), + onRelationClicked = {}, + onLocalInfoIconClicked = {}, + onTypeIconClicked = {}, + onAddToTypeClicked = {}, + onRemoveFromObjectClicked = {} + ) +} diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/MultiSelectPreviews.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/MultiSelectPreviews.kt index 1d42dc9bb1..1b9e79a6e6 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/MultiSelectPreviews.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/MultiSelectPreviews.kt @@ -54,7 +54,11 @@ fun TagsPreview() { color = ThemeColor.PINK.code, ) ), - title = "Tag" + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } } @@ -76,7 +80,11 @@ fun SingleLongTagPreview() { color = ThemeColor.RED.code, ), ), - title = "Tag" + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } } @@ -98,7 +106,11 @@ fun SingleShortTagPreview() { color = ThemeColor.RED.code, ), ), - title = "Tag" + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } } @@ -125,7 +137,11 @@ fun TwoTagsFirstShortSecondLongPreview() { color = ThemeColor.ORANGE.code, ) ), - title = "Tag" + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } } @@ -152,7 +168,11 @@ fun TwoShortTagsPreview() { color = ThemeColor.ORANGE.code, ) ), - title = "Tag" + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } } @@ -184,7 +204,11 @@ fun ThreeShortTagsPreview() { color = ThemeColor.LIME.code, ), ), - title = "Tag" + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } } @@ -221,7 +245,11 @@ fun FourTagsWithOverflowPreview() { color = ThemeColor.BLUE.code, ) ), - title = "Tag" + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} ) } } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/DocumentRelationAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/DocumentRelationAdapter.kt index a5f60473d5..7703d633e2 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/DocumentRelationAdapter.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/relations/DocumentRelationAdapter.kt @@ -21,6 +21,7 @@ import com.anytypeio.anytype.presentation.relations.ObjectRelationView import com.anytypeio.anytype.presentation.relations.RelationListViewModel import timber.log.Timber +@Deprecated("Use ListRelationViewHolder instead") class DocumentRelationAdapter( private var items: List, private val onRelationClicked: (RelationListViewModel.Model.Item) -> Unit, @@ -189,8 +190,8 @@ class DocumentRelationAdapter( if (payload is GranularChange) { if (payload.isModeChanged) { val item = items[position] - check(item is RelationListViewModel.Model.Item) - holder.setIsRemovable(item.isRemovable) +// check(item is RelationListViewModel.Model.Item) +// holder.setIsRemovable(item.isRemovable) } else { super.onBindViewHolder(holder, position, payloads) } @@ -258,7 +259,7 @@ class DocumentRelationAdapter( if (holder is ListRelationViewHolder) { check(item is RelationListViewModel.Model.Item) holder.setIsFeatured(item.view.featured) - holder.setIsRemovable(item.isRemovable) + //holder.setIsRemovable(item.isRemovable) } } @@ -276,9 +277,9 @@ class DocumentRelationAdapter( else -> R.layout.item_relation_list_relation_default } } - RelationListViewModel.Model.Section.Featured -> R.layout.item_relation_list_section - RelationListViewModel.Model.Section.Other -> R.layout.item_relation_list_section - is RelationListViewModel.Model.Section.TypeFrom -> R.layout.item_relation_list_section +// RelationListViewModel.Model.Section.Featured -> R.layout.item_relation_list_section +// RelationListViewModel.Model.Section.Other -> R.layout.item_relation_list_section +// is RelationListViewModel.Model.Section.TypeFrom -> R.layout.item_relation_list_section else -> throw IllegalStateException("Unexpected item type: $item") } @@ -292,18 +293,18 @@ class DocumentRelationAdapter( class SectionViewHolder(view: View) : RecyclerView.ViewHolder(view) { fun bind(section: RelationListViewModel.Model.Section) { when (section) { - RelationListViewModel.Model.Section.Featured -> { - itemView.findViewById(R.id.tvSectionName) - .setText(R.string.featured_relations) - } - RelationListViewModel.Model.Section.Other -> { - itemView.findViewById(R.id.tvSectionName) - .setText(R.string.other_relations) - } - is RelationListViewModel.Model.Section.TypeFrom -> { - val text = itemView.resources.getString(R.string.from_type, section.typeName) - itemView.findViewById(R.id.tvSectionName).text = text - } +// RelationListViewModel.Model.Section.Featured -> { +// itemView.findViewById(R.id.tvSectionName) +// .setText(R.string.featured_relations) +// } +// RelationListViewModel.Model.Section.Other -> { +// itemView.findViewById(R.id.tvSectionName) +// .setText(R.string.other_relations) +// } +// is RelationListViewModel.Model.Section.TypeFrom -> { +// val text = itemView.resources.getString(R.string.from_type, section.typeName) +// itemView.findViewById(R.id.tvSectionName).text = text +// } else -> throw IllegalStateException("Unexpected item type: $section") } } @@ -316,13 +317,14 @@ class DocumentRelationAdapter( override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { val oldItem = old[oldItemPosition] val newItem = new[newItemPosition] - return if (oldItem is RelationListViewModel.Model.Item && newItem is RelationListViewModel.Model.Item) { - if (newItem.isRemovable != oldItem.isRemovable) - GranularChange(isModeChanged = true) - else - null - } else - null + return null +// return if (oldItem is RelationListViewModel.Model.Item && newItem is RelationListViewModel.Model.Item) { +// if (newItem.isRemovable != oldItem.isRemovable) +// GranularChange(isModeChanged = true) +// else +// null +// } else +// null } } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt index 1911d0f710..513ac73be6 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt @@ -78,16 +78,18 @@ fun Dragger( @Composable fun Divider( modifier: Modifier = Modifier, + height: Dp = 0.5.dp, paddingStart: Dp = 20.dp, paddingEnd: Dp = 20.dp, - visible: Boolean = true + visible: Boolean = true, + color: Color = colorResource(R.color.shape_primary) ) { Box( modifier = modifier .alpha(if (visible) 1f else 0f) .padding(start = paddingStart, end = paddingEnd) - .background(color = colorResource(R.color.shape_primary)) - .height(0.5.dp) + .background(color = color) + .height(height) .fillMaxWidth() ) } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/SearchBar.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/SearchBar.kt new file mode 100644 index 0000000000..1dd568cb69 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/SearchBar.kt @@ -0,0 +1,162 @@ +package com.anytypeio.anytype.core_ui.foundation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.R + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun DefaultSearchBar( + modifier: Modifier = Modifier, + onQueryChanged: (String) -> Unit +) { + + val interactionSource = remember { MutableInteractionSource() } + val focus = LocalFocusManager.current + val focusRequester = FocusRequester() + + val selectionColors = TextSelectionColors( + backgroundColor = colorResource(id = R.color.cursor_color).copy( + alpha = 0.2f + ), + handleColor = colorResource(id = R.color.cursor_color), + ) + + var query by remember { mutableStateOf(TextFieldValue()) } + + Row( + modifier = modifier + .background( + color = colorResource(id = R.color.shape_transparent), + shape = RoundedCornerShape(10.dp) + ) + .height(40.dp), + verticalAlignment = Alignment.CenterVertically + + ) { + Image( + painter = painterResource(id = R.drawable.ic_search_18), + contentDescription = "Search icon", + modifier = Modifier + .align(Alignment.CenterVertically) + .padding( + start = 11.dp + ) + ) + CompositionLocalProvider(value = LocalTextSelectionColors provides selectionColors) { + + BasicTextField( + value = query, + modifier = Modifier + .weight(1.0f) + .padding(start = 6.dp) + .align(Alignment.CenterVertically) + .focusRequester(focusRequester), + textStyle = BodyRegular.copy( + color = colorResource(id = R.color.text_primary) + ), + onValueChange = { input -> + query = input.also { + onQueryChanged(input.text) + } + }, + singleLine = true, + maxLines = 1, + keyboardActions = KeyboardActions( + onDone = { + focus.clearFocus(true) + } + ), + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.OutlinedTextFieldDecorationBox( + value = query.text, + innerTextField = innerTextField, + enabled = true, + singleLine = true, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + placeholder = { + Text( + text = stringResource(id = R.string.search), + style = BodyRegular.copy( + color = colorResource(id = R.color.text_tertiary) + ) + ) + }, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Transparent, + cursorColor = colorResource(id = R.color.cursor_color), + ), + border = {}, + contentPadding = PaddingValues() + ) + }, + cursorBrush = SolidColor(colorResource(id = R.color.palette_system_blue)), + ) + } + Spacer(Modifier.width(9.dp)) + AnimatedVisibility( + visible = query.text.isNotEmpty(), + enter = fadeIn(tween(100)), + exit = fadeOut(tween(100)) + ) { + Image( + painter = painterResource(id = R.drawable.ic_clear_18), + contentDescription = "Clear icon", + modifier = Modifier + .padding(end = 9.dp) + .noRippleClickable { + query = TextFieldValue().also { + onQueryChanged("") + } + } + ) + } + } +} + +@DefaultPreviews +@Composable +private fun AllContentSearchBarPreview() { + DefaultSearchBar {} +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsListItem.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsListItem.kt new file mode 100644 index 0000000000..158bb3c2f9 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsListItem.kt @@ -0,0 +1,137 @@ +package com.anytypeio.anytype.core_ui.lists.objects + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.common.ShimmerEffect +import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular +import com.anytypeio.anytype.core_ui.views.Relations3 +import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon +import com.anytypeio.anytype.presentation.objects.ObjectIcon +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem + +/** + * A reusable composable for displaying a single UiObjectsListItem.Item + */ +@Composable +fun ObjectsListItem( + item: UiObjectsListItem.Item, + modifier: Modifier = Modifier, +) { + val createdBy = item.createdBy + val typeName = item.typeName + + ListItem( + colors = ListItemDefaults.colors( + containerColor = colorResource(id = R.color.background_primary), + ), + modifier = modifier + .height(72.dp) + .fillMaxWidth(), + headlineContent = { + Text( + text = item.name, + style = PreviewTitle2Regular, + color = colorResource(id = R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + supportingContent = { + Row { + if (!typeName.isNullOrBlank()) { + Text( + text = typeName, + style = Relations3, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (!createdBy.isNullOrBlank()) { + Text( + text = "${stringResource(R.string.date_layout_item_created_by)} • $createdBy", + style = Relations3, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + }, + leadingContent = { + ListWidgetObjectIcon( + icon = item.icon, + modifier = Modifier, + iconSize = 48.dp + ) + } + ) +} + +@Composable +fun ListItemLoading( + modifier: Modifier +) { + ListItem( + colors = ListItemDefaults.colors( + containerColor = colorResource(id = R.color.background_primary), + ), + modifier = modifier + .height(72.dp) + .fillMaxWidth(), + headlineContent = { + ShimmerEffect( + modifier = Modifier + .width(164.dp) + .height(18.dp) + ) + }, + supportingContent = { + ShimmerEffect( + modifier = Modifier + .width(64.dp) + .height(13.dp) + ) + }, + leadingContent = { + ShimmerEffect( + modifier = Modifier + .size(48.dp) + ) + } + ) +} + +@DefaultPreviews +@Composable +fun PreviewObjectListItem() { + ObjectsListItem( + item = UiObjectsListItem.Item( + id = "123", + name = "Some name", + space = SpaceId("123"), + type = "123", + typeName = "Some type", + createdBy = "Some user", + layout = ObjectType.Layout.BASIC, + icon = ObjectIcon.Empty.Page, + isPossibleToDelete = true + ) + ) +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsListScreen.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsListScreen.kt new file mode 100644 index 0000000000..f67ec55924 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsListScreen.kt @@ -0,0 +1,165 @@ +package com.anytypeio.anytype.core_ui.lists.objects + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.extensions.swapList +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.lists.objects.stubs.StubVerticalItems +import com.anytypeio.anytype.core_ui.views.ButtonSize +import com.anytypeio.anytype.core_ui.views.animations.DotsLoadingIndicator +import com.anytypeio.anytype.core_ui.views.animations.FadeAnimationSpecs +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem +import kotlinx.coroutines.launch + +@Composable +fun PaginatedObjectList( + state: UiObjectsListState, + uiState: UiContentState, + canPaginate: Boolean, + onLoadMore: () -> Unit, + onObjectClicked: (UiObjectsListItem.Item) -> Unit, + onMoveToBin: (UiObjectsListItem.Item) -> Unit, +) { + val items = remember { mutableStateListOf() } + items.swapList(state.items) + + val scope = rememberCoroutineScope() + + val lazyListState = rememberLazyListState() + + val canPaginateState = remember { mutableStateOf(false) } + LaunchedEffect(key1 = canPaginate) { + canPaginateState.value = canPaginate + } + + val shouldStartPaging = remember { + derivedStateOf { + canPaginateState.value && (lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index + ?: -9) >= (lazyListState.layoutInfo.totalItemsCount - 2) + } + } + + LaunchedEffect(key1 = shouldStartPaging.value) { + if (shouldStartPaging.value && uiState is UiContentState.Idle) { + onLoadMore() + } + } + + LazyColumn( + modifier = Modifier + .padding(top = 8.dp) + .fillMaxSize(), + state = lazyListState + ) { + items( + count = items.size, + key = { index -> items[index].id }, + contentType = { index -> + when (items[index]) { + is UiObjectsListItem.Loading -> "loading" + is UiObjectsListItem.Item -> "item" + } + } + ) { index -> + val item = items[index] + when (item) { + is UiObjectsListItem.Item -> { + SwipeToDismissItem( + modifier = Modifier + .fillMaxWidth() + .animateItem() + .noRippleThrottledClickable { + onObjectClicked(item) + }, + item = item, + onObjectClicked = onObjectClicked, + onMoveToBin = onMoveToBin + ) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) + } + is UiObjectsListItem.Loading -> { + ListItemLoading(modifier = Modifier) + } + } + } + if (uiState is UiContentState.Paging) { + item { + Box( + modifier = Modifier + .fillParentMaxWidth() + .height(52.dp), + contentAlignment = Alignment.Center + ) { + LoadingState() + } + } + } + item { + Spacer(modifier = Modifier.height(200.dp)) + } + } + + LaunchedEffect(key1 = uiState) { + if (uiState is UiContentState.Idle) { + if (uiState.scrollToTop) { + scope.launch { + lazyListState.scrollToItem(0) + } + } + } + } +} + +@Composable +private fun BoxScope.LoadingState() { + val loadingAlpha by animateFloatAsState(targetValue = 1f, label = "") + DotsLoadingIndicator( + animating = true, + modifier = Modifier + .graphicsLayer { alpha = loadingAlpha } + .align(Alignment.Center), + animationSpecs = FadeAnimationSpecs(itemCount = 3), + color = colorResource(id = R.color.glyph_active), + size = ButtonSize.Small + ) +} + +@Composable +@DefaultPreviews +fun ObjectsListScreenPreview() { + val contentListState = UiObjectsListState( + items = StubVerticalItems + ) + PaginatedObjectList( + state = contentListState, + uiState = UiContentState.Idle(scrollToTop = false), + canPaginate = true, + onObjectClicked = {}, + onLoadMore = {}, + onMoveToBin = {}, + ) +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsModel.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsModel.kt new file mode 100644 index 0000000000..d9f4abca5a --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/ObjectsModel.kt @@ -0,0 +1,27 @@ +package com.anytypeio.anytype.core_ui.lists.objects + +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem + +sealed class UiContentState { + data class Idle(val scrollToTop: Boolean = false) : UiContentState() + data object InitLoading : UiContentState() + data object Paging : UiContentState() + data object Empty : UiContentState() +} + +data class UiObjectsListState( + val items: List +) { + companion object { + + val Empty = UiObjectsListState(items = emptyList()) + val LoadingState = UiObjectsListState( + items = listOf( + UiObjectsListItem.Loading("Loading-Item-1"), + UiObjectsListItem.Loading("Loading-Item-2"), + UiObjectsListItem.Loading("Loading-Item-3"), + UiObjectsListItem.Loading("Loading-Item-4"), + ) + ) + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/SwipeToDismissItem.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/SwipeToDismissItem.kt new file mode 100644 index 0000000000..85d3f1f0c3 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/SwipeToDismissItem.kt @@ -0,0 +1,89 @@ +package com.anytypeio.anytype.core_ui.lists.objects + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.foundation.DismissBackground +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem +import kotlinx.coroutines.delay + +@Composable +fun SwipeToDismissItem( + item: UiObjectsListItem.Item, + modifier: Modifier, + animationDuration: Int = 500, + onObjectClicked: (UiObjectsListItem.Item) -> Unit, + onMoveToBin: (UiObjectsListItem.Item) -> Unit, +) { + var isRemoved by remember { mutableStateOf(false) } + val dismissState = rememberSwipeToDismissBoxState( + initialValue = SwipeToDismissBoxValue.Settled, + confirmValueChange = { value -> + if (value == SwipeToDismissBoxValue.EndToStart) { + isRemoved = true + true + } else { + false + } + return@rememberSwipeToDismissBoxState true + }, + positionalThreshold = { it * .5f } + ) + + if (dismissState.currentValue != SwipeToDismissBoxValue.Settled) { + LaunchedEffect(Unit) { + dismissState.snapTo(SwipeToDismissBoxValue.Settled) + } + } + + LaunchedEffect(key1 = isRemoved) { + if (isRemoved) { + delay(animationDuration.toLong()) + onMoveToBin(item) + } + } + AnimatedVisibility( + visible = !isRemoved, + exit = shrinkVertically( + animationSpec = tween(durationMillis = animationDuration), + shrinkTowards = Alignment.Top + ) + fadeOut() + ) { + SwipeToDismissBox( + modifier = modifier, + state = dismissState, + enableDismissFromEndToStart = item.isPossibleToDelete, + enableDismissFromStartToEnd = false, + backgroundContent = { + DismissBackground( + actionText = stringResource(R.string.move_to_bin), + dismissState = dismissState + ) + }, + content = { + ObjectsListItem( + modifier = Modifier + .noRippleThrottledClickable { + onObjectClicked(item) + }, + item = item + ) + } + ) + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/menu/SortingMenu.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/menu/SortingMenu.kt new file mode 100644 index 0000000000..884b7952b1 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/menu/SortingMenu.kt @@ -0,0 +1,212 @@ +package com.anytypeio.anytype.core_ui.lists.objects.menu + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.ColorFilter.Companion.tint +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_models.DVSortType +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular +import com.anytypeio.anytype.core_ui.views.UXBody +import com.anytypeio.anytype.presentation.objects.MenuSortsItem +import com.anytypeio.anytype.presentation.objects.ObjectsListSort + +@Composable +fun ObjectsListSortingMenuContainer( + container: MenuSortsItem.Container, + sorts: List, + types: List, + sortingExpanded: Boolean, + onChangeSortExpandedState: (Boolean) -> Unit, + onSortClick: (ObjectsListSort) -> Unit +) { + + SortingBox( + modifier = Modifier + .clickable { + onChangeSortExpandedState(!sortingExpanded) + }, + subtitle = container.sort.title(), + isExpanded = sortingExpanded + ) + Divider( + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_secondary) + ) + if (sortingExpanded) { + sorts.forEach { item -> + ObjectsListMenuItem( + title = item.sort.title(), + isSelected = item.sort.isSelected, + modifier = Modifier + .clickable { + onSortClick(item.sort) + } + ) + Divider( + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_secondary) + ) + } + Divider( + height = 7.5.dp, + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_secondary) + ) + val size = types.size + types.forEachIndexed { index, item -> + val s = item.sort + ObjectsListMenuItem( + title = item.sortType.title(item.sort), + isSelected = item.isSelected, + modifier = Modifier + .clickable { + val updatedSort = when (val sort = item.sort) { + is ObjectsListSort.ByName -> sort.copy(sortType = item.sortType) + is ObjectsListSort.ByDateCreated -> sort.copy(sortType = item.sortType) + is ObjectsListSort.ByDateUpdated -> sort.copy(sortType = item.sortType) + is ObjectsListSort.ByDateUsed -> sort.copy(sortType = item.sortType) + } + onSortClick(updatedSort) + } + ) + if (index < size - 1) { + Divider( + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_secondary) + ) + } + } + } +} + +@Composable +private fun SortingBox(modifier: Modifier, subtitle: String, isExpanded: Boolean) { + val rotationAngle = if (isExpanded) 90f else 0f + Row( + modifier = modifier + .fillMaxWidth() + .background(colorResource(id = R.color.background_secondary)), + verticalAlignment = CenterVertically + ) { + Image( + modifier = Modifier + .padding(start = 10.dp) + .size(18.dp) + .rotate(rotationAngle), + painter = painterResource(R.drawable.ic_arrow_disclosure_18), + contentDescription = "", + colorFilter = tint(colorResource(id = R.color.glyph_selected)) + ) + Column( + modifier = Modifier + .wrapContentHeight() + .padding(top = 11.dp, bottom = 10.dp, start = 6.dp) + ) { + Text( + text = stringResource(id = R.string.all_content_sort_by), + modifier = Modifier.wrapContentSize(), + style = UXBody, + color = colorResource(id = R.color.text_primary) + ) + Text( + text = subtitle, + modifier = Modifier.wrapContentSize(), + style = BodyCalloutRegular, + color = colorResource(id = R.color.text_secondary) + ) + } + } +} + +@Composable +fun ObjectsListMenuItem( + modifier: Modifier, + title: String, + isSelected: Boolean, + contentDescription: String? = null +) { + Row( + modifier = modifier + .fillMaxWidth() + .height(44.dp) + .background(colorResource(id = R.color.background_secondary)), + verticalAlignment = CenterVertically, + ) { + Image( + modifier = Modifier + .wrapContentSize() + .padding(start = 12.dp), + painter = painterResource(R.drawable.ic_check_16), + contentDescription = contentDescription, + alpha = if (isSelected) 1f else 0f + ) + Text( + text = title, + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp), + style = UXBody, + color = colorResource(id = R.color.text_primary) + ) + } +} + +@Composable +fun ObjectsListSort.title(): String = stringResource( + when (this) { + is ObjectsListSort.ByDateCreated -> R.string.all_content_sort_date_created + is ObjectsListSort.ByDateUpdated -> R.string.all_content_sort_date_updated + is ObjectsListSort.ByName -> R.string.all_content_sort_name + is ObjectsListSort.ByDateUsed -> R.string.all_content_sort_date_used + } +) + +@Composable +fun DVSortType.title(sort: ObjectsListSort): String = when (this) { + DVSortType.ASC -> { + when (sort) { + is ObjectsListSort.ByDateCreated, is ObjectsListSort.ByDateUpdated, is ObjectsListSort.ByDateUsed -> stringResource( + id = R.string.all_content_sort_date_asc + ) + + is ObjectsListSort.ByName -> stringResource(id = R.string.all_content_sort_name_asc) + } + } + + DVSortType.DESC -> { + when (sort) { + is ObjectsListSort.ByDateCreated, + is ObjectsListSort.ByDateUpdated, + is ObjectsListSort.ByDateUsed -> stringResource( + id = R.string.all_content_sort_date_desc + ) + + is ObjectsListSort.ByName -> stringResource(id = R.string.all_content_sort_name_desc) + } + } + + DVSortType.CUSTOM -> "" +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/stubs/Stubs.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/stubs/Stubs.kt new file mode 100644 index 0000000000..b43edd3a31 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/lists/objects/stubs/Stubs.kt @@ -0,0 +1,42 @@ +package com.anytypeio.anytype.core_ui.lists.objects.stubs + +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.presentation.objects.ObjectIcon +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem + +val StubVerticalItems = listOf( + UiObjectsListItem.Item( + id = "1", + name = "Task Object", + space = SpaceId("space1"), + type = "type1", + typeName = "Task", + createdBy = "by Joseph Wolf", + layout = ObjectType.Layout.TODO, + icon = ObjectIcon.Task(isChecked = true) + ), + UiObjectsListItem.Item( + id = "2", + name = "Page Object", + space = SpaceId("space2"), + type = "type2", + typeName = "Page", + createdBy = "by Mike Long", + layout = ObjectType.Layout.BASIC, + icon = ObjectIcon.Empty.Page + ), + UiObjectsListItem.Item( + id = "3", + name = "File Object", + space = SpaceId("space3"), + type = "type3", + typeName = "File", + createdBy = "by John Doe", + layout = ObjectType.Layout.FILE, + icon = ObjectIcon.File( + mime = "image/png", + fileName = "test_image.png" + ) + ) +) \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconCompose.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconCompose.kt index 370a32893b..58f68aaf84 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconCompose.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconCompose.kt @@ -32,7 +32,8 @@ fun ListWidgetObjectIcon( icon: ObjectIcon, modifier: Modifier, iconSize: Dp = 48.dp, - onTaskIconClicked: (Boolean) -> Unit = {} + onTaskIconClicked: (Boolean) -> Unit = {}, + backgroundColor: Int = R.color.shape_tertiary ) { when (icon) { is ObjectIcon.Profile.Avatar -> { @@ -46,7 +47,7 @@ fun ListWidgetObjectIcon( DefaultProfileIconImage(icon, modifier, iconSize) } is ObjectIcon.Basic.Emoji -> { - EmojiIconView(icon = icon, backgroundSize = iconSize, modifier = modifier) + EmojiIconView(icon = icon, backgroundSize = iconSize, modifier = modifier, backgroundColor = backgroundColor) } is ObjectIcon.Basic.Image -> { DefaultObjectImageIcon(icon.hash, modifier, iconSize, fallback = icon.emptyState) @@ -77,7 +78,8 @@ fun ListWidgetObjectIcon( EmptyIconView( modifier = modifier, emptyType = icon, - backgroundSize = iconSize + backgroundSize = iconSize, + backgroundColor = backgroundColor ) } ObjectIcon.None -> {} diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectMenuDescriptionItem.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectMenuDescriptionItem.kt new file mode 100644 index 0000000000..9e1925e948 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectMenuDescriptionItem.kt @@ -0,0 +1,24 @@ +package com.anytypeio.anytype.core_ui.widgets + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import com.anytypeio.anytype.core_ui.databinding.WidgetObjectMenuDescriptionBinding +import com.anytypeio.anytype.core_ui.R + +class ObjectMenuDescriptionItem @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { + + + val binding = WidgetObjectMenuDescriptionBinding.inflate( + LayoutInflater.from(context), this + ) + + fun setAction(setAsHide: Boolean) { + binding.descriptionAction.text = + if (setAsHide) context.getString(R.string.modal_hide) else context.getString(R.string.modal_show) + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectMenuItemWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectMenuItemWidget.kt index c61c790ea1..63d7f4a2c4 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectMenuItemWidget.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectMenuItemWidget.kt @@ -25,7 +25,6 @@ class ObjectMenuItemWidget @JvmOverloads constructor( if (set == null) return val attrs = context.obtainStyledAttributes(set, R.styleable.ObjectMenuItemWidget, 0, 0) tvTitle.text = attrs.getString(R.styleable.ObjectMenuItemWidget_title) - tvSubtitle.text = attrs.getString(R.styleable.ObjectMenuItemWidget_subtitle) ivIcon.setImageResource(attrs.getResourceId(R.styleable.ObjectMenuItemWidget_icon, -1)) attrs.recycle() } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/TemplatesPreview.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/TemplatesPreview.kt new file mode 100644 index 0000000000..1348696bd1 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/TemplatesPreview.kt @@ -0,0 +1,72 @@ +package com.anytypeio.anytype.core_ui.widgets + +import androidx.compose.runtime.Composable +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_models.primitives.TypeId +import com.anytypeio.anytype.core_models.primitives.TypeKey +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.presentation.editor.cover.CoverColor +import com.anytypeio.anytype.presentation.templates.TemplateObjectTypeView +import com.anytypeio.anytype.presentation.templates.TemplateView +import com.anytypeio.anytype.presentation.templates.TemplateView.Companion.DEFAULT_TEMPLATE_ID_BLANK +import com.anytypeio.anytype.presentation.widgets.TypeTemplatesWidgetUI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +@DefaultPreviews +@Composable +fun TypeTemplatesWidgetPreview() { + val items = listOf( + TemplateView.Blank( + id = DEFAULT_TEMPLATE_ID_BLANK, + targetTypeId = TypeId("page"), + targetTypeKey = TypeKey("ot-page"), + typeName = "Page", + layout = ObjectType.Layout.BASIC.code + ), + TemplateView.Template( + id = "1", + name = "Template Title", + targetTypeId = TypeId("page"), + targetTypeKey = TypeKey("ot-page"), + layout = ObjectType.Layout.PROFILE, + image = null, + emoji = "📄", + coverColor = CoverColor.RED, + coverGradient = null, + coverImage = null, + isDefault = true + ), + ) + val state = TypeTemplatesWidgetUI.Data( + templates = items, + showWidget = true, + isEditing = true, + moreMenuItem = null, + objectTypes = listOf( + TemplateObjectTypeView.Search, + TemplateObjectTypeView.Item( + type = ObjectWrapper.Type( + map = mapOf(Relations.ID to "123", Relations.NAME to "Page"), + ) + ) + ), + viewerId = "", + isPossibleToChangeType = true, + isPossibleToChangeTemplate = false + ) + TypeTemplatesWidget( + state = state, + onDismiss = {}, + editClick = {}, + doneClick = {}, + moreClick = {}, + scope = CoroutineScope( + Dispatchers.Main + ), + menuClick = {}, + action = {} + ) +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/TypeTemplatesWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/TypeTemplatesWidget.kt index a628600429..57722ea5fd 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/TypeTemplatesWidget.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/TypeTemplatesWidget.kt @@ -14,7 +14,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -71,7 +73,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -80,19 +81,17 @@ import androidx.compose.ui.unit.sp import coil.compose.rememberAsyncImagePainter 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_models.primitives.TypeId -import com.anytypeio.anytype.core_models.primitives.TypeKey import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.AvatarTitle import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular import com.anytypeio.anytype.core_ui.views.BodyRegular import com.anytypeio.anytype.core_ui.views.Caption1Medium +import com.anytypeio.anytype.core_ui.views.Caption2Medium import com.anytypeio.anytype.core_ui.views.Caption2Semibold import com.anytypeio.anytype.core_ui.views.Title1 import com.anytypeio.anytype.core_ui.views.fontInterRegular @@ -101,14 +100,15 @@ import com.anytypeio.anytype.presentation.editor.cover.CoverGradient import com.anytypeio.anytype.presentation.templates.TemplateMenuClick import com.anytypeio.anytype.presentation.templates.TemplateObjectTypeView import com.anytypeio.anytype.presentation.templates.TemplateView -import com.anytypeio.anytype.presentation.templates.TemplateView.Companion.DEFAULT_TEMPLATE_ID_BLANK import com.anytypeio.anytype.presentation.widgets.TypeTemplatesWidgetUI import com.anytypeio.anytype.presentation.widgets.TypeTemplatesWidgetUIAction import com.anytypeio.anytype.presentation.widgets.TypeTemplatesWidgetUIAction.TemplateClick import com.anytypeio.anytype.presentation.widgets.TypeTemplatesWidgetUIAction.TypeClick +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.bumptech.glide.integration.compose.placeholder import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import timber.log.Timber @@ -414,7 +414,7 @@ private fun TemplatesList( scrollState: LazyListState, state: TypeTemplatesWidgetUI.Data, action: (TypeTemplatesWidgetUIAction) -> Unit, - moreClick: (TemplateView, IntOffset) -> Unit + moreClick: (TemplateView, IntOffset) -> Unit, ) { LazyRow( state = scrollState, @@ -497,7 +497,10 @@ private fun TemplatesList( } @Composable -private fun TemplateItemContent(item: TemplateView) { +fun BoxScope.TemplateItemContent( + item: TemplateView, + showDefaultIcon: Boolean = false +) { Column { when (item) { is TemplateView.Blank -> { @@ -506,63 +509,20 @@ private fun TemplateItemContent(item: TemplateView) { } is TemplateView.Template -> { - if (item.isCoverPresent()) { - TemplateItemCoverAndIcon(item) - if (item.layout == ObjectType.Layout.TODO) { - Spacer(modifier = Modifier.height(12.dp)) - TemplateItemTodoTitle(text = item.name) - } else { - if (!item.isImageOrEmojiPresent()) { - Spacer(modifier = Modifier.height(12.dp)) - } else { - Spacer(modifier = Modifier.height(6.dp)) - } - TemplateItemTitle( - text = item.name, - textAlign = getProperTextAlign(item.layout) - ) + when (item.layout) { + ObjectType.Layout.BASIC -> { + TemplateHeaderBasic(item) } - } else { - if (item.layout == ObjectType.Layout.TODO) { + ObjectType.Layout.PROFILE -> { + TemplateHeaderProfile(item) + } + ObjectType.Layout.TODO -> { + TemplateHeaderTask(item) + } + else -> { Spacer(modifier = Modifier.height(28.dp)) - TemplateItemTodoTitle(text = item.name) - } else { - if (item.isImageOrEmojiPresent()) { - if (item.layout.isProfileOrParticipant()) { - Box( - modifier = Modifier - .wrapContentWidth() - .height(68.dp) - .padding(top = 28.dp) - .align(Alignment.CenterHorizontally) - ) { - val modifier = Modifier.clip(CircleShape) - TemplateItemIconOrImage(item = item, modifier = modifier) - } - Spacer(modifier = Modifier.height(6.dp)) - TemplateItemTitle( - text = item.name, - textAlign = getProperTextAlign(item.layout) - ) - } else { - val modifier = Modifier - .padding(start = 14.dp, top = 26.dp) - TemplateItemIconOrImage(item = item, modifier = modifier) - Spacer(modifier = Modifier.height(8.dp)) - TemplateItemTitle( - text = item.name, textAlign = getProperTextAlign(item.layout) - ) - } - } else { - Spacer(modifier = Modifier.height(28.dp)) - TemplateItemTitle( - text = item.name, - textAlign = getProperTextAlign(item.layout) - ) - } } } - Spacer(modifier = Modifier.height(8.dp)) TemplateItemRectangles() } @@ -581,8 +541,104 @@ private fun TemplateItemContent(item: TemplateView) { } } } + if (showDefaultIcon && item.isDefault) { + Text( + modifier = Modifier + .padding(bottom = 8.dp) + .wrapContentSize() + .background( + shape = RoundedCornerShape(4.dp), + color = colorResource(R.color.shape_tertiary) + ) + .padding(start = 6.dp, end = 6.dp, top = 2.dp, bottom = 2.dp) + .align(Alignment.BottomCenter), + text = stringResource(R.string.default_template_icon), + textAlign = TextAlign.Center, + color = colorResource(R.color.text_secondary), + style = Caption2Medium + ) + } } +@Composable +private fun ColumnScope.TemplateHeaderBasic(item: TemplateView.Template) { + if (item.isCoverPresent()) { + TemplateItemCoverAndIcon(item) + } else { + Spacer(modifier = Modifier.height(28.dp)) + TemplateItemIconOrImage(item = item) + } + Spacer(modifier = Modifier.height(12.dp)) + TemplateItemTitle( + text = item.name, + textAlign = getProperTextAlign(item.layout) + ) +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +private fun ColumnScope.TemplateHeaderProfile(item: TemplateView.Template) { + if (item.isCoverPresent()) { + TemplateItemCoverAndIcon(item) + } else { + ProfileIcon( + modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 28.dp), + item = item + ) + } + Spacer(modifier = Modifier.height(12.dp)) + TemplateItemTitle( + text = item.name, + textAlign = getProperTextAlign(item.layout) + ) +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +private fun ProfileIcon( + modifier: Modifier = Modifier, + item: TemplateView.Template +) { + Box( + modifier = modifier + .size(40.dp) + .background( + color = colorResource(id = R.color.shape_tertiary), + shape = CircleShape + ) + ) { + if (item.image != null) { + GlideImage( + model = item.image, + contentDescription = "Custom image template's icon", + modifier = Modifier + .size(40.dp) + .align(Alignment.Center), + contentScale = ContentScale.Crop + ) + } else { + Text( + modifier = Modifier.align(Alignment.Center), + text = item.name.firstOrNull()?.toString() ?: "U", + style = AvatarTitle.copy( + fontSize = 24.sp, + ), + color = colorResource(id = R.color.glyph_active) + ) + } + } +} + +@Composable +private fun ColumnScope.TemplateHeaderTask(item: TemplateView.Template) { + TemplateItemCoverColor(item = item) + TemplateItemCoverImage(item = item) + TemplateItemCoverGradient(item = item) + Spacer(modifier = Modifier.height(12.dp)) + TemplateItemTodoTitle(text = item.name) +} + +@OptIn(ExperimentalGlideComposeApi::class) @Composable private fun TemplateItemIconOrImage( item: TemplateView.Template, @@ -591,22 +647,16 @@ private fun TemplateItemIconOrImage( item.image?.let { Box( modifier = modifier - .wrapContentSize() - .border( - width = 2.dp, - color = colorResource(id = R.color.background_primary), - shape = RoundedCornerShape(2.dp) - ) - .clip(RoundedCornerShape(2.dp)) + .padding(start = 16.dp) + .size(40.dp) .background( - color = colorResource(id = R.color.shape_tertiary) + color = colorResource(id = R.color.shape_tertiary), + shape = RoundedCornerShape(5.dp) ) ) { - Image( - painter = rememberAsyncImagePainter( - model = it, - error = painterResource(id = R.drawable.ic_home_widget_space) - ), + GlideImage( + model = it, + failure = placeholder(painterResource(id = R.drawable.ic_home_widget_space)), contentDescription = "Custom image template's icon", modifier = Modifier .size(40.dp) @@ -618,17 +668,12 @@ private fun TemplateItemIconOrImage( item.emoji?.let { Box( modifier = modifier - .wrapContentSize() - .border( - width = 2.dp, - color = colorResource(id = R.color.background_primary), - shape = RoundedCornerShape(8.dp) - ) - .clip(RoundedCornerShape(8.dp)) + .padding(start = 16.dp) + .size(40.dp) .background( - color = colorResource(id = R.color.shape_tertiary) + color = colorResource(id = R.color.text_tertiary), + shape = RoundedCornerShape(5.dp) ) - .padding(8.dp) ) { Image( painter = rememberAsyncImagePainter( @@ -645,6 +690,7 @@ private fun TemplateItemIconOrImage( } } +@OptIn(ExperimentalGlideComposeApi::class) @Composable private fun TemplateItemCoverAndIcon(item: TemplateView.Template) { Box( @@ -658,20 +704,11 @@ private fun TemplateItemCoverAndIcon(item: TemplateView.Template) { when (item.layout) { ObjectType.Layout.TODO -> {} ObjectType.Layout.PROFILE, ObjectType.Layout.PARTICIPANT -> { - Box( - modifier = Modifier - .wrapContentWidth() - .height(82.dp) - .padding(top = 50.dp) - .align(Alignment.TopCenter) - ) { - val modifier = Modifier - .clip(CircleShape) - .align(Alignment.Center) - TemplateItemIconOrImage(item = item, modifier = modifier) - } + ProfileIcon( + modifier = Modifier.align(Alignment.BottomCenter), + item = item + ) } - else -> { val modifier = Modifier .padding(start = 14.dp, top = 44.dp) @@ -991,65 +1028,4 @@ fun ObjectTypesList( enum class DragStates { VISIBLE, DISMISSED -} - - -@Preview -@Composable -fun ComposablePreview() { - val items = listOf( - TemplateView.Blank( - id = DEFAULT_TEMPLATE_ID_BLANK, - targetTypeId = TypeId("page"), - targetTypeKey = TypeKey("ot-page"), - typeName = "Page", - layout = ObjectType.Layout.BASIC.code - ), - TemplateView.Template( - id = "1", - name = "Template 1", - targetTypeId = TypeId("page"), - targetTypeKey = TypeKey("ot-page"), - layout = ObjectType.Layout.BASIC, - image = null, - emoji = null, - coverColor = null, - coverGradient = null, - coverImage = null, - ), - ) - val state = TypeTemplatesWidgetUI.Data( - templates = items, - showWidget = true, - isEditing = true, - moreMenuItem = TemplateView.Template( - id = "123", - name = "Template 1", - targetTypeId = TypeId("page"), - targetTypeKey = TypeKey("ot-page"), - ), - objectTypes = listOf( - TemplateObjectTypeView.Search, - TemplateObjectTypeView.Item( - type = ObjectWrapper.Type( - map = mapOf(Relations.ID to "123", Relations.NAME to "Page"), - ) - ) - ), - viewerId = "", - isPossibleToChangeType = true, - isPossibleToChangeTemplate = false - ) - TypeTemplatesWidget( - state = state, - onDismiss = {}, - editClick = {}, - doneClick = {}, - moreClick = {}, - scope = CoroutineScope( - Dispatchers.Main - ), - menuClick = {}, - action = {} - ) } \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/ic_obj_settings_cover_24.xml b/core-ui/src/main/res/drawable/ic_obj_settings_cover_24.xml new file mode 100644 index 0000000000..92434b30d9 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_obj_settings_cover_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_obj_settings_description_24.xml b/core-ui/src/main/res/drawable/ic_obj_settings_description_24.xml new file mode 100644 index 0000000000..e065e896be --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_obj_settings_description_24.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/core-ui/src/main/res/drawable/ic_obj_settings_fields_24.xml b/core-ui/src/main/res/drawable/ic_obj_settings_fields_24.xml new file mode 100644 index 0000000000..2ece8103d1 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_obj_settings_fields_24.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/core-ui/src/main/res/drawable/ic_obj_settings_history_24.xml b/core-ui/src/main/res/drawable/ic_obj_settings_history_24.xml new file mode 100644 index 0000000000..b87b620d2f --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_obj_settings_history_24.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/core-ui/src/main/res/drawable/ic_obj_settings_icon_24.xml b/core-ui/src/main/res/drawable/ic_obj_settings_icon_24.xml new file mode 100644 index 0000000000..1a54e12f9f --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_obj_settings_icon_24.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/core-ui/src/main/res/drawable/ic_obj_settings_layout_24.xml b/core-ui/src/main/res/drawable/ic_obj_settings_layout_24.xml new file mode 100644 index 0000000000..97a574d044 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_obj_settings_layout_24.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/core-ui/src/main/res/drawable/ic_section_local_fields.xml b/core-ui/src/main/res/drawable/ic_section_local_fields.xml new file mode 100644 index 0000000000..ec92f6a450 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_section_local_fields.xml @@ -0,0 +1,16 @@ + + + + diff --git a/core-ui/src/main/res/drawable/ic_settings_24.xml b/core-ui/src/main/res/drawable/ic_settings_24.xml new file mode 100644 index 0000000000..7b6c06a0f6 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_settings_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_type_layout_basic_icon.xml b/core-ui/src/main/res/drawable/ic_type_layout_basic_icon.xml new file mode 100644 index 0000000000..84c32f7d3a --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_type_layout_basic_icon.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/core-ui/src/main/res/drawable/ic_type_layout_todo_icon.xml b/core-ui/src/main/res/drawable/ic_type_layout_todo_icon.xml new file mode 100644 index 0000000000..60d3f52436 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_type_layout_todo_icon.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core-ui/src/main/res/layout/widget_object_menu_description.xml b/core-ui/src/main/res/layout/widget_object_menu_description.xml new file mode 100644 index 0000000000..3b64d13efb --- /dev/null +++ b/core-ui/src/main/res/layout/widget_object_menu_description.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/widget_object_menu_item.xml b/core-ui/src/main/res/layout/widget_object_menu_item.xml index 49f060e446..46db34ebea 100644 --- a/core-ui/src/main/res/layout/widget_object_menu_item.xml +++ b/core-ui/src/main/res/layout/widget_object_menu_item.xml @@ -1,13 +1,14 @@ - - - + android:layout_marginEnd="12dp" + android:src="@drawable/ic_arrow_forward_24" /> \ No newline at end of file diff --git a/core-ui/src/main/res/values-night/colors.xml b/core-ui/src/main/res/values-night/colors.xml index 0d9db42013..e0cb7a5b2e 100644 --- a/core-ui/src/main/res/values-night/colors.xml +++ b/core-ui/src/main/res/values-night/colors.xml @@ -41,6 +41,9 @@ #99000000 #99252525 + + #BF252525 + #F3F2EC #E0D56C diff --git a/core-ui/src/main/res/values/colors.xml b/core-ui/src/main/res/values/colors.xml index 9fba0d3008..167825c824 100644 --- a/core-ui/src/main/res/values/colors.xml +++ b/core-ui/src/main/res/values/colors.xml @@ -79,6 +79,9 @@ #FFFFFF #99F5F5F5 + + #FFFFFF + #252525 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 419f8b73fc..18a70652b9 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 @@ -2,6 +2,7 @@ package com.anytypeio.anytype.data.auth.repo.block import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Command.ObjectTypeConflictingFields import com.anytypeio.anytype.core_models.Config import com.anytypeio.anytype.core_models.CreateBlockLinkWithObjectResult import com.anytypeio.anytype.core_models.CreateObjectResult @@ -307,12 +308,18 @@ class BlockDataRepository( override suspend fun createSet( space: Id, - objectType: String? + objectType: String?, + details: Struct? ): CreateObjectSet.Response { - val result = remote.createSet(space = space, objectType = objectType) + val result = remote.createSet( + space = space, + objectType = objectType, + details = details + ) return CreateObjectSet.Response( - target = result.targetId, - payload = result.payload + target = result.objectId, + payload = result.payload, + details = result.details ) } @@ -1101,4 +1108,16 @@ class BlockDataRepository( override suspend fun setDeviceNetworkState(type: DeviceNetworkType) { remote.setDeviceNetworkState(type) } + + override suspend fun objectTypeListConflictingRelations(command: ObjectTypeConflictingFields): List { + return remote.objectTypeListConflictingRelations(command) + } + + override suspend fun objectTypeSetRecommendedHeaderFields(command: Command.ObjectTypeSetRecommendedHeaderFields) { + remote.objectTypeSetRecommendedHeaderFields(command) + } + + override suspend fun objectTypeSetRecommendedFields(command: Command.ObjectTypeSetRecommendedFields) { + remote.objectTypeSetRecommendedFields(command) + } } \ No newline at end of file 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 1e7b874c28..ddba8f6881 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 @@ -2,6 +2,7 @@ package com.anytypeio.anytype.data.auth.repo.block import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Command.ObjectTypeConflictingFields import com.anytypeio.anytype.core_models.Config import com.anytypeio.anytype.core_models.CreateBlockLinkWithObjectResult import com.anytypeio.anytype.core_models.CreateObjectResult @@ -106,7 +107,8 @@ interface BlockRemote { suspend fun createSet( space: Id, - objectType: String? + objectType: String?, + details: Struct? ): Response.Set.Create suspend fun setDataViewViewerPosition( @@ -468,4 +470,9 @@ interface BlockRemote { suspend fun debugAccountSelectTrace(dir: String): String suspend fun setDeviceNetworkState(type: DeviceNetworkType) + + suspend fun objectTypeListConflictingRelations(command: ObjectTypeConflictingFields): List + + suspend fun objectTypeSetRecommendedHeaderFields(command: Command.ObjectTypeSetRecommendedHeaderFields) + suspend fun objectTypeSetRecommendedFields(command: Command.ObjectTypeSetRecommendedFields) } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/CreateObjectSet.kt b/domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/CreateObjectSet.kt index 3cb2f6975d..9292622515 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/CreateObjectSet.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/block/interactor/sets/CreateObjectSet.kt @@ -2,6 +2,7 @@ package com.anytypeio.anytype.domain.block.interactor.sets import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_models.Struct import com.anytypeio.anytype.domain.base.BaseUseCase import com.anytypeio.anytype.domain.block.repo.BlockRepository @@ -10,12 +11,17 @@ class CreateObjectSet( ) : BaseUseCase() { override suspend fun run(params: Params) = safe { - repo.createSet(space = params.space, objectType = params.type) + repo.createSet( + space = params.space, + objectType = params.type, + details = params.details + ) } data class Params( val space: Id, - val type: Id? = null + val type: Id? = null, + val details: Struct? = null ) /** @@ -23,6 +29,7 @@ class CreateObjectSet( */ data class Response( val target: Id, - val payload: Payload + val payload: Payload, + val details: Struct ) } \ No newline at end of file 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 46d43d1538..f893fa119a 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 @@ -2,6 +2,7 @@ package com.anytypeio.anytype.domain.block.repo import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Command.ObjectTypeConflictingFields import com.anytypeio.anytype.core_models.Config import com.anytypeio.anytype.core_models.CreateBlockLinkWithObjectResult import com.anytypeio.anytype.core_models.CreateObjectResult @@ -159,7 +160,8 @@ interface BlockRepository { suspend fun createSet( space: Id, - objectType: String? = null + objectType: String? = null, + details: Struct? = null ): CreateObjectSet.Response suspend fun addRelationToDataView(ctx: Id, dv: Id, relation: Key): Payload @@ -511,4 +513,9 @@ interface BlockRepository { suspend fun objectDateByTimestamp(command: Command.ObjectDateByTimestamp): Struct? suspend fun setDeviceNetworkState(type: DeviceNetworkType) + + suspend fun objectTypeListConflictingRelations(command: ObjectTypeConflictingFields): List + + suspend fun objectTypeSetRecommendedHeaderFields(command: Command.ObjectTypeSetRecommendedHeaderFields) + suspend fun objectTypeSetRecommendedFields(command: Command.ObjectTypeSetRecommendedFields) } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/objects/ObjectWatcher.kt b/domain/src/main/java/com/anytypeio/anytype/domain/objects/ObjectWatcher.kt index d301cd0fc6..d6024f03ff 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/objects/ObjectWatcher.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/objects/ObjectWatcher.kt @@ -8,6 +8,7 @@ import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.event.interactor.EventChannel import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.scan @@ -17,17 +18,38 @@ class ObjectWatcher @Inject constructor( private val events: EventChannel, private val reducer: Reducer ) { + /** + * Watches an object and returns a flow of ObjectView. + * If opening the object or observing events fails, the flow will throw an exception. + */ fun watch(target: Id, space: SpaceId): Flow = flow { - emitAll( - events.observeEvents(context = target).scan( - initial = repo.openObject(id = target, space = space), - operation = reducer - ) - ) + // Attempt to open the object. If it fails, throw. + val initialObjectView = try { + repo.openObject(id = target, space = space) + } catch (e: Exception) { + throw RuntimeException("Failed to open object with id=$target in space=$space", e) + } + + // Start observing events. If events flow fails at any point, it will throw. + val eventFlow = events.observeEvents(context = target) + .catch { e -> throw RuntimeException("Failed to observe events for object=$target", e) } + + emitAll(eventFlow.scan(initialObjectView, reducer)) + }.catch { e -> + // Optional: If you want a centralized place to rethrow or log, you can do it here. + throw e } + /** + * Stops watching an object. + * If closing the page fails, it will throw an exception. + */ suspend fun unwatch(target: Id, space: SpaceId) { - repo.closePage(id = target, space = space) + try { + repo.closePage(id = target, space = space) + } catch (e: Exception) { + throw RuntimeException("Failed to unwatch object with id=$target in space=$space", e) + } } interface Reducer : (ObjectView, List) -> ObjectView diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfObjectTypes.kt b/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfObjectTypes.kt index cd1e52e818..bdfc922eef 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfObjectTypes.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfObjectTypes.kt @@ -6,6 +6,10 @@ import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Struct import com.anytypeio.anytype.domain.`object`.amend import com.anytypeio.anytype.domain.`object`.unset +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes.TrackedEvent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -20,6 +24,13 @@ interface StoreOfObjectTypes { suspend fun set(target: Id, data: Struct) suspend fun remove(target: Id) suspend fun clear() + + fun trackChanges() : Flow + + sealed class TrackedEvent { + object Init : TrackedEvent() + object Change: TrackedEvent() + } } class DefaultStoreOfObjectTypes : StoreOfObjectTypes { @@ -27,6 +38,8 @@ class DefaultStoreOfObjectTypes : StoreOfObjectTypes { private val mutex = Mutex() private val store = mutableMapOf() + private val updates = MutableSharedFlow() + override val size: Int get() = store.size override suspend fun get(id: Id): ObjectWrapper.Type? = mutex.withLock { @@ -50,6 +63,7 @@ class DefaultStoreOfObjectTypes : StoreOfObjectTypes { store[o.id] = current.amend(o.map) } } + updates.emit(TrackedEvent.Change) } override suspend fun amend(target: Id, diff: Map): Unit = mutex.withLock { @@ -59,6 +73,7 @@ class DefaultStoreOfObjectTypes : StoreOfObjectTypes { } else { store[target] = ObjectWrapper.Type(diff) } + updates.emit(TrackedEvent.Change) } override suspend fun set( @@ -66,6 +81,7 @@ class DefaultStoreOfObjectTypes : StoreOfObjectTypes { data: Map ): Unit = mutex.withLock { store[target] = ObjectWrapper.Type(data) + updates.emit(TrackedEvent.Change) } override suspend fun unset( @@ -76,6 +92,7 @@ class DefaultStoreOfObjectTypes : StoreOfObjectTypes { if (current != null) { store[target] = current.unset(keys) } + updates.emit(TrackedEvent.Change) } override suspend fun remove(target: Id) : Unit = mutex.withLock { @@ -83,9 +100,14 @@ class DefaultStoreOfObjectTypes : StoreOfObjectTypes { if (current != null) { store.remove(target) } + updates.emit(TrackedEvent.Change) } override suspend fun clear(): Unit = mutex.withLock { store.clear() } + + override fun trackChanges(): Flow = updates.onStart { + emit(TrackedEvent.Init) + } } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfRelations.kt b/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfRelations.kt index d596697ace..e0cceea2c1 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfRelations.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/objects/StoreOfRelations.kt @@ -132,4 +132,10 @@ class DefaultStoreOfRelations : StoreOfRelations { override suspend fun observe(): Flow> { return trackChanges().map { store } } +} + +suspend fun StoreOfRelations.getValidRelations(ids: List): List { + return ids.mapNotNull { id -> + getById(id)?.takeIf { it.isValidToUse } + } } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/primitives/FieldParser.kt b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/FieldParser.kt index d3f4d0bf2f..81ec536e73 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/primitives/FieldParser.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/FieldParser.kt @@ -1,10 +1,10 @@ package com.anytypeio.anytype.domain.primitives import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Key import com.anytypeio.anytype.core_models.MAX_SNIPPET_SIZE import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectTypeIds -import com.anytypeio.anytype.core_models.ObjectView import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.RelativeDate @@ -12,6 +12,7 @@ import com.anytypeio.anytype.core_models.SupportedLayouts import com.anytypeio.anytype.core_models.TimeInSeconds import com.anytypeio.anytype.core_models.primitives.Field import com.anytypeio.anytype.core_models.primitives.FieldDateValue +import com.anytypeio.anytype.core_models.primitives.ParsedFields import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.primitives.TimestampInSeconds import com.anytypeio.anytype.core_models.primitives.Value @@ -19,9 +20,12 @@ import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.debugging.Logger import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp +import com.anytypeio.anytype.domain.objects.StoreOfRelations +import com.anytypeio.anytype.domain.objects.getValidRelations import com.anytypeio.anytype.domain.resources.StringResourceProvider import javax.inject.Inject import kotlin.collections.contains +import kotlin.collections.plus interface FieldParser { fun toDate(any: Any?): Field.Date? @@ -31,11 +35,29 @@ interface FieldParser { actionSuccess: suspend (ObjectWrapper.Basic) -> Unit, actionFailure: suspend (Throwable) -> Unit ) + fun getObjectName(objectWrapper: ObjectWrapper.Basic): String + fun getObjectName(objectWrapper: ObjectWrapper.Type): String fun getObjectTypeIdAndName( objectWrapper: ObjectWrapper.Basic, types: List ): Pair + + suspend fun getObjectParsedFields( + objectType: ObjectWrapper.Type, + objFieldKeys: List, + storeOfRelations: StoreOfRelations + ): ParsedFields + + suspend fun getObjectTypeParsedFields( + objectType: ObjectWrapper.Type, + objectTypeConflictingFieldsIds: List, + storeOfRelations: StoreOfRelations + ): ParsedFields + + fun isFieldEditable(relation: ObjectWrapper.Relation): Boolean + + fun isFieldCanBeDeletedFromType(field: ObjectWrapper.Relation): Boolean } class FieldParserImpl @Inject constructor( @@ -118,13 +140,16 @@ class FieldParserImpl @Inject constructor( val result = when (objectWrapper.layout) { ObjectType.Layout.DATE -> { val relativeDate = dateProvider.calculateRelativeDates( - dateInSeconds = objectWrapper.getSingleValue(Relations.TIMESTAMP)?.toLong() + dateInSeconds = objectWrapper.getSingleValue(Relations.TIMESTAMP) + ?.toLong() ) stringResourceProvider.getRelativeDateName(relativeDate) } + ObjectType.Layout.NOTE -> { objectWrapper.snippet?.replace("\n", " ")?.take(MAX_SNIPPET_SIZE) } + in SupportedLayouts.fileLayouts -> { val fileName = if (objectWrapper.name.isNullOrBlank()) { stringResourceProvider.getUntitledObjectTitle() @@ -137,6 +162,7 @@ class FieldParserImpl @Inject constructor( else -> "$fileName.${objectWrapper.fileExt}" } } + else -> { objectWrapper.name } @@ -148,6 +174,15 @@ class FieldParserImpl @Inject constructor( } } + override fun getObjectName(objectWrapper: ObjectWrapper.Type): String { + val name = objectWrapper.name + return if (name.isNullOrBlank()) { + stringResourceProvider.getUntitledObjectTitle() + } else { + name + } + } + override fun getObjectTypeIdAndName( objectWrapper: ObjectWrapper.Basic, types: List @@ -164,4 +199,99 @@ class FieldParserImpl @Inject constructor( } } //endregion + + //region Parsed fields + + // Consolidated function to build ParsedFields. + private suspend fun getParsedFields( + objType: ObjectWrapper.Type, + localFieldIds: Collection, + storeOfRelations: StoreOfRelations + ): ParsedFields { + + val headerFields = storeOfRelations.getValidRelations( + ids = objType.recommendedFeaturedRelations + ) + val sidebarFields = storeOfRelations.getValidRelations( + ids = objType.recommendedRelations + ) + val hiddenFields = storeOfRelations.getValidRelations( + ids = objType.recommendedHiddenRelations + ) + val fileFields = storeOfRelations.getValidRelations( + ids = objType.recommendedFileRelations + ) + + // Combine IDs from all recommended relations. + val existingIds = (headerFields + sidebarFields + hiddenFields + fileFields) + .map { it.id } + .toSet() + + // Filter out fields already present in the recommended groups. + val allLocalFields = storeOfRelations.getValidRelations( + ids = localFieldIds + .filter { it !in existingIds } + .toList() + ) + + // Partition local fields into system and non-system fields. + val (localSystemFields, localFieldsWithoutSystem) = allLocalFields.partition { + Relations.systemRelationKeys.contains(it.key) + } + + return ParsedFields( + header = headerFields, + sidebar = sidebarFields, + hidden = hiddenFields, + localWithoutSystem = localFieldsWithoutSystem, + localSystem = localSystemFields, + file = fileFields + ) + } + + override suspend fun getObjectParsedFields( + objectType: ObjectWrapper.Type, + objFieldKeys: List, + storeOfRelations: StoreOfRelations + ): ParsedFields { + val localFieldIds = storeOfRelations.getByKeys( + keys = objFieldKeys + ).mapNotNull { + if (it.isValidToUse) { + it.id + } else { + null + } + } + return getParsedFields( + objType = objectType, + localFieldIds = localFieldIds, + storeOfRelations = storeOfRelations + ) + } + + override suspend fun getObjectTypeParsedFields( + objectType: ObjectWrapper.Type, + objectTypeConflictingFieldsIds: List, + storeOfRelations: StoreOfRelations + ): ParsedFields { + return getParsedFields( + objType = objectType, + localFieldIds = objectTypeConflictingFieldsIds, + storeOfRelations = storeOfRelations + ) + } + + override fun isFieldEditable(relation: ObjectWrapper.Relation): Boolean { + return !(relation.isReadOnly == true || + relation.isHidden == true || + relation.isArchived == true || + relation.isDeleted == true || + Relations.systemRelationKeys.contains(relation.key)) + } + + override fun isFieldCanBeDeletedFromType(field: ObjectWrapper.Relation): Boolean { + return !(field.isHidden == true || Relations.systemRelationKeys.contains(field.key)) + } + //endregion } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/primitives/GetObjectTypeConflictingFields.kt b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/GetObjectTypeConflictingFields.kt new file mode 100644 index 0000000000..8e8acd964e --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/GetObjectTypeConflictingFields.kt @@ -0,0 +1,27 @@ +package com.anytypeio.anytype.domain.primitives + +import com.anytypeio.anytype.core_models.Command.ObjectTypeConflictingFields +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import javax.inject.Inject + +class GetObjectTypeConflictingFields @Inject constructor( + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor>(dispatchers.io) { + + override suspend fun doWork(params: Params): List { + val command = ObjectTypeConflictingFields( + spaceId = params.spaceId, + objectTypeId = params.objectTypeId + ) + return repo.objectTypeListConflictingRelations(command = command) + } + + data class Params( + val spaceId: String, + val objectTypeId: String + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/primitives/SetObjectTypeHeaderRecommendedFields.kt b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/SetObjectTypeHeaderRecommendedFields.kt new file mode 100644 index 0000000000..ef1800a7f4 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/SetObjectTypeHeaderRecommendedFields.kt @@ -0,0 +1,28 @@ +package com.anytypeio.anytype.domain.primitives + +import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields.Params +import javax.inject.Inject + +class SetObjectTypeHeaderRecommendedFields @Inject constructor( + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { + + override suspend fun doWork(params: Params) { + val command = Command.ObjectTypeSetRecommendedHeaderFields( + objectTypeId = params.objectTypeId, + fields = params.fields + ) + return repo.objectTypeSetRecommendedHeaderFields(command = command) + } + + data class Params( + val objectTypeId: String, + val fields: List + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/primitives/SetObjectTypeRecommendedFields.kt b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/SetObjectTypeRecommendedFields.kt new file mode 100644 index 0000000000..da6642892f --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/primitives/SetObjectTypeRecommendedFields.kt @@ -0,0 +1,29 @@ +package com.anytypeio.anytype.domain.primitives + +import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Command.ObjectTypeConflictingFields +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields.Params +import javax.inject.Inject + +class SetObjectTypeRecommendedFields @Inject constructor( + private val repo: BlockRepository, + dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { + + override suspend fun doWork(params: Params) { + val command = Command.ObjectTypeSetRecommendedFields( + objectTypeId = params.objectTypeId, + fields = params.fields + ) + return repo.objectTypeSetRecommendedFields(command = command) + } + + data class Params( + val objectTypeId: String, + val fields: List + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt b/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt index 9f32a72620..c1c772f9db 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt @@ -6,4 +6,5 @@ interface StringResourceProvider { fun getRelativeDateName(relativeDate: RelativeDate): String fun getDeletedObjectTitle(): String fun getUntitledObjectTitle(): String + fun getSetOfObjectsTitle(): String } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt b/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt index 7481e50b6a..b610efecf7 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt @@ -114,8 +114,14 @@ class ObjectTypesSubscriptionManager ( Relations.DEFAULT_TEMPLATE_ID, Relations.SPACE_ID, Relations.UNIQUE_KEY, - Relations.RESTRICTIONS - ), + Relations.RESTRICTIONS, + Relations.TARGET_SPACE_ID, + Relations.TYPE, + Relations.RECOMMENDED_RELATIONS, + Relations.RECOMMENDED_FEATURED_RELATIONS, + Relations.RECOMMENDED_HIDDEN_RELATIONS, + Relations.RECOMMENDED_FILE_RELATIONS, + ), ignoreWorkspace = true ) } diff --git a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/models/AllContentModels.kt b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/models/AllContentModels.kt index 4d4f390644..0ba74c6a60 100644 --- a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/models/AllContentModels.kt +++ b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/models/AllContentModels.kt @@ -12,7 +12,6 @@ import com.anytypeio.anytype.core_models.RelationFormat import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.Relations.SOURCE_OBJECT import com.anytypeio.anytype.core_models.ext.DateParser -import com.anytypeio.anytype.core_models.primitives.RelationKey import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction import com.anytypeio.anytype.domain.all_content.RestoreAllContentState @@ -20,6 +19,8 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.primitives.FieldParser import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel.Companion.DEFAULT_INITIAL_TAB import com.anytypeio.anytype.presentation.mapper.objectIcon +import com.anytypeio.anytype.presentation.objects.MenuSortsItem +import com.anytypeio.anytype.presentation.objects.ObjectsListSort import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.objects.getDescriptionOrSnippet import com.anytypeio.anytype.presentation.objects.getProperType @@ -41,41 +42,6 @@ sealed class AllContentMenuMode { override val isSelected: Boolean = false ) : AllContentMenuMode() } - -sealed class AllContentSort { - abstract val relationKey: RelationKey - abstract val sortType: DVSortType - abstract val canGroupByDate: Boolean - abstract val isSelected: Boolean - - data class ByName( - override val relationKey: RelationKey = RelationKey(Relations.NAME), - override val sortType: DVSortType = DVSortType.ASC, - override val canGroupByDate: Boolean = false, - override val isSelected: Boolean = false - ) : AllContentSort() - - data class ByDateUpdated( - override val relationKey: RelationKey = RelationKey(Relations.LAST_MODIFIED_DATE), - override val sortType: DVSortType = DVSortType.DESC, - override val canGroupByDate: Boolean = true, - override val isSelected: Boolean = false - ) : AllContentSort() - - data class ByDateCreated( - override val relationKey: RelationKey = RelationKey(Relations.CREATED_DATE), - override val sortType: DVSortType = DVSortType.DESC, - override val canGroupByDate: Boolean = true, - override val isSelected: Boolean = false - ) : AllContentSort() - - data class ByDateUsed( - override val relationKey: RelationKey = RelationKey(Relations.LAST_USED_DATE), - override val sortType: DVSortType = DVSortType.DESC, - override val canGroupByDate: Boolean = false, - override val isSelected: Boolean = false - ) : AllContentSort() -} //endregion //region VIEW STATES @@ -190,18 +156,6 @@ sealed class UiMenuState { val showBin: Boolean = true ) : UiMenuState() } - - -sealed class MenuSortsItem { - data class Container(val sort: AllContentSort) : MenuSortsItem() - data class Sort(val sort: AllContentSort) : MenuSortsItem() - data object Spacer : MenuSortsItem() - data class SortType( - val sort: AllContentSort, - val sortType: DVSortType, - val isSelected: Boolean - ) : MenuSortsItem() -} //endregion //region BOTTOM_MENU @@ -217,14 +171,14 @@ sealed class UiSnackbarState { //region MAPPING -fun RestoreAllContentState.Response.Success.mapToSort(): AllContentSort { +fun RestoreAllContentState.Response.Success.mapToSort(): ObjectsListSort { val sortType = if (isAsc) DVSortType.ASC else DVSortType.DESC return when (activeSort) { - Relations.CREATED_DATE -> AllContentSort.ByDateCreated(sortType = sortType) - Relations.LAST_MODIFIED_DATE -> AllContentSort.ByDateUpdated(sortType = sortType) - Relations.NAME -> AllContentSort.ByName(sortType = sortType) - Relations.LAST_USED_DATE -> AllContentSort.ByDateUsed(sortType = sortType) - else -> AllContentSort.ByName(sortType = DVSortType.ASC) + Relations.CREATED_DATE -> ObjectsListSort.ByDateCreated(sortType = sortType) + Relations.LAST_MODIFIED_DATE -> ObjectsListSort.ByDateUpdated(sortType = sortType) + Relations.NAME -> ObjectsListSort.ByName(sortType = sortType) + Relations.LAST_USED_DATE -> ObjectsListSort.ByDateUsed(sortType = sortType) + else -> ObjectsListSort.ByName(sortType = DVSortType.ASC) } } @@ -302,12 +256,12 @@ fun ObjectWrapper.Basic.toAllContentRelation( ) } -fun AllContentSort.toAnalyticsSortType(): Pair { +fun ObjectsListSort.toAnalyticsSortType(): Pair { return when (this) { - is AllContentSort.ByName -> "Name" to sortType.toAnalyticsSortType() - is AllContentSort.ByDateUpdated -> "Updated" to sortType.toAnalyticsSortType() - is AllContentSort.ByDateCreated -> "Created" to sortType.toAnalyticsSortType() - is AllContentSort.ByDateUsed -> "Used" to sortType.toAnalyticsSortType() + is ObjectsListSort.ByName -> "Name" to sortType.toAnalyticsSortType() + is ObjectsListSort.ByDateUpdated -> "Updated" to sortType.toAnalyticsSortType() + is ObjectsListSort.ByDateCreated -> "Created" to sortType.toAnalyticsSortType() + is ObjectsListSort.ByDateUsed -> "Used" to sortType.toAnalyticsSortType() } } diff --git a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/models/AllContentSearchParams.kt b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/models/AllContentSearchParams.kt index fa2d80beff..33f8f1bd71 100644 --- a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/models/AllContentSearchParams.kt +++ b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/models/AllContentSearchParams.kt @@ -7,10 +7,11 @@ import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectTypeIds import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys -import com.anytypeio.anytype.core_models.RelationFormat import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.domain.library.StoreSearchParams +import com.anytypeio.anytype.presentation.objects.ObjectsListSort +import com.anytypeio.anytype.presentation.objects.toDVSort import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeysObjectType import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultRelationKeys @@ -45,7 +46,7 @@ fun createSubscriptionParams( spaceId: Id, activeMode: UiTitleState, activeTab: AllContentTab, - activeSort: AllContentSort, + activeSort: ObjectsListSort, limitedObjectIds: List, limit: Int, subscriptionId: String @@ -73,7 +74,7 @@ fun createSubscriptionParams( fun AllContentTab.filtersForSubscribe( spaces: List, - activeSort: AllContentSort, + activeSort: ObjectsListSort, limitedObjectIds: List, activeMode: UiTitleState ): Pair, List> { @@ -240,36 +241,4 @@ private fun buildDeletedFilter(): List { value = true ) ) -} - -fun AllContentSort.toDVSort(): DVSort { - return when (this) { - is AllContentSort.ByDateCreated -> DVSort( - relationKey = relationKey.key, - type = sortType, - relationFormat = RelationFormat.DATE, - includeTime = true, - ) - - is AllContentSort.ByDateUpdated -> DVSort( - relationKey = relationKey.key, - type = sortType, - relationFormat = RelationFormat.DATE, - includeTime = true, - ) - - is AllContentSort.ByName -> DVSort( - relationKey = relationKey.key, - type = sortType, - relationFormat = RelationFormat.LONG_TEXT, - includeTime = false - ) - - is AllContentSort.ByDateUsed -> DVSort( - relationKey = relationKey.key, - type = sortType, - relationFormat = RelationFormat.DATE, - includeTime = true, - ) - } } \ 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 4f9660b38a..fe7f40dda9 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 @@ -30,9 +30,7 @@ import com.anytypeio.anytype.domain.search.SearchObjects import com.anytypeio.anytype.domain.workspace.RemoveObjectsFromWorkspace import com.anytypeio.anytype.feature_allcontent.models.AllContentBottomMenu import com.anytypeio.anytype.feature_allcontent.models.AllContentMenuMode -import com.anytypeio.anytype.feature_allcontent.models.AllContentSort import com.anytypeio.anytype.feature_allcontent.models.AllContentTab -import com.anytypeio.anytype.feature_allcontent.models.MenuSortsItem import com.anytypeio.anytype.feature_allcontent.models.UiContentItem import com.anytypeio.anytype.feature_allcontent.models.UiContentState import com.anytypeio.anytype.feature_allcontent.models.UiItemsState @@ -63,6 +61,8 @@ import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEve import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.home.navigation import com.anytypeio.anytype.presentation.navigation.NavPanelState +import com.anytypeio.anytype.presentation.objects.MenuSortsItem +import com.anytypeio.anytype.presentation.objects.ObjectsListSort import com.anytypeio.anytype.presentation.objects.getCreateObjectParams import java.time.Instant import java.time.LocalDate @@ -116,7 +116,7 @@ class AllContentViewModel( ) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate { private val searchResultIds = MutableStateFlow>(emptyList()) - private val sortState = MutableStateFlow(AllContentSort.ByName()) + private val sortState = MutableStateFlow(ObjectsListSort.ByName()) val uiTitleState = MutableStateFlow(DEFAULT_INITIAL_MODE) val uiTabsState = MutableStateFlow(UiTabsState()) val uiMenuState = MutableStateFlow(UiMenuState.Hidden) @@ -279,7 +279,7 @@ class AllContentViewModel( private suspend fun handleData( objWrappers: List, - activeSort: AllContentSort, + activeSort: ObjectsListSort, activeTab: AllContentTab ): List { @@ -310,7 +310,7 @@ class AllContentViewModel( private suspend fun mapToUiContentItems( objectWrappers: List, - activeSort: AllContentSort, + activeSort: ObjectsListSort, activeTab: AllContentTab ): List { val isOwnerOrEditor = permission.value?.isOwnerOrEditor() == true @@ -345,19 +345,19 @@ class AllContentViewModel( ) } val result = when (activeSort) { - is AllContentSort.ByDateCreated -> { + is ObjectsListSort.ByDateCreated -> { groupItemsByDate(items = items, isSortByDateCreated = true, activeSort = activeSort) } - is AllContentSort.ByDateUpdated -> { + is ObjectsListSort.ByDateUpdated -> { groupItemsByDate(items = items, isSortByDateCreated = false, activeSort = activeSort) } - is AllContentSort.ByName -> { + is ObjectsListSort.ByName -> { items } - is AllContentSort.ByDateUsed -> { + is ObjectsListSort.ByDateUsed -> { items } } @@ -376,7 +376,7 @@ class AllContentViewModel( private fun groupItemsByDate( items: List, isSortByDateCreated: Boolean, - activeSort: AllContentSort + activeSort: ObjectsListSort ): List { val groupedItems = mutableListOf() @@ -502,35 +502,35 @@ class AllContentViewModel( } } - fun AllContentTab.sorts(activeSort: AllContentSort): List { + fun AllContentTab.sorts(activeSort: ObjectsListSort): List { return when (this) { AllContentTab.TYPES -> { listOf( MenuSortsItem.Sort( - sort = AllContentSort.ByName(isSelected = activeSort is AllContentSort.ByName) + sort = ObjectsListSort.ByName(isSelected = activeSort is ObjectsListSort.ByName) ), MenuSortsItem.Sort( - sort = AllContentSort.ByDateUsed(isSelected = activeSort is AllContentSort.ByDateUsed) + sort = ObjectsListSort.ByDateUsed(isSelected = activeSort is ObjectsListSort.ByDateUsed) ) ) } AllContentTab.RELATIONS -> { listOf( MenuSortsItem.Sort( - sort = AllContentSort.ByName(isSelected = activeSort is AllContentSort.ByName) + sort = ObjectsListSort.ByName(isSelected = activeSort is ObjectsListSort.ByName) ) ) } else -> { listOf( MenuSortsItem.Sort( - sort = AllContentSort.ByDateUpdated(isSelected = activeSort is AllContentSort.ByDateUpdated) + sort = ObjectsListSort.ByDateUpdated(isSelected = activeSort is ObjectsListSort.ByDateUpdated) ), MenuSortsItem.Sort( - sort = AllContentSort.ByDateCreated(isSelected = activeSort is AllContentSort.ByDateCreated) + sort = ObjectsListSort.ByDateCreated(isSelected = activeSort is ObjectsListSort.ByDateCreated) ), MenuSortsItem.Sort( - sort = AllContentSort.ByName(isSelected = activeSort is AllContentSort.ByName) + sort = ObjectsListSort.ByName(isSelected = activeSort is ObjectsListSort.ByName) ) ) } @@ -552,12 +552,12 @@ class AllContentViewModel( private fun AllContentTab.updateInitialState() { return when (this) { AllContentTab.TYPES -> { - sortState.value = AllContentSort.ByName() + sortState.value = ObjectsListSort.ByName() userInput.value = DEFAULT_QUERY uiTitleState.value = UiTitleState.AllContent } AllContentTab.RELATIONS -> { - sortState.value = AllContentSort.ByName() + sortState.value = ObjectsListSort.ByName() userInput.value = DEFAULT_QUERY uiTitleState.value = UiTitleState.AllContent } @@ -598,19 +598,19 @@ class AllContentViewModel( } } - fun onSortClicked(sort: AllContentSort) { + fun onSortClicked(sort: ObjectsListSort) { Timber.d("onSortClicked: $sort") val newSort = when (sort) { - is AllContentSort.ByDateCreated -> { + is ObjectsListSort.ByDateCreated -> { sort.copy(isSelected = true) } - is AllContentSort.ByDateUpdated -> { + is ObjectsListSort.ByDateUpdated -> { sort.copy(isSelected = true) } - is AllContentSort.ByName -> { + is ObjectsListSort.ByName -> { sort.copy(isSelected = true) } - is AllContentSort.ByDateUsed -> { + is ObjectsListSort.ByDateUsed -> { sort.copy(isSelected = true) } } @@ -628,7 +628,7 @@ class AllContentViewModel( } } - private fun proceedWithSortSaving(activeTab: UiTabsState, sort: AllContentSort) { + private fun proceedWithSortSaving(activeTab: UiTabsState, sort: ObjectsListSort) { if (activeTab.selectedTab == AllContentTab.TYPES || activeTab.selectedTab == AllContentTab.RELATIONS ) { diff --git a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentMenu.kt b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentMenu.kt index 0b96d0adc9..4097a815a6 100644 --- a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentMenu.kt +++ b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentMenu.kt @@ -1,109 +1,77 @@ package com.anytypeio.anytype.feature_allcontent.ui -import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.graphics.ColorFilter.Companion.tint import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.DVSortType import com.anytypeio.anytype.core_ui.common.DefaultPreviews -import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular -import com.anytypeio.anytype.core_ui.views.UXBody +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.lists.objects.menu.ObjectsListMenuItem +import com.anytypeio.anytype.core_ui.lists.objects.menu.ObjectsListSortingMenuContainer import com.anytypeio.anytype.feature_allcontent.R import com.anytypeio.anytype.feature_allcontent.models.AllContentMenuMode -import com.anytypeio.anytype.feature_allcontent.models.AllContentSort -import com.anytypeio.anytype.feature_allcontent.models.MenuSortsItem import com.anytypeio.anytype.feature_allcontent.models.UiMenuState +import com.anytypeio.anytype.presentation.objects.MenuSortsItem +import com.anytypeio.anytype.presentation.objects.ObjectsListSort @Composable fun AllContentMenu( uiMenuState: UiMenuState.Visible, onModeClick: (AllContentMenuMode) -> Unit, - onSortClick: (AllContentSort) -> Unit, + onSortClick: (ObjectsListSort) -> Unit, onBinClick: () -> Unit ) { var sortingExpanded by remember { mutableStateOf(false) } uiMenuState.mode.forEach { item -> - MenuItem( + ObjectsListMenuItem( title = getModeTitle(item), isSelected = item.isSelected, modifier = Modifier.clickable { onModeClick(item) } ) - Divider(0.5.dp) + Divider( + height = 0.5.dp, + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_secondary) + ) } + if (uiMenuState.mode.isNotEmpty()) { - Divider(7.5.dp) + Divider( + height = 7.5.dp, + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_secondary) + ) } - SortingBox( - modifier = Modifier - .clickable { - sortingExpanded = !sortingExpanded - }, - subtitle = uiMenuState.container.sort.title(), - isExpanded = sortingExpanded + + ObjectsListSortingMenuContainer( + container = uiMenuState.container, + sorts = uiMenuState.sorts, + types = uiMenuState.types, + sortingExpanded = sortingExpanded, + onSortClick = onSortClick, + onChangeSortExpandedState = { sortingExpanded = it } ) - Divider(0.5.dp) - if (sortingExpanded) { - uiMenuState.sorts.forEach { item -> - MenuItem( - title = item.sort.title(), - isSelected = item.sort.isSelected, - modifier = Modifier - .clickable { - onSortClick(item.sort) - } - ) - Divider(0.5.dp) - } - Divider(7.5.dp) - uiMenuState.types.forEachIndexed { index, item -> - MenuItem( - title = item.sortType.title(item.sort), - isSelected = item.isSelected, - modifier = Modifier - .clickable { - val updatedSort = when (item.sort) { - is AllContentSort.ByName -> item.sort.copy(sortType = item.sortType) - is AllContentSort.ByDateCreated -> item.sort.copy(sortType = item.sortType) - is AllContentSort.ByDateUpdated -> item.sort.copy(sortType = item.sortType) - is AllContentSort.ByDateUsed -> item.sort.copy(sortType = item.sortType) - } - onSortClick(updatedSort) - } - ) - if (index < uiMenuState.types.size - 1) { - Divider(0.5.dp) - } - } - } + if (uiMenuState.showBin && !sortingExpanded) { - Divider(7.5.dp) - MenuItem( + Divider( + height = 7.5.dp, + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_secondary) + ) + ObjectsListMenuItem( title = stringResource(id = R.string.all_content_view_bin), isSelected = false, modifier = Modifier.clickable { onBinClick() } @@ -111,83 +79,6 @@ fun AllContentMenu( } } -@Composable -private fun Divider(height: Dp) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(height) - .background(colorResource(id = R.color.shape_secondary)) - ) -} - -@Composable -private fun SortingBox(modifier: Modifier, subtitle: String, isExpanded: Boolean) { - val rotationAngle = if (isExpanded) 90f else 0f - Row( - modifier = modifier - .fillMaxWidth() - .background(colorResource(id = R.color.background_secondary)), - verticalAlignment = CenterVertically - ) { - Image( - modifier = Modifier - .padding(start = 10.dp) - .size(18.dp) - .rotate(rotationAngle), - painter = painterResource(R.drawable.ic_arrow_disclosure_18), - contentDescription = "", - colorFilter = tint(colorResource(id = R.color.glyph_selected)) - ) - Column( - modifier = Modifier - .wrapContentHeight() - .padding(top = 11.dp, bottom = 10.dp, start = 6.dp) - ) { - Text( - text = stringResource(id = R.string.all_content_sort_by), - modifier = Modifier.wrapContentSize(), - style = UXBody, - color = colorResource(id = R.color.text_primary) - ) - Text( - text = subtitle, - modifier = Modifier.wrapContentSize(), - style = BodyCalloutRegular, - color = colorResource(id = R.color.text_secondary) - ) - } - } -} - -@Composable -private fun MenuItem(modifier: Modifier, title: String, isSelected: Boolean) { - Row( - modifier = modifier - .fillMaxWidth() - .height(44.dp) - .background(colorResource(id = R.color.background_secondary)), - verticalAlignment = CenterVertically, - ) { - Image( - modifier = Modifier - .wrapContentSize() - .padding(start = 12.dp), - painter = painterResource(R.drawable.ic_check_16), - contentDescription = "All Content mode selected", - alpha = if (isSelected) 1f else 0f - ) - Text( - text = title, - modifier = Modifier - .wrapContentSize() - .padding(start = 8.dp), - style = UXBody, - color = colorResource(id = R.color.text_primary) - ) - } -} - //region RESOURCES @Composable private fun getModeTitle(mode: AllContentMenuMode): String = stringResource( @@ -196,43 +87,6 @@ private fun getModeTitle(mode: AllContentMenuMode): String = stringResource( is AllContentMenuMode.Unlinked -> R.string.all_content_title_only_unlinked } ) - -@Composable -private fun AllContentSort.title(): String = stringResource( - when (this) { - is AllContentSort.ByDateCreated -> R.string.all_content_sort_date_created - is AllContentSort.ByDateUpdated -> R.string.all_content_sort_date_updated - is AllContentSort.ByName -> R.string.all_content_sort_name - is AllContentSort.ByDateUsed -> R.string.all_content_sort_date_used - } -) - -@Composable -private fun DVSortType.title(sort: AllContentSort): String = when (this) { - DVSortType.ASC -> { - when (sort) { - is AllContentSort.ByDateCreated, is AllContentSort.ByDateUpdated, is AllContentSort.ByDateUsed -> stringResource( - id = R.string.all_content_sort_date_asc - ) - - is AllContentSort.ByName -> stringResource(id = R.string.all_content_sort_name_asc) - } - } - - DVSortType.DESC -> { - when (sort) { - is AllContentSort.ByDateCreated, - is AllContentSort.ByDateUpdated, - is AllContentSort.ByDateUsed -> stringResource( - id = R.string.all_content_sort_date_desc - ) - - is AllContentSort.ByName -> stringResource(id = R.string.all_content_sort_name_desc) - } - } - - DVSortType.CUSTOM -> "" -} //endregion //region PREVIEW @@ -247,28 +101,28 @@ fun AllContentMenuPreview() { ), sorts = listOf( MenuSortsItem.Sort( - sort = AllContentSort.ByName(isSelected = true) + sort = ObjectsListSort.ByName(isSelected = true) ), MenuSortsItem.Sort( - AllContentSort.ByDateUpdated(isSelected = false) + ObjectsListSort.ByDateUpdated(isSelected = false) ), MenuSortsItem.Sort( - AllContentSort.ByDateCreated(isSelected = false) + ObjectsListSort.ByDateCreated(isSelected = false) ) ), types = listOf( MenuSortsItem.SortType( sortType = DVSortType.ASC, isSelected = true, - sort = AllContentSort.ByName(isSelected = true) + sort = ObjectsListSort.ByName(isSelected = true) ), MenuSortsItem.SortType( sortType = DVSortType.DESC, isSelected = false, - sort = AllContentSort.ByName(isSelected = false) + sort = ObjectsListSort.ByName(isSelected = false) ), ), - container = MenuSortsItem.Container(AllContentSort.ByName()) + container = MenuSortsItem.Container(ObjectsListSort.ByName()) ), onModeClick = {}, onSortClick = {}, diff --git a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentScreen.kt b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentScreen.kt index d8219a4795..4ec69e7bb3 100644 --- a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentScreen.kt +++ b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentScreen.kt @@ -70,6 +70,7 @@ import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.extensions.simpleIcon import com.anytypeio.anytype.core_ui.extensions.swapList +import com.anytypeio.anytype.core_ui.foundation.DefaultSearchBar import com.anytypeio.anytype.core_ui.foundation.DismissBackground import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.components.BottomNavigationMenu @@ -90,7 +91,6 @@ import com.anytypeio.anytype.feature_allcontent.BuildConfig import com.anytypeio.anytype.feature_allcontent.R import com.anytypeio.anytype.feature_allcontent.models.AllContentBottomMenu import com.anytypeio.anytype.feature_allcontent.models.AllContentMenuMode -import com.anytypeio.anytype.feature_allcontent.models.AllContentSort import com.anytypeio.anytype.feature_allcontent.models.AllContentTab import com.anytypeio.anytype.feature_allcontent.models.UiContentItem import com.anytypeio.anytype.feature_allcontent.models.UiContentState @@ -99,6 +99,7 @@ import com.anytypeio.anytype.feature_allcontent.models.UiMenuState import com.anytypeio.anytype.feature_allcontent.models.UiSnackbarState import com.anytypeio.anytype.feature_allcontent.models.UiTabsState import com.anytypeio.anytype.feature_allcontent.models.UiTitleState +import com.anytypeio.anytype.presentation.objects.ObjectsListSort import com.anytypeio.anytype.presentation.navigation.NavPanelState import com.anytypeio.anytype.presentation.objects.ObjectIcon import kotlinx.coroutines.CoroutineScope @@ -117,7 +118,7 @@ fun AllContentWrapperScreen( onTabClick: (AllContentTab) -> Unit, onQueryChanged: (String) -> Unit, onModeClick: (AllContentMenuMode) -> Unit, - onSortClick: (AllContentSort) -> Unit, + onSortClick: (ObjectsListSort) -> Unit, onItemClicked: (UiContentItem.Item) -> Unit, onTypeClicked: (UiContentItem) -> Unit, onRelationClicked: (UiContentItem) -> Unit, @@ -180,7 +181,7 @@ fun AllContentMainScreen( onTabClick: (AllContentTab) -> Unit, onQueryChanged: (String) -> Unit, onModeClick: (AllContentMenuMode) -> Unit, - onSortClick: (AllContentSort) -> Unit, + onSortClick: (ObjectsListSort) -> Unit, onItemClicked: (UiContentItem.Item) -> Unit, onTypeClicked: (UiContentItem) -> Unit, onRelationClicked: (UiContentItem) -> Unit, @@ -267,10 +268,14 @@ fun AllContentMainScreen( onTabClick(tab) } Spacer(modifier = Modifier.size(10.dp)) - AllContentSearchBar(onQueryChanged = { - isSearchEmpty = it.isEmpty() - onQueryChanged(it) - }) + DefaultSearchBar( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + onQueryChanged = { + isSearchEmpty = it.isEmpty() + onQueryChanged(it) + }) Spacer(modifier = Modifier.size(10.dp)) Divider(paddingStart = 0.dp, paddingEnd = 0.dp) } diff --git a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentTopToolbar.kt b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentTopToolbar.kt index afb967f74f..c19c208f71 100644 --- a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentTopToolbar.kt +++ b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/ui/AllContentTopToolbar.kt @@ -1,17 +1,9 @@ package com.anytypeio.anytype.feature_allcontent.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -22,54 +14,38 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.selection.LocalTextSelectionColors -import androidx.compose.foundation.text.selection.TextSelectionColors -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.IconButton -import androidx.compose.material.TextFieldDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.DVSortType import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.extensions.bouncingClickable import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable -import com.anytypeio.anytype.core_ui.views.BodyRegular import com.anytypeio.anytype.core_ui.views.Title1 import com.anytypeio.anytype.core_ui.views.Title2 import com.anytypeio.anytype.feature_allcontent.R import com.anytypeio.anytype.feature_allcontent.models.AllContentMenuMode -import com.anytypeio.anytype.feature_allcontent.models.AllContentSort import com.anytypeio.anytype.feature_allcontent.models.AllContentTab -import com.anytypeio.anytype.feature_allcontent.models.MenuSortsItem import com.anytypeio.anytype.feature_allcontent.models.UiMenuState import com.anytypeio.anytype.feature_allcontent.models.UiTabsState import com.anytypeio.anytype.feature_allcontent.models.UiTitleState +import com.anytypeio.anytype.presentation.objects.MenuSortsItem +import com.anytypeio.anytype.presentation.objects.ObjectsListSort //region AllContentTopBarContainer @OptIn(ExperimentalMaterial3Api::class) @@ -78,7 +54,7 @@ fun AllContentTopBarContainer( titleState: UiTitleState, uiMenuState: UiMenuState, onModeClick: (AllContentMenuMode) -> Unit, - onSortClick: (AllContentSort) -> Unit, + onSortClick: (ObjectsListSort) -> Unit, onBinClick: () -> Unit, onBackClick: () -> Unit ) { @@ -139,21 +115,21 @@ private fun AllContentTopBarContainerPreview() { AllContentMenuMode.Unlinked() ), container = MenuSortsItem.Container( - sort = AllContentSort.ByName(isSelected = true) + sort = ObjectsListSort.ByName(isSelected = true) ), sorts = listOf( MenuSortsItem.Sort( - sort = AllContentSort.ByName(isSelected = true) + sort = ObjectsListSort.ByName(isSelected = true) ), ), types = listOf( MenuSortsItem.SortType( - sort = AllContentSort.ByName(isSelected = true), + sort = ObjectsListSort.ByName(isSelected = true), sortType = DVSortType.DESC, isSelected = true ), MenuSortsItem.SortType( - sort = AllContentSort.ByDateCreated(isSelected = false), + sort = ObjectsListSort.ByDateCreated(isSelected = false), sortType = DVSortType.ASC, isSelected = false ), @@ -302,122 +278,4 @@ private fun AllContentTabsPreview() { ) } -//endregion - -//region SearchBar -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun AllContentSearchBar(onQueryChanged: (String) -> Unit) { - - val interactionSource = remember { MutableInteractionSource() } - val focus = LocalFocusManager.current - val focusRequester = FocusRequester() - - val selectionColors = TextSelectionColors( - backgroundColor = colorResource(id = R.color.cursor_color).copy( - alpha = 0.2f - ), - handleColor = colorResource(id = R.color.cursor_color), - ) - - var query by remember { mutableStateOf(TextFieldValue()) } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .background( - color = colorResource(id = R.color.shape_transparent), - shape = RoundedCornerShape(10.dp) - ) - .height(40.dp), - verticalAlignment = Alignment.CenterVertically - - ) { - Image( - painter = painterResource(id = R.drawable.ic_search_18), - contentDescription = "Search icon", - modifier = Modifier - .align(Alignment.CenterVertically) - .padding( - start = 11.dp - ) - ) - CompositionLocalProvider(value = LocalTextSelectionColors provides selectionColors) { - - BasicTextField( - value = query, - modifier = Modifier - .weight(1.0f) - .padding(start = 6.dp) - .align(Alignment.CenterVertically) - .focusRequester(focusRequester), - textStyle = BodyRegular.copy( - color = colorResource(id = R.color.text_primary) - ), - onValueChange = { input -> - query = input.also { - onQueryChanged(input.text) - } - }, - singleLine = true, - maxLines = 1, - keyboardActions = KeyboardActions( - onDone = { - focus.clearFocus(true) - } - ), - decorationBox = @Composable { innerTextField -> - TextFieldDefaults.OutlinedTextFieldDecorationBox( - value = query.text, - innerTextField = innerTextField, - enabled = true, - singleLine = true, - visualTransformation = VisualTransformation.None, - interactionSource = interactionSource, - placeholder = { - Text( - text = stringResource(id = R.string.search), - style = BodyRegular.copy( - color = colorResource(id = R.color.text_tertiary) - ) - ) - }, - colors = TextFieldDefaults.textFieldColors( - backgroundColor = Color.Transparent, - cursorColor = colorResource(id = R.color.cursor_color), - ), - border = {}, - contentPadding = PaddingValues() - ) - }, - cursorBrush = SolidColor(colorResource(id = R.color.palette_system_blue)), - ) - } - Spacer(Modifier.width(9.dp)) - AnimatedVisibility( - visible = query.text.isNotEmpty(), - enter = fadeIn(tween(100)), - exit = fadeOut(tween(100)) - ) { - Image( - painter = painterResource(id = R.drawable.ic_clear_18), - contentDescription = "Clear icon", - modifier = Modifier - .padding(end = 9.dp) - .noRippleClickable { - query = TextFieldValue().also { - onQueryChanged("") - } - } - ) - } - } -} - -@DefaultPreviews -@Composable -private fun AllContentSearchBarPreview() { - AllContentSearchBar() {} -} //endregion \ No newline at end of file diff --git a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/mapping/DateObjectModelsExt.kt b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/mapping/DateObjectModelsExt.kt index ec75305085..f1252de873 100644 --- a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/mapping/DateObjectModelsExt.kt +++ b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/mapping/DateObjectModelsExt.kt @@ -1,20 +1,9 @@ package com.anytypeio.anytype.feature_date.mapping -import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds -import com.anytypeio.anytype.core_models.ObjectType -import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys -import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.RelationListWithValueItem import com.anytypeio.anytype.core_models.Relations -import com.anytypeio.anytype.core_models.primitives.SpaceId -import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction -import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.objects.StoreOfRelations -import com.anytypeio.anytype.domain.primitives.FieldParser import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem -import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListItem -import com.anytypeio.anytype.presentation.mapper.objectIcon -import com.anytypeio.anytype.presentation.objects.getProperType import timber.log.Timber suspend fun List.toUiFieldsItem( @@ -52,33 +41,4 @@ suspend fun List.toUiFieldsItem( ) } } -} - -fun ObjectWrapper.Basic.toUiObjectsListItem( - space: SpaceId, - urlBuilder: UrlBuilder, - objectTypes: List, - fieldParser: FieldParser, - isOwnerOrEditor: Boolean -): UiObjectsListItem { - val obj = this - val typeUrl = obj.getProperType() - val isProfile = typeUrl == MarketplaceObjectTypeIds.PROFILE - val layout = obj.layout ?: ObjectType.Layout.BASIC - return UiObjectsListItem.Item( - id = obj.id, - space = space, - name = fieldParser.getObjectName(obj), - type = typeUrl, - typeName = objectTypes.firstOrNull { type -> - if (isProfile) { - type.uniqueKey == ObjectTypeUniqueKeys.PROFILE - } else { - type.id == typeUrl - } - }?.name, - layout = layout, - icon = obj.objectIcon(builder = urlBuilder), - isPossibleToDelete = isOwnerOrEditor && !restrictions.contains(ObjectRestriction.DELETE) - ) } \ No newline at end of file diff --git a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/MainScreen.kt b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/MainScreen.kt index 69f466dcb1..e368a3ac6f 100644 --- a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/MainScreen.kt +++ b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/MainScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource @@ -32,18 +31,19 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_ui.foundation.components.BottomNavigationMenu +import com.anytypeio.anytype.core_ui.lists.objects.PaginatedObjectList +import com.anytypeio.anytype.core_ui.lists.objects.UiContentState +import com.anytypeio.anytype.core_ui.lists.objects.UiObjectsListState import com.anytypeio.anytype.core_ui.syncstatus.SpaceSyncStatusScreen import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK import com.anytypeio.anytype.feature_date.R import com.anytypeio.anytype.feature_date.ui.models.DateEvent import com.anytypeio.anytype.feature_date.viewmodel.UiCalendarIconState import com.anytypeio.anytype.feature_date.viewmodel.UiCalendarState -import com.anytypeio.anytype.feature_date.viewmodel.UiContentState import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsSheetState import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsState import com.anytypeio.anytype.feature_date.viewmodel.UiHeaderState import com.anytypeio.anytype.feature_date.viewmodel.UiNavigationWidget -import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListState import com.anytypeio.anytype.feature_date.viewmodel.UiSnackbarState import com.anytypeio.anytype.feature_date.viewmodel.UiSyncStatusBadgeState import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState @@ -68,8 +68,6 @@ fun DateMainScreen( onDateEvent: (DateEvent) -> Unit ) { - val scope = rememberCoroutineScope() - val snackBarHostState = remember { SnackbarHostState() } val snackBarText = stringResource(R.string.all_content_snackbar_title) @@ -156,11 +154,19 @@ fun DateMainScreen( if (uiContentState is UiContentState.Empty) { EmptyScreen() } - ObjectsScreen( + PaginatedObjectList( state = uiObjectsListState, uiState = uiContentState, canPaginate = canPaginate, - onDateEvent = onDateEvent, + onLoadMore = { + DateEvent.ObjectsList.OnLoadMore + }, + onMoveToBin = { item -> + DateEvent.ObjectsList.OnObjectMoveToBin(item) + }, + onObjectClicked = { item -> + DateEvent.ObjectsList.OnObjectClicked(item) + } ) BottomNavigationMenu( modifier = Modifier diff --git a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/ObjectsScreen.kt b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/ObjectsScreen.kt deleted file mode 100644 index e0022ffb17..0000000000 --- a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/ObjectsScreen.kt +++ /dev/null @@ -1,337 +0,0 @@ -package com.anytypeio.anytype.feature_date.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.SwipeToDismissBox -import androidx.compose.material3.SwipeToDismissBoxValue -import androidx.compose.material3.rememberSwipeToDismissBoxState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.anytypeio.anytype.core_ui.common.DefaultPreviews -import com.anytypeio.anytype.core_ui.common.ShimmerEffect -import com.anytypeio.anytype.core_ui.extensions.swapList -import com.anytypeio.anytype.core_ui.foundation.DismissBackground -import com.anytypeio.anytype.core_ui.foundation.Divider -import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable -import com.anytypeio.anytype.core_ui.views.ButtonSize -import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular -import com.anytypeio.anytype.core_ui.views.Relations3 -import com.anytypeio.anytype.core_ui.views.animations.DotsLoadingIndicator -import com.anytypeio.anytype.core_ui.views.animations.FadeAnimationSpecs -import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon -import com.anytypeio.anytype.feature_date.R -import com.anytypeio.anytype.feature_date.ui.models.DateEvent -import com.anytypeio.anytype.feature_date.ui.models.StubVerticalItems -import com.anytypeio.anytype.feature_date.viewmodel.UiContentState -import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListItem -import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListState -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Composable -fun ObjectsScreen( - state: UiObjectsListState, - uiState: UiContentState, - canPaginate: Boolean, - onDateEvent: (DateEvent) -> Unit -) { - val items = remember { mutableStateListOf() } - items.swapList(state.items) - - val scope = rememberCoroutineScope() - - val lazyListState = rememberLazyListState() - - val canPaginateState = remember { mutableStateOf(false) } - LaunchedEffect(key1 = canPaginate) { - canPaginateState.value = canPaginate - } - - val shouldStartPaging = remember { - derivedStateOf { - canPaginateState.value && (lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index - ?: -9) >= (lazyListState.layoutInfo.totalItemsCount - 2) - } - } - - LaunchedEffect(key1 = shouldStartPaging.value) { - if (shouldStartPaging.value && uiState is UiContentState.Idle) { - onDateEvent(DateEvent.ObjectsList.OnLoadMore) - } - } - - LazyColumn( - modifier = Modifier - .padding(top = 8.dp) - .fillMaxSize(), - state = lazyListState - ) { - items( - count = items.size, - key = { index -> items[index].id }, - contentType = { index -> - when (items[index]) { - is UiObjectsListItem.Loading -> "loading" - is UiObjectsListItem.Item -> "item" - } - } - ) { index -> - val item = items[index] - when (item) { - is UiObjectsListItem.Item -> { - SwipeToDismissListItems( - modifier = Modifier - .fillMaxWidth() - .animateItem() - .noRippleThrottledClickable { - onDateEvent(DateEvent.ObjectsList.OnObjectClicked(item)) - }, - item = item, - onDateEvent = onDateEvent - ) - Divider(paddingStart = 16.dp, paddingEnd = 16.dp) - } - is UiObjectsListItem.Loading -> { - ListItemLoading(modifier = Modifier) - } - } - } - if (uiState is UiContentState.Paging) { - item { - Box( - modifier = Modifier - .fillParentMaxWidth() - .height(52.dp), - contentAlignment = Alignment.Center - ) { - LoadingState() - } - } - } - item { - Spacer(modifier = Modifier.height(200.dp)) - } - } - - LaunchedEffect(key1 = uiState) { - if (uiState is UiContentState.Idle) { - if (uiState.scrollToTop) { - scope.launch { - lazyListState.scrollToItem(0) - } - } - } - } -} - -@Composable -private fun ListItem( - modifier: Modifier, - item: UiObjectsListItem.Item -) { - val name = item.name.trim().ifBlank { stringResource(R.string.untitled) } - val createdBy = item.createdBy - val typeName = item.typeName - ListItem( - colors = ListItemDefaults.colors( - containerColor = colorResource(id = R.color.background_primary), - ), - modifier = modifier - .height(72.dp) - .fillMaxWidth(), - headlineContent = { - Text( - text = name, - style = PreviewTitle2Regular, - color = colorResource(id = R.color.text_primary), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - supportingContent = { - Row { - if (typeName != null) { - Text( - text = typeName, - style = Relations3, - color = colorResource(id = R.color.text_secondary), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - if (!createdBy.isNullOrBlank()) { - Text( - text = "${stringResource(R.string.date_layout_item_created_by)} • $createdBy", - style = Relations3, - color = colorResource(id = R.color.text_secondary), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - }, - leadingContent = { - ListWidgetObjectIcon(icon = item.icon, modifier = Modifier, iconSize = 48.dp) - } - ) -} - - -@Composable -private fun ListItemLoading( - modifier: Modifier -) { - ListItem( - colors = ListItemDefaults.colors( - containerColor = colorResource(id = R.color.background_primary), - ), - modifier = modifier - .height(72.dp) - .fillMaxWidth(), - headlineContent = { - ShimmerEffect( - modifier = Modifier - .width(164.dp) - .height(18.dp) - ) - }, - supportingContent = { - ShimmerEffect( - modifier = Modifier - .width(64.dp) - .height(13.dp) - ) - }, - leadingContent = { - ShimmerEffect( - modifier = Modifier - .size(48.dp) - ) - } - ) -} - -@Composable -fun SwipeToDismissListItems( - item: UiObjectsListItem.Item, - modifier: Modifier, - animationDuration: Int = 500, - onDateEvent: (DateEvent) -> Unit, -) { - var isRemoved by remember { mutableStateOf(false) } - val dismissState = rememberSwipeToDismissBoxState( - initialValue = SwipeToDismissBoxValue.Settled, - confirmValueChange = { value -> - if (value == SwipeToDismissBoxValue.EndToStart) { - isRemoved = true - true - } else { - false - } - return@rememberSwipeToDismissBoxState true - }, - positionalThreshold = { it * .5f } - ) - - if (dismissState.currentValue != SwipeToDismissBoxValue.Settled) { - LaunchedEffect(Unit) { - dismissState.snapTo(SwipeToDismissBoxValue.Settled) - } - } - - LaunchedEffect(key1 = isRemoved) { - if (isRemoved) { - delay(animationDuration.toLong()) - onDateEvent(DateEvent.ObjectsList.OnObjectMoveToBin(item)) - } - } - AnimatedVisibility( - visible = !isRemoved, - exit = shrinkVertically( - animationSpec = tween(durationMillis = animationDuration), - shrinkTowards = Alignment.Top - ) + fadeOut() - ) { - SwipeToDismissBox( - modifier = modifier, - state = dismissState, - enableDismissFromEndToStart = item.isPossibleToDelete, - enableDismissFromStartToEnd = false, - backgroundContent = { - DismissBackground( - actionText = stringResource(R.string.move_to_bin), - dismissState = dismissState - ) - }, - content = { - ListItem( - modifier = Modifier - .noRippleThrottledClickable { - onDateEvent(DateEvent.ObjectsList.OnObjectClicked(item)) - }, - item = item - ) - } - ) - } -} - -@Composable -private fun BoxScope.LoadingState() { - val loadingAlpha by animateFloatAsState(targetValue = 1f, label = "") - DotsLoadingIndicator( - animating = true, - modifier = Modifier - .graphicsLayer { alpha = loadingAlpha } - .align(Alignment.Center), - animationSpecs = FadeAnimationSpecs(itemCount = 3), - color = colorResource(id = R.color.glyph_active), - size = ButtonSize.Small - ) -} - -@Composable -@DefaultPreviews -fun ObjectsListScreenPreview() { - val contentListState = UiObjectsListState( - items = StubVerticalItems - ) - ObjectsScreen( - state = contentListState, - uiState = UiContentState.Idle(scrollToTop = false), - canPaginate = true, - onDateEvent = {} - ) -} \ No newline at end of file diff --git a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/models/DateEvent.kt b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/models/DateEvent.kt index 7051eadfc4..1a3793e219 100644 --- a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/models/DateEvent.kt +++ b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/models/DateEvent.kt @@ -4,7 +4,7 @@ import com.anytypeio.anytype.core_models.TimeInMillis import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState import com.anytypeio.anytype.core_models.primitives.TimestampInSeconds import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem -import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListItem +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem sealed class DateEvent { diff --git a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/models/PreviewStubs.kt b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/models/PreviewStubs.kt index a476db0b83..b8848fc661 100644 --- a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/models/PreviewStubs.kt +++ b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/ui/models/PreviewStubs.kt @@ -1,49 +1,9 @@ package com.anytypeio.anytype.feature_date.ui.models -import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.RelationFormat import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.primitives.RelationKey -import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem -import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListItem -import com.anytypeio.anytype.presentation.objects.ObjectIcon - -val StubVerticalItems = listOf( - UiObjectsListItem.Item( - id = "1", - name = "Task Object", - space = SpaceId("space1"), - type = "type1", - typeName = "Task", - createdBy = "by Joseph Wolf", - layout = ObjectType.Layout.TODO, - icon = ObjectIcon.Task(isChecked = true) - ), - UiObjectsListItem.Item( - id = "2", - name = "Page Object", - space = SpaceId("space2"), - type = "type2", - typeName = "Page", - createdBy = "by Mike Long", - layout = ObjectType.Layout.BASIC, - icon = ObjectIcon.Empty.Page - ), - UiObjectsListItem.Item( - id = "3", - name = "File Object", - space = SpaceId("space3"), - type = "type3", - typeName = "File", - createdBy = "by John Doe", - layout = ObjectType.Layout.FILE, - icon = ObjectIcon.File( - mime = "image/png", - fileName = "test_image.png" - ) - ) -) val StubHorizontalItems = listOf( UiFieldsItem.Settings(), diff --git a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateModels.kt b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateModels.kt index 3fca84a293..87782e5da0 100644 --- a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateModels.kt +++ b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateModels.kt @@ -2,7 +2,6 @@ package com.anytypeio.anytype.feature_date.viewmodel import com.anytypeio.anytype.core_models.DVSortType import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.RelationFormat import com.anytypeio.anytype.core_models.RelativeDate import com.anytypeio.anytype.core_models.TimeInMillis @@ -11,7 +10,6 @@ import com.anytypeio.anytype.core_models.primitives.RelationKey import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.primitives.TimestampInSeconds import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsItem.Loading -import com.anytypeio.anytype.presentation.objects.ObjectIcon data class DateObjectVmParams( val objectId: Id, @@ -100,55 +98,12 @@ sealed class UiFieldsItem { } } -data class UiObjectsListState( - val items: List -) { - companion object { - - val Empty = UiObjectsListState(items = emptyList()) - val LoadingState = UiObjectsListState( - items = listOf( - UiObjectsListItem.Loading("Loading-Item-1"), - UiObjectsListItem.Loading("Loading-Item-2"), - UiObjectsListItem.Loading("Loading-Item-3"), - UiObjectsListItem.Loading("Loading-Item-4"), - ) - ) - } -} - -sealed class UiObjectsListItem { - - abstract val id: String - - data class Loading(override val id: String) : UiObjectsListItem() - - data class Item( - override val id: String, - val name: String, - val space: SpaceId, - val type: String? = null, - val typeName: String? = null, - val createdBy: String? = null, - val layout: ObjectType.Layout? = null, - val icon: ObjectIcon = ObjectIcon.None, - val isPossibleToDelete: Boolean = false - ) : UiObjectsListItem() -} - sealed class UiNavigationWidget { data object Hidden : UiNavigationWidget() data object Editor : UiNavigationWidget() data object Viewer : UiNavigationWidget() } -sealed class UiContentState { - data class Idle(val scrollToTop: Boolean = false) : UiContentState() - data object InitLoading : UiContentState() - data object Paging : UiContentState() - data object Empty : UiContentState() -} - sealed class UiFieldsSheetState { data object Hidden : UiFieldsSheetState() data class Visible( diff --git a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt index 1aa2efd99c..326d5bf633 100644 --- a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt +++ b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt @@ -14,6 +14,9 @@ import com.anytypeio.anytype.core_models.TimeInSeconds import com.anytypeio.anytype.core_models.getSingleValue import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.primitives.TimestampInSeconds +import com.anytypeio.anytype.core_ui.lists.objects.UiContentState +import com.anytypeio.anytype.core_ui.lists.objects.UiContentState.Idle +import com.anytypeio.anytype.core_ui.lists.objects.UiObjectsListState import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider import com.anytypeio.anytype.domain.library.StoreSearchParams @@ -29,11 +32,9 @@ import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.page.CreateObject import com.anytypeio.anytype.domain.primitives.FieldParser import com.anytypeio.anytype.domain.relations.GetObjectRelationListById -import com.anytypeio.anytype.feature_date.viewmodel.UiErrorState.Reason import com.anytypeio.anytype.feature_date.mapping.toUiFieldsItem -import com.anytypeio.anytype.feature_date.mapping.toUiObjectsListItem import com.anytypeio.anytype.feature_date.ui.models.DateEvent -import com.anytypeio.anytype.feature_date.viewmodel.UiContentState.* +import com.anytypeio.anytype.feature_date.viewmodel.UiErrorState.Reason import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate import com.anytypeio.anytype.presentation.extension.sendAnalyticsClickDateBack import com.anytypeio.anytype.presentation.extension.sendAnalyticsClickDateCalendarView @@ -44,7 +45,9 @@ import com.anytypeio.anytype.presentation.extension.sendAnalyticsScreenDate import com.anytypeio.anytype.presentation.extension.sendAnalyticsSwitchRelationDate import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.home.navigation +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem import com.anytypeio.anytype.presentation.objects.getCreateObjectParams +import com.anytypeio.anytype.presentation.objects.toUiObjectsListItem import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel.Companion.DEFAULT_DEBOUNCE_DURATION import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState import com.anytypeio.anytype.presentation.sync.toSyncStatusWidgetState diff --git a/feature-object-type/build.gradle b/feature-object-type/build.gradle new file mode 100644 index 0000000000..0ae1a94563 --- /dev/null +++ b/feature-object-type/build.gradle @@ -0,0 +1,61 @@ +plugins { + id "com.android.library" + id "kotlin-android" + alias(libs.plugins.compose.compiler) +} + +android { + + defaultConfig { + buildConfigField "boolean", "USE_NEW_WINDOW_INSET_API", "true" + buildConfigField "boolean", "USE_EDGE_TO_EDGE", "true" + } + + buildFeatures { + compose true + } + + namespace 'com.anytypeio.anytype.feature_object_type' +} + +dependencies { + + implementation project(':domain') + implementation project(':core-ui') + implementation project(':analytics') + implementation project(':core-models') + implementation project(':core-utils') + implementation project(':localization') + implementation project(':presentation') + implementation project(':library-emojifier') + + compileOnly libs.javaxInject + + implementation libs.lifecycleViewModel + implementation libs.lifecycleRuntime + + implementation libs.appcompat + implementation libs.compose + implementation libs.composeFoundation + implementation libs.composeToolingPreview + implementation libs.composeMaterial3 + implementation libs.composeMaterial + implementation libs.navigationCompose + implementation libs.composeReorderable + + debugImplementation libs.composeTooling + + implementation libs.timber + + testImplementation project(':test:android-utils') + testImplementation project(':test:utils') + testImplementation project(":test:core-models-stub") + testImplementation libs.junit + testImplementation libs.kotlinTest + testImplementation libs.robolectric + testImplementation libs.androidXTestCore + testImplementation libs.mockitoKotlin + testImplementation libs.coroutineTesting + testImplementation libs.timberJUnit + testImplementation libs.turbine +} \ No newline at end of file diff --git a/feature-object-type/src/main/AndroidManifest.xml b/feature-object-type/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1d26c87a17 --- /dev/null +++ b/feature-object-type/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/UiEvent.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/UiEvent.kt new file mode 100644 index 0000000000..3c158075c9 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/UiEvent.kt @@ -0,0 +1,52 @@ +package com.anytypeio.anytype.feature_object_type.fields + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.RelationFormat + +sealed class FieldEvent { + + data object OnFieldEditScreenDismiss : FieldEvent() + data object OnAddFieldScreenDismiss : FieldEvent() + + data class OnFieldItemClick(val item: UiFieldsListItem) : FieldEvent() + + data class OnAddToHeaderFieldClick( + val item: UiAddFieldItem + ) : FieldEvent() + + data class OnAddToSidebarFieldClick( + val item: UiAddFieldItem + ) : FieldEvent() + + data class OnSaveButtonClicked( + val name: String, + val format: RelationFormat, + val limitObjectTypes: List + ) : FieldEvent() + + data object OnChangeTypeClick : FieldEvent() + data object OnLimitTypesClick : FieldEvent() + + sealed class FieldItemMenu : FieldEvent() { + data class OnDeleteFromTypeClick(val item: UiFieldsListItem) : FieldItemMenu() + data class OnRemoveLocalClick(val item: UiFieldsListItem) : FieldItemMenu() + data class OnAddLocalToTypeClick(val item: UiFieldsListItem) : FieldItemMenu() + } + + sealed class FieldLocalInfo : FieldEvent() { + data object OnDismiss : FieldLocalInfo() + } + + sealed class Section : FieldEvent() { + data object OnAddToHeaderIconClick : Section() + data object OnAddToSidebarIconClick : Section() + data object OnLocalInfoClick : Section() + } + + sealed class DragEvent : FieldEvent() { + data class OnMove(val fromKey: String, val toKey: String) : DragEvent() + data object OnDragEnd : DragEvent() + } + + data class OnAddFieldSearchQueryChanged(val query: String) : FieldEvent() +} diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/UiState.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/UiState.kt new file mode 100644 index 0000000000..a2334232b0 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/UiState.kt @@ -0,0 +1,203 @@ +package com.anytypeio.anytype.feature_object_type.fields + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.presentation.objects.ObjectIcon + +//region Top bar +sealed class UiFieldsTitleState { + data object Hidden : UiFieldsTitleState() + data class Visible(val title: String) : UiFieldsTitleState() +} + +sealed class UiFieldsCancelButtonState { + data object Hidden : UiFieldsCancelButtonState() + data object Visible : UiFieldsCancelButtonState() +} + +sealed class UiFieldsSaveButtonState { + data object Hidden : UiFieldsSaveButtonState() + data object Visible : UiFieldsSaveButtonState() +} + +sealed class UiFieldsEditingPanelState { + data object Hidden : UiFieldsEditingPanelState() + data object Visible : UiFieldsEditingPanelState() +} +//endregion + +//region Fields List +data class UiFieldsListState(val items: List) { + companion object { + val EMPTY = UiFieldsListState(emptyList()) + } +} + +sealed class UiFieldsListItem { + abstract val id: Id + + sealed class Item : UiFieldsListItem() { + abstract val fieldKey: Key + abstract val fieldTitle: String + abstract val format: RelationFormat + abstract val limitObjectTypes: List + abstract val canDelete: Boolean + abstract val isEditableField: Boolean + + data class Draggable( + override val id: Id, + override val fieldKey: Key, + override val fieldTitle: String, + override val format: RelationFormat, + override val limitObjectTypes: List = emptyList(), + override val canDelete: Boolean, + override val isEditableField: Boolean + ) : Item() + + data class Local( + override val id: Id, + override val fieldKey: Key, + override val fieldTitle: String, + override val format: RelationFormat, + override val limitObjectTypes: List = emptyList(), + override val canDelete: Boolean = false, + override val isEditableField: Boolean + ) : Item() + } + + sealed class Section : UiFieldsListItem() { + abstract val canAdd: Boolean + + data class Header( + override val id: Id = ID, + override val canAdd: Boolean = false + ) : Section() { + companion object { + const val ID = "section_header" + } + } + + data class SideBar( + override val id: Id = ID, + override val canAdd: Boolean = false + ) : Section() { + companion object { + const val ID = "section_sidebar" + } + } + + data class Hidden( + override val id: Id = ID, + override val canAdd: Boolean = false + ) : Section() { + companion object { + const val ID = "section_hidden" + } + } + + data class Local( + override val id: Id = ID, + override val canAdd: Boolean = false + ) : Section() { + companion object { + const val ID = "section_local" + } + } + + data class File( + override val id: Id = ID, + override val canAdd: Boolean = false + ) : Section() { + companion object { + const val ID = "section_file_recommended" + } + } + + data class SpaceFields( + override val id: Id = ID, + override val canAdd: Boolean = false + ) : Section() { + companion object { + const val ID = "section_space_fields" + } + } + + data class LibraryFields( + override val id: Id = ID, + override val canAdd: Boolean = false + ) : Section() { + companion object { + const val ID = "section_library_fields" + } + } + } +} +//endregion + +//region Edit or New Field + +data class UiFieldObjectItem( + val id: Id, val key: Key, val title: String, val icon: ObjectIcon +) + +sealed class UiFieldEditOrNewState { + data object Hidden : UiFieldEditOrNewState() + sealed class Visible : UiFieldEditOrNewState() { + abstract val item: UiFieldsListItem.Item + + data class Edit( + override val item: UiFieldsListItem.Item + ) : Visible() + + data class New( + override val item: UiFieldsListItem.Item + ) : Visible() + + data class ViewOnly( + override val item: UiFieldsListItem.Item + ) : Visible() + } +} +//endregion + +//region ERRORS +sealed class UiFieldsErrorState { + data object Hidden : UiFieldsErrorState() + data class Show(val reason: Reason) : UiFieldsErrorState() + + sealed class Reason { + data class ErrorGettingObjects(val msg: String) : Reason() + data class Other(val msg: String) : Reason() + } +} +//endregion + +//region COMMANDS +sealed class TypeFieldsCommand { + data class OpenEmojiPicker(val emoji: String) : TypeFieldsCommand() +} +//endregion + +//region Section Local Fields Info +sealed class UiLocalsFieldsInfoState { + data object Hidden : UiLocalsFieldsInfoState() + data object Visible : UiLocalsFieldsInfoState() +} +//endregion + +//region Add Fields screen +sealed class UiAddFieldsScreenState { + data object Hidden : UiAddFieldsScreenState() + data class Visible(val items: List, val addToHeader: Boolean) : UiAddFieldsScreenState() +} + +data class UiAddFieldItem( + val id: Id, + val fieldKey: Key, + val fieldTitle: String, + val format: RelationFormat +) +//endregion + + diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/AddScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/AddScreen.kt new file mode 100644 index 0000000000..580f01174c --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/AddScreen.kt @@ -0,0 +1,170 @@ +package com.anytypeio.anytype.feature_object_type.fields.ui + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.extensions.simpleIcon +import com.anytypeio.anytype.core_ui.foundation.DefaultSearchBar +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle +import com.anytypeio.anytype.feature_object_type.fields.FieldEvent +import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem +import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddFieldScreen( + state: UiAddFieldsScreenState, + fieldEvent: (FieldEvent) -> Unit +) { + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + + var isSearchEmpty by remember { mutableStateOf(true) } + + val lazyListState = rememberLazyListState() + + if (state is UiAddFieldsScreenState.Visible) { + ModalBottomSheet( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.safeDrawing) + .nestedScroll(rememberNestedScrollInteropConnection()), + dragHandle = { DragHandle() }, + scrimColor = colorResource(id = R.color.modal_screen_outside_background), + containerColor = colorResource(id = R.color.background_primary), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + sheetState = bottomSheetState, + onDismissRequest = { + fieldEvent(FieldEvent.OnAddFieldScreenDismiss) + }, + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize(), + state = lazyListState + ) { + item { + DefaultSearchBar( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + isSearchEmpty = it.isEmpty() + fieldEvent(FieldEvent.OnAddFieldSearchQueryChanged(it)) + } + } + items( + count = state.items.size, + key = { index -> state.items[index].id }, + itemContent = { index -> + val item = state.items[index] + FieldItem( + modifier = commonItemModifier() + .noRippleThrottledClickable { + if (state.addToHeader) { + fieldEvent( + FieldEvent.OnAddToHeaderFieldClick( + item = item + ) + ) + } else { + fieldEvent( + FieldEvent.OnAddToSidebarFieldClick( + item = item + ) + ) + } + }, + item = item + ) + } + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun FieldItem( + modifier: Modifier, + item: UiAddFieldItem +) { + Row( + modifier = modifier, + verticalAlignment = CenterVertically + ) { + val formatIcon = item.format.simpleIcon() + if (formatIcon != null) { + Image( + modifier = Modifier + .padding(end = 10.dp) + .size(24.dp), + painter = painterResource(id = formatIcon), + contentDescription = "Relation format icon", + ) + } + Text( + modifier = Modifier + .fillMaxWidth() + .weight(1.0f) + .padding(end = 16.dp), + text = item.fieldTitle, + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@DefaultPreviews +@Composable +fun PreviewAddFieldScreen() { + AddFieldScreen( + state = UiAddFieldsScreenState.Visible( + items = listOf( + UiAddFieldItem( + id = "1", + fieldKey = "key", + fieldTitle = "Title", + format = RelationFormat.LONG_TEXT + ) + ), + addToHeader = true + ), + fieldEvent = {} + ) +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/EditScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/EditScreen.kt new file mode 100644 index 0000000000..06564236e6 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/EditScreen.kt @@ -0,0 +1,386 @@ +package com.anytypeio.anytype.feature_object_type.fields.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.extensions.getPrettyName +import com.anytypeio.anytype.core_ui.extensions.simpleIcon +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.views.ButtonPrimary +import com.anytypeio.anytype.core_ui.views.ButtonSize +import com.anytypeio.anytype.core_ui.views.Caption1Regular +import com.anytypeio.anytype.core_ui.views.HeadlineHeading +import com.anytypeio.anytype.core_ui.views.Title2 +import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon +import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle +import com.anytypeio.anytype.feature_object_type.fields.FieldEvent +import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState +import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem +import com.anytypeio.anytype.feature_object_type.ui.createDummyFieldDraggableItem + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditFieldScreen( + modifier: Modifier, + uiFieldEditOrNewState: UiFieldEditOrNewState, + fieldEvent: (FieldEvent) -> Unit +) { + if (uiFieldEditOrNewState is UiFieldEditOrNewState.Visible) { + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + modifier = modifier, + dragHandle = { DragHandle() }, + scrimColor = colorResource(id = R.color.modal_screen_outside_background), + containerColor = colorResource(id = R.color.background_primary), + shape = RoundedCornerShape(16.dp), + sheetState = bottomSheetState, + onDismissRequest = { fieldEvent(FieldEvent.OnFieldEditScreenDismiss) }, + ) { + EditFieldContent( + modifier = Modifier.fillMaxWidth(), + uiState = uiFieldEditOrNewState, + fieldEvent = fieldEvent + ) + } + } +} + +@Composable +private fun EditFieldContent( + modifier: Modifier, + uiState: UiFieldEditOrNewState.Visible, + fieldEvent: (FieldEvent) -> Unit +) { + + val field = uiState.item + var innerValue by remember(field.fieldTitle) { mutableStateOf(field.fieldTitle) } + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + val isEditable = field.isEditableField + + val title = when (uiState) { + is UiFieldEditOrNewState.Visible.Edit -> stringResource(R.string.object_type_fields_edit_field) + is UiFieldEditOrNewState.Visible.New -> stringResource(R.string.object_type_fields_new_field) + is UiFieldEditOrNewState.Visible.ViewOnly -> stringResource(R.string.object_type_fields_preview_field) + } + + Column(modifier = modifier) { + // Header title + Box( + modifier = Modifier + .fillMaxWidth() + .height(44.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = title, + style = Title2, + color = colorResource(id = R.color.text_primary) + ) + } + + // Name text field + NameTextField( + modifier = Modifier.fillMaxWidth(), + value = innerValue, + isEditable = isEditable, + focusRequester = focusRequester, + keyboardController = keyboardController, + onValueChange = { innerValue = it } + ) + + Spacer(modifier = Modifier.height(10.dp)) + Divider() + + // Field type section + FieldTypeSection( + format = field.format, + isEditable = isEditable, + onTypeClick = { fieldEvent(FieldEvent.OnChangeTypeClick) } + ) + Divider() + + // Limit object types (only for OBJECT format) + if (field.format == RelationFormat.OBJECT) { + LimitTypesSection( + objTypes = field.limitObjectTypes, + isEditable = isEditable, + onLimitTypesClick = { fieldEvent(FieldEvent.OnLimitTypesClick) } + ) + Divider() + } + + Spacer(modifier = Modifier.height(14.dp)) + + if (isEditable) { + ButtonPrimary( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = stringResource(R.string.object_type_fields_btn_save), + onClick = { + fieldEvent( + FieldEvent.OnSaveButtonClicked( + name = innerValue, + format = field.format, + limitObjectTypes = field.limitObjectTypes.map { it.id } + ) + ) + }, + size = ButtonSize.Large + ) + } + } +} + +@Composable +fun NameTextField( + modifier: Modifier, + value: String, + isEditable: Boolean, + focusRequester: FocusRequester, + keyboardController: SoftwareKeyboardController?, + onValueChange: (String) -> Unit +) { + val focusManager = LocalFocusManager.current + + Column(modifier = modifier) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = stringResource(id = R.string.name), + style = Caption1Regular, + color = colorResource(id = R.color.text_secondary) + ) + + BasicTextField( + value = value, + onValueChange = onValueChange, + textStyle = HeadlineHeading.copy(color = colorResource(id = R.color.text_primary)), + singleLine = true, + enabled = isEditable, + cursorBrush = SolidColor(colorResource(id = R.color.text_primary)), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 20.dp, top = 6.dp, end = 20.dp) + .focusRequester(focusRequester) + .onFocusChanged { /* You can handle focus changes here if needed */ }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions { + keyboardController?.hide() + focusManager.clearFocus() + onValueChange(value) + }, + decorationBox = { innerTextField -> + if (value.isEmpty()) { + Text( + text = stringResource(id = R.string.untitled), + style = HeadlineHeading, + color = colorResource(id = R.color.text_tertiary), + modifier = Modifier.fillMaxWidth() + ) + } + innerTextField() + } + ) + } +} + +@Composable +fun FieldTypeSection( + format: RelationFormat, + isEditable: Boolean, + onTypeClick: () -> Unit +) { + val icon = format.simpleIcon() + SectionItem( + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + text = stringResource(id = R.string.type) + ) + Divider() + Box( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .padding(horizontal = 20.dp) + .noRippleThrottledClickable { if (isEditable) onTypeClick() } + ) { + if (icon != null) { + Image( + modifier = Modifier + .size(24.dp) + .align(Alignment.CenterStart), + painter = painterResource(id = icon), + contentDescription = "Relation format icon" + ) + } + Text( + modifier = Modifier + .fillMaxWidth() + .padding(start = 34.dp) + .align(Alignment.CenterStart), + text = stringResource(format.getPrettyName()), + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (isEditable) { + Image( + modifier = Modifier.align(Alignment.CenterEnd), + painter = painterResource(id = R.drawable.ic_arrow_forward_24), + contentDescription = "Change field format icon" + ) + } + } +} + +@Composable +fun LimitTypesSection( + objTypes: List, + isEditable: Boolean, + onLimitTypesClick: () -> Unit +) { + val size = objTypes.size + SectionItem( + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + text = stringResource(id = R.string.limit_object_types) + ) + Divider() + Box( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .padding(horizontal = 20.dp) + .noRippleThrottledClickable { if (isEditable) onLimitTypesClick() } + ) { + if (objTypes.isNotEmpty()) { + Row(modifier = Modifier.align(Alignment.CenterStart)) { + ListWidgetObjectIcon( + modifier = Modifier.size(20.dp), + icon = objTypes.first().icon, + backgroundColor = R.color.transparent_black + ) + Text( + modifier = Modifier.padding(start = 6.dp), + text = objTypes.first().title.take(20), + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (size > 1) { + Text( + text = " +${size - 1}", + style = BodyRegular, + color = colorResource(id = R.color.text_primary) + ) + } + } + } else { + Text( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterStart), + text = stringResource(R.string.none), + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (isEditable) { + Image( + modifier = Modifier.align(Alignment.CenterEnd), + painter = painterResource(id = R.drawable.ic_arrow_forward_24), + contentDescription = "Change field object types icon" + ) + } + } +} + +@Composable +fun SectionItem(modifier: Modifier, text: String) { + Box(modifier = modifier) { + Text( + modifier = Modifier + .padding(start = 20.dp, bottom = 8.dp) + .align(Alignment.BottomStart), + text = text, + style = Caption1Regular, + color = colorResource(id = R.color.text_secondary) + ) + } +} + +@DefaultPreviews +@Composable +private fun MyPreview() { + EditFieldScreen( + modifier = Modifier.fillMaxWidth(), + uiFieldEditOrNewState = UiFieldEditOrNewState.Visible.Edit( + item = createDummyFieldDraggableItem() + ), + fieldEvent = {} + ) +} + +@DefaultPreviews +@Composable +private fun MyPreviewOnlyPreview() { + EditFieldScreen( + modifier = Modifier.fillMaxWidth(), + uiFieldEditOrNewState = UiFieldEditOrNewState.Visible.ViewOnly( + item = createDummyFieldDraggableItem( + isEditableField = false + ) + ), + fieldEvent = {} + ) +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/InfoScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/InfoScreen.kt new file mode 100644 index 0000000000..d2d404051c --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/InfoScreen.kt @@ -0,0 +1,113 @@ +package com.anytypeio.anytype.feature_object_type.fields.ui + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular +import com.anytypeio.anytype.core_ui.views.ButtonSecondary +import com.anytypeio.anytype.core_ui.views.ButtonSize +import com.anytypeio.anytype.core_ui.views.HeadlineHeading +import com.anytypeio.anytype.core_ui.widgets.dv.DragHandle +import com.anytypeio.anytype.feature_object_type.fields.FieldEvent +import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SectionLocalFieldsInfo( + modifier: Modifier, + state: UiLocalsFieldsInfoState, + fieldEvent: (FieldEvent) -> Unit +) { + + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + + if (state is UiLocalsFieldsInfoState.Visible) { + LocalInfoScreen( + modifier = modifier, + bottomSheetState = bottomSheetState, + onDismiss = { + fieldEvent(FieldEvent.FieldLocalInfo.OnDismiss) + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LocalInfoScreen( + modifier: Modifier, + bottomSheetState: SheetState, + onDismiss: () -> Unit +) { + ModalBottomSheet( + modifier = modifier, + dragHandle = { DragHandle() }, + scrimColor = colorResource(id = R.color.modal_screen_outside_background), + containerColor = colorResource(id = R.color.background_primary), + shape = RoundedCornerShape(16.dp), + sheetState = bottomSheetState, + onDismissRequest = { + onDismiss() + }, + ) { + Spacer(modifier = Modifier.height(20.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + textAlign = TextAlign.Center, + style = HeadlineHeading, + color = colorResource(id = R.color.text_primary), + text = stringResource(R.string.object_type_fields_local_info_title) + ) + Spacer(modifier = Modifier.height(7.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + textAlign = TextAlign.Center, + style = BodyCalloutRegular, + color = colorResource(id = R.color.text_primary), + text = stringResource(R.string.object_type_fields_local_info_description) + ) + Spacer(modifier = Modifier.height(30.dp)) + ButtonSecondary( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = stringResource(R.string.object_type_fields_local_info_button), + size = ButtonSize.LargeSecondary, + onClick = { + onDismiss() + } + ) + Spacer(modifier = Modifier.height(10.dp)) + } +} + +@DefaultPreviews +@Composable +fun SectionLocalFieldsInfoPreview() { + SectionLocalFieldsInfo( + modifier = Modifier.fillMaxWidth(), + state = UiLocalsFieldsInfoState.Visible, + fieldEvent = {} + ) +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/ListScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/ListScreen.kt new file mode 100644 index 0000000000..5367707496 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/ListScreen.kt @@ -0,0 +1,775 @@ +package com.anytypeio.anytype.feature_object_type.fields.ui + +import android.os.Build +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedback +import com.anytypeio.anytype.core_ui.common.ReorderHapticFeedbackType +import com.anytypeio.anytype.core_ui.common.rememberReorderHapticFeedback +import com.anytypeio.anytype.core_ui.extensions.simpleIcon +import com.anytypeio.anytype.core_ui.foundation.Dragger +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium +import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular +import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.views.Caption1Medium +import com.anytypeio.anytype.core_ui.views.Title1 +import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon +import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK +import com.anytypeio.anytype.feature_object_type.R +import com.anytypeio.anytype.feature_object_type.fields.FieldEvent +import com.anytypeio.anytype.feature_object_type.fields.FieldEvent.* +import com.anytypeio.anytype.feature_object_type.fields.FieldEvent.FieldItemMenu.* +import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState +import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState +import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem +import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Section +import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListState +import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState +import com.anytypeio.anytype.feature_object_type.ui.UiIconState +import com.anytypeio.anytype.feature_object_type.ui.UiTitleState +import com.anytypeio.anytype.presentation.objects.ObjectIcon +import kotlinx.coroutines.delay +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.ReorderableLazyListState +import sh.calvin.reorderable.rememberReorderableLazyListState + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FieldsMainScreen( + uiFieldsListState: UiFieldsListState, + uiTitleState: UiTitleState, + uiIconState: UiIconState, + uiFieldEditOrNewState: UiFieldEditOrNewState, + uiFieldLocalInfoState: UiLocalsFieldsInfoState, + uiAddFieldsScreenState: UiAddFieldsScreenState, + fieldEvent: (FieldEvent) -> Unit +) { + + val hapticFeedback = rememberReorderHapticFeedback() + + val lazyListState = rememberLazyListState() + + val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to -> + fieldEvent(DragEvent.OnMove(from.key as String, to.key as String)) + hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.MOVE) + } + + var isDragging by remember { mutableStateOf(false) } + + LaunchedEffect(reorderableLazyColumnState.isAnyItemDragging) { + if (reorderableLazyColumnState.isAnyItemDragging) { + isDragging = true + // Optional: Add a small delay to avoid triggering on very short drags + delay(50) + } else if (isDragging) { + isDragging = false + fieldEvent(DragEvent.OnDragEnd) + hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.MOVE) + } + } + + Scaffold( + modifier = Modifier + .nestedScroll(rememberNestedScrollInteropConnection()) + .background( + color = colorResource(id = R.color.widget_background), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + ) + .fillMaxSize(), + containerColor = colorResource(id = R.color.transparent_black), + contentColor = colorResource(id = R.color.widget_background), + topBar = { + TopBar( + modifier = Modifier.fillMaxWidth(), + uiTitleState = uiTitleState, + uiIconState = uiIconState + ) + }, + content = { paddingValues -> + val contentModifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) { + Modifier + .windowInsetsPadding(WindowInsets.navigationBars) + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()) + } else { + Modifier + .fillMaxSize() + .padding(paddingValues) + } + LazyColumn( + modifier = contentModifier, + state = lazyListState + ) { + items( + count = uiFieldsListState.items.size, + key = { index -> uiFieldsListState.items[index].id }, + contentType = { index -> getContentType(uiFieldsListState.items[index]) }, + itemContent = { index -> + val item = uiFieldsListState.items[index] + when (item) { + is UiFieldsListItem.Item.Draggable -> { + FieldItemDraggable( + modifier = commonItemModifier(), + item = item, + reorderingState = reorderableLazyColumnState, + fieldEvent = fieldEvent, + hapticFeedback = hapticFeedback + ) + } + + is UiFieldsListItem.Item.Local -> { + FieldItemLocal( + modifier = commonItemModifier(), + item = item, + fieldEvent = fieldEvent + ) + } + + is Section.SideBar -> { + SectionItem( + item = item, + reorderingState = reorderableLazyColumnState, + fieldEvent = fieldEvent, + isReorderable = true, + onAddIconClick = { + fieldEvent(FieldEvent.Section.OnAddToSidebarIconClick) + } + ) + } + is Section.Hidden -> { + SectionItem( + item = item, + reorderingState = reorderableLazyColumnState, + fieldEvent = fieldEvent, + isReorderable = true + ) + } + is Section.Header -> { + SectionItem( + item = item, + reorderingState = reorderableLazyColumnState, + fieldEvent = fieldEvent, + isReorderable = false, + onAddIconClick = { + fieldEvent(FieldEvent.Section.OnAddToHeaderIconClick) + } + ) + } + is Section.Local, + is Section.File -> { + SectionItem( + item = item, + reorderingState = reorderableLazyColumnState, + fieldEvent = fieldEvent, + isReorderable = false + ) + } + + is Section.LibraryFields -> TODO() + is Section.SpaceFields -> TODO() + } + } + ) + item { + Spacer(modifier = Modifier.height(60.dp)) + } + } + } + ) + + if (uiFieldEditOrNewState is UiFieldEditOrNewState.Visible) { + EditFieldScreen( + modifier = Modifier.fillMaxWidth(), + uiFieldEditOrNewState = uiFieldEditOrNewState, + fieldEvent = fieldEvent + ) + } + + if (uiFieldLocalInfoState is UiLocalsFieldsInfoState.Visible) { + SectionLocalFieldsInfo( + modifier = Modifier.fillMaxWidth(), + state = uiFieldLocalInfoState, + fieldEvent = fieldEvent + ) + } + + if (uiAddFieldsScreenState is UiAddFieldsScreenState.Visible) { + AddFieldScreen( + state = uiAddFieldsScreenState, + fieldEvent = fieldEvent + ) + } +} + +/** Returns a content type string based on the item type. **/ +private fun getContentType(item: UiFieldsListItem): String { + return when (item) { + is UiFieldsListItem.Item.Draggable -> FieldsItemsContentType.FIELD_ITEM_DRAGGABLE + is UiFieldsListItem.Item.Local -> FieldsItemsContentType.FIELD_ITEM_LOCAL + is Section.SideBar -> FieldsItemsContentType.SECTION_SIDEBAR + is Section.Header -> FieldsItemsContentType.SECTION_HEADER + is Section.Hidden -> FieldsItemsContentType.SECTION_HIDDEN + is Section.Local -> FieldsItemsContentType.SECTION_LOCAL + is Section.File -> FieldsItemsContentType.SECTION_FILE + is Section.LibraryFields -> "content_type_section_library_fields" + is Section.SpaceFields -> "content_type_section_space_fields" + } +} + +/** A common modifier for list items. **/ +@Composable +fun LazyItemScope.commonItemModifier() = Modifier + .height(52.dp) + .fillMaxWidth() + .padding(horizontal = 20.dp) + .bottomBorder() + .animateItem() + +@Composable +private fun TopBar( + modifier: Modifier, + uiTitleState: UiTitleState, + uiIconState: UiIconState, +) { + val modifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) { + modifier.windowInsetsPadding(WindowInsets.statusBars) + } else { + modifier + } + Column( + modifier = modifier + .background( + color = colorResource(id = R.color.widget_background), + shape = RoundedCornerShape(16.dp, 16.dp, 0.dp, 0.dp) + ) + ) { + Dragger( + modifier = Modifier + .padding(vertical = 6.dp) + .align(Alignment.CenterHorizontally) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) { + Text( + modifier = Modifier + .wrapContentSize() + .align(Alignment.Center), + text = stringResource(R.string.object_type_fields_title), + style = Title1, + color = colorResource(R.color.text_primary) + ) + } + InfoBar( + modifier = Modifier + .fillMaxWidth() + .height(36.dp) + .background(color = colorResource(R.color.shape_transparent_secondary)), + uiTitleState = uiTitleState, + uiIconState = uiIconState + ) + } +} + +@Composable +private fun InfoBar(modifier: Modifier, uiTitleState: UiTitleState, uiIconState: UiIconState) { + Row( + modifier = modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = CenterVertically + ) { + Text( + text = stringResource(R.string.object_type_fields_info_text), + style = Caption1Medium, + color = colorResource(id = R.color.text_primary), + ) + ListWidgetObjectIcon( + modifier = Modifier + + .padding(start = 4.dp) + .size(18.dp), + icon = uiIconState.icon, + backgroundColor = R.color.transparent_black + ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = uiTitleState.title, + style = Caption1Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = colorResource(id = R.color.text_primary), + ) + } +} + +@Composable +private fun LazyItemScope.SectionItem( + item: UiFieldsListItem.Section, + reorderingState: ReorderableLazyListState, + isReorderable: Boolean = true, + onAddIconClick: () -> Unit = {}, + fieldEvent: (FieldEvent) -> Unit +) { + val (title, textColor) = when (item) { + is Section.Header -> stringResource(R.string.object_type_fields_section_header) to colorResource( + id = R.color.text_secondary + ) + + is Section.SideBar -> + stringResource(R.string.object_type_fields_section_fields_menu) to colorResource( + id = R.color.text_secondary + ) + + is Section.Hidden -> stringResource(R.string.object_type_fields_section_hidden) to colorResource( + id = R.color.text_secondary + ) + + is Section.Local -> stringResource(R.string.object_type_fields_section_local_fields) to colorResource( + id = R.color.text_primary + ) + + is Section.LibraryFields -> TODO() + is Section.SpaceFields -> TODO() + is Section.File -> stringResource(R.string.object_type_fields_section_file) to colorResource( + id = R.color.text_secondary + ) + } + ReorderableItem( + state = reorderingState, + key = item.id, + enabled = isReorderable + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + ) { + Text( + modifier = Modifier + .padding(bottom = 7.dp, start = 20.dp) + .align(Alignment.BottomStart), + text = title, + style = BodyCalloutMedium, + color = textColor, + ) + if (item.canAdd) { + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .width(54.dp) + .height(40.dp) + .noRippleThrottledClickable { + onAddIconClick() + } + ) { + Image( + modifier = Modifier + .padding(bottom = 6.dp, end = 20.dp) + .wrapContentSize() + .align(Alignment.BottomEnd), + painter = painterResource(R.drawable.ic_default_plus), + contentDescription = "$title plus button" + ) + } + } + if (item is Section.Local) { + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .height(37.dp) + .width(44.dp) + .noRippleThrottledClickable { + fieldEvent(FieldEvent.Section.OnLocalInfoClick) + } + ) { + Image( + modifier = Modifier + .padding(bottom = 9.dp, end = 20.dp) + .wrapContentSize() + .align(Alignment.BottomEnd), + painter = painterResource(R.drawable.ic_section_local_fields), + contentDescription = "Section local fields info" + ) + } + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun FieldItemLocal( + modifier: Modifier, + item: UiFieldsListItem.Item.Local, + fieldEvent: (FieldEvent) -> Unit +) { + val isMenuExpanded = remember { mutableStateOf(false) } + + Row( + modifier = modifier + .combinedClickable( + onClick = { fieldEvent(OnFieldItemClick(item = item)) }, + onLongClick = { isMenuExpanded.value = true } + ), + verticalAlignment = CenterVertically + ) { + val formatIcon = item.format.simpleIcon() + if (formatIcon != null) { + Image( + modifier = Modifier + .padding(end = 10.dp) + .size(24.dp), + painter = painterResource(id = formatIcon), + contentDescription = "Relation format icon", + ) + } + + Text( + modifier = Modifier + .fillMaxWidth() + .weight(1.0f) + .padding(end = 16.dp), + text = item.fieldTitle, + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Image( + modifier = Modifier + .size(24.dp) + .noRippleThrottledClickable { + isMenuExpanded.value = true + }, + painter = painterResource(R.drawable.ic_space_list_dots), + contentDescription = "Local item menu" + ) + ItemDropDownMenu( + item = item, + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onFieldEvent = { + isMenuExpanded.value = false + fieldEvent(it) + } + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun LazyItemScope.FieldItemDraggable( + modifier: Modifier, + reorderingState: ReorderableLazyListState, + hapticFeedback: ReorderHapticFeedback, + item: UiFieldsListItem.Item.Draggable, + fieldEvent: (FieldEvent) -> Unit +) { + val isMenuExpanded = remember { mutableStateOf(false) } + + ReorderableItem( + state = reorderingState, + key = item.id, + ) { isDragging -> + Row( + modifier = modifier, + verticalAlignment = CenterVertically + ) { + val formatIcon = item.format.simpleIcon() + if (formatIcon != null) { + Image( + modifier = Modifier + .padding(end = 10.dp) + .size(24.dp), + painter = painterResource(id = formatIcon), + contentDescription = "Relation format icon", + ) + } + + Box( + modifier = Modifier + .weight(1f) // fill remaining space + .combinedClickable( + onClick = { + // normal click => open/edit + fieldEvent(OnFieldItemClick(item = item)) + }, + onLongClick = { + // show your menu, only if NOT dragging + if (item.canDelete) { + isMenuExpanded.value = true + } + } + ) + .padding(end = 16.dp) + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + text = item.fieldTitle, + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Image( + modifier = Modifier + .size(24.dp) + .draggableHandle( + onDragStarted = { + hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.START) + }, + onDragStopped = { + hapticFeedback.performHapticFeedback(ReorderHapticFeedbackType.END) + } + ), + painter = painterResource(R.drawable.ic_dnd), + contentDescription = "Icon drag" + ) + + ItemDropDownMenu( + item = item, + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onFieldEvent = { + isMenuExpanded.value = false + fieldEvent(it) + } + ) + } + } +} + +@Composable +fun Modifier.bottomBorder( + strokeWidth: Dp = 0.5.dp, + color: Color = colorResource(R.color.shape_primary) +) = composed( + factory = { + val density = LocalDensity.current + val strokeWidthPx = density.run { strokeWidth.toPx() } + + Modifier.drawBehind { + val width = size.width + val height = size.height - strokeWidthPx / 2 + + drawLine( + color = color, + start = Offset(x = 0f, y = height), + end = Offset(x = width, y = height), + strokeWidth = strokeWidthPx + ) + } + } +) + +@Composable +fun ItemDropDownMenu( + item: UiFieldsListItem.Item, + showMenu: Boolean, + onDismissRequest: () -> Unit, + onFieldEvent: (FieldEvent) -> Unit, +) { + DropdownMenu( + modifier = Modifier + .width(244.dp), + expanded = showMenu, + offset = DpOffset(x = 0.dp, y = 0.dp), + onDismissRequest = { + onDismissRequest() + }, + shape = RoundedCornerShape(10.dp), + containerColor = colorResource(id = R.color.background_secondary), + ) { + when (item) { + is UiFieldsListItem.Item.Draggable -> { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.object_type_fields_menu_delete), + style = BodyCalloutRegular, + color = colorResource(id = R.color.palette_system_red) + ) + }, + onClick = { + onFieldEvent(OnDeleteFromTypeClick(item)) + }, + ) + } + + is UiFieldsListItem.Item.Local -> { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.object_type_fields_menu_add_to_type), + style = BodyCalloutRegular, + color = colorResource(id = R.color.text_primary) + ) + }, + onClick = { + onFieldEvent(OnAddLocalToTypeClick(item)) + }, + ) +// DropdownMenuItem( +// text = { +// Text( +// text = stringResource(R.string.object_type_fields_menu_remove), +// style = BodyCalloutRegular, +// color = colorResource(id = R.color.palette_system_red) +// ) +// }, +// onClick = { +// onFieldEvent(FieldItemMenu.OnRemoveLocalClick(item)) +// }, +// ) + } + } + } +} + +@DefaultPreviews +@Composable +fun PreviewTypeFieldsMainScreen() { + FieldsMainScreen( + uiTitleState = UiTitleState(title = "Page", isEditable = false), + uiIconState = UiIconState(icon = ObjectIcon.Empty.ObjectType, isEditable = false), + uiFieldsListState = UiFieldsListState( + items = listOf( + UiFieldsListItem.Section.Header(), + UiFieldsListItem.Item.Draggable( + id = "id1", + fieldKey = "key1", + fieldTitle = "Status", + format = RelationFormat.STATUS, + canDelete = true, + isEditableField = true + ), + UiFieldsListItem.Item.Draggable( + id = "id2", + fieldKey = "key2", + fieldTitle = "Very long field title, just to test how it looks", + format = RelationFormat.LONG_TEXT, + canDelete = true, + isEditableField = true + ), + UiFieldsListItem.Section.SideBar( + canAdd = true + ), + UiFieldsListItem.Item.Draggable( + id = "id3", + fieldKey = "key3", + fieldTitle = "Links", + format = RelationFormat.URL, + isEditableField = true, + canDelete = true + ), + UiFieldsListItem.Item.Draggable( + id = "id4", + fieldKey = "key4", + fieldTitle = "Very long field title, just to test how it looks", + format = RelationFormat.DATE, + isEditableField = true, + canDelete = true + ), + UiFieldsListItem.Section.Hidden(), + UiFieldsListItem.Item.Draggable( + id = "id555", + fieldKey = "key555", + fieldTitle = "Hidden field", + format = RelationFormat.LONG_TEXT, + isEditableField = true, + canDelete = true + ), + UiFieldsListItem.Section.Local(), + UiFieldsListItem.Item.Local( + id = "id5", + fieldKey = "key5", + fieldTitle = "Local field", + format = RelationFormat.LONG_TEXT, + isEditableField = true + ), + UiFieldsListItem.Item.Local( + id = "id6", + fieldKey = "key6", + fieldTitle = "Local Very long field title, just to test how it looks", + format = RelationFormat.LONG_TEXT, + isEditableField = true + ) + ) + ), + fieldEvent = {}, + uiFieldEditOrNewState = UiFieldEditOrNewState.Hidden, + uiFieldLocalInfoState = UiLocalsFieldsInfoState.Hidden, + uiAddFieldsScreenState = UiAddFieldsScreenState.Hidden + ) +} + +object FieldsItemsContentType { + const val FIELD_ITEM_DRAGGABLE = "content_type_field_item_draggable" + const val FIELD_ITEM_DEFAULT = "content_type_field_item_default" + const val FIELD_ITEM_LOCAL = "content_type_field_item_local" + const val SECTION_HEADER = "content_type_section_header" + const val SECTION_SIDEBAR = "content_type_section_sidebar" + const val SECTION_HIDDEN = "content_type_section_hidden" + const val SECTION_LOCAL = "content_type_section_local" + const val SECTION_FILE = "content_type_section_file" +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/Stubs.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/Stubs.kt new file mode 100644 index 0000000000..3f93f6bf32 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/Stubs.kt @@ -0,0 +1,39 @@ +package com.anytypeio.anytype.feature_object_type.ui + +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem +import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem +import com.anytypeio.anytype.presentation.objects.ObjectIcon + +fun createDummyFieldDraggableItem(isEditableField: Boolean = true): UiFieldsListItem.Item.Draggable { + return UiFieldsListItem.Item.Draggable( + id = "dummyId", + fieldKey = "dummyKey", + fieldTitle = "Field Title", + format = RelationFormat.OBJECT, + limitObjectTypes = listOf( + UiFieldObjectItem( + id = "dummyObjectId1", + key = "dummyKey1", + title = "Dummy Object Type 1", + icon = ObjectIcon.Empty.ObjectType, + ), + UiFieldObjectItem( + id = "dummyObjectId1", + key = "dummyKey1", + title = "Dummy Object Type 1", + icon = ObjectIcon.Empty.ObjectType, + + ), + UiFieldObjectItem( + id = "dummyObjectId1", + key = "dummyKey1", + title = "Dummy Object Type 1", + icon = ObjectIcon.Empty.ObjectType, + + ), + ), + isEditableField = isEditableField, + canDelete = true + ) +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/TypeScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/TypeScreen.kt new file mode 100644 index 0000000000..700eb4b159 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/TypeScreen.kt @@ -0,0 +1,462 @@ +package com.anytypeio.anytype.feature_object_type.ui + +import android.os.Build +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.multiplayer.P2PStatusUpdate +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncError +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncNetwork +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncStatus +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncUpdate +import com.anytypeio.anytype.core_models.primitives.TypeId +import com.anytypeio.anytype.core_models.primitives.TypeKey +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.extensions.swapList +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.lists.objects.ListItemLoading +import com.anytypeio.anytype.core_ui.lists.objects.ObjectsListItem +import com.anytypeio.anytype.core_ui.lists.objects.UiContentState +import com.anytypeio.anytype.core_ui.lists.objects.UiObjectsListState +import com.anytypeio.anytype.core_ui.syncstatus.SpaceSyncStatusScreen +import com.anytypeio.anytype.core_ui.views.ButtonSecondary +import com.anytypeio.anytype.core_ui.views.ButtonSize +import com.anytypeio.anytype.core_ui.views.Relations3 +import com.anytypeio.anytype.core_ui.views.Title2 +import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK +import com.anytypeio.anytype.feature_object_type.R +import com.anytypeio.anytype.feature_object_type.ui.alerts.DeleteAlertScreen +import com.anytypeio.anytype.feature_object_type.ui.header.HorizontalButtons +import com.anytypeio.anytype.feature_object_type.ui.header.IconAndTitleWidget +import com.anytypeio.anytype.feature_object_type.ui.header.TopToolbar +import com.anytypeio.anytype.feature_object_type.ui.layouts.TypeLayoutsScreen +import com.anytypeio.anytype.feature_object_type.ui.objects.ObjectsHeader +import com.anytypeio.anytype.feature_object_type.ui.templates.TemplatesScreen +import com.anytypeio.anytype.presentation.editor.cover.CoverColor +import com.anytypeio.anytype.presentation.objects.ObjectIcon +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem +import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState +import com.anytypeio.anytype.presentation.templates.TemplateView + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ObjectTypeMainScreen( + //top bar + uiEditButtonState: UiEditButton, + uiSyncStatusBadgeState: UiSyncStatusBadgeState, + uiSyncStatusState: SyncStatusWidgetState, + //header + uiIconState: UiIconState, + uiTitleState: UiTitleState, + //layout and fields buttons + uiFieldsButtonState: UiFieldsButtonState, + uiLayoutButtonState: UiLayoutButtonState, + uiLayoutTypeState: UiLayoutTypeState, + //templates header + uiTemplatesHeaderState: UiTemplatesHeaderState, + uiTemplatesAddIconState: UiTemplatesAddIconState, + //templates list + uiTemplatesListState: UiTemplatesListState, + //objects header + uiObjectsHeaderState: UiObjectsHeaderState, + uiObjectsAddIconState: UiObjectsAddIconState, + uiObjectsSettingsIconState: UiObjectsSettingsIconState, + uiObjectsMenuState: UiMenuState, + //objects list + uiObjectsListState: UiObjectsListState, + uiContentState: UiContentState, + //delete alert + uiDeleteAlertState: UiDeleteAlertState, + //events + onTypeEvent: (TypeEvent) -> Unit +) { + + val objects = remember { mutableStateListOf() } + objects.swapList(uiObjectsListState.items) + + val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( + state = rememberTopAppBarState() + ) + + Scaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection), + containerColor = colorResource(id = R.color.background_primary), + contentColor = colorResource(id = R.color.background_primary), + topBar = { + TopBarContent( + uiSyncStatusBadgeState = uiSyncStatusBadgeState, + uiEditButtonState = uiEditButtonState, + uiTitleState = uiTitleState, + topBarScrollBehavior = topAppBarScrollBehavior, + onTypeEvent = onTypeEvent + ) + }, + content = { paddingValues -> + MainContent( + paddingValues = paddingValues, + uiIconState = uiIconState, + uiTitleState = uiTitleState, + uiFieldsButtonState = uiFieldsButtonState, + uiLayoutButtonState = uiLayoutButtonState, + uiTemplatesHeaderState = uiTemplatesHeaderState, + uiTemplatesAddIconState = uiTemplatesAddIconState, + uiTemplatesListState = uiTemplatesListState, + uiObjectsHeaderState = uiObjectsHeaderState, + uiObjectsAddIconState = uiObjectsAddIconState, + uiObjectsSettingsIconState = uiObjectsSettingsIconState, + uiObjectsMenuState = uiObjectsMenuState, + objects = objects, + onTypeEvent = onTypeEvent + ) + } + ) + + BottomSyncStatus( + uiSyncStatusState = uiSyncStatusState, + onDismiss = { onTypeEvent(TypeEvent.OnSyncStatusDismiss) } + ) + + if (uiDeleteAlertState is UiDeleteAlertState.Show) { + DeleteAlertScreen( + onTypeEvent = onTypeEvent + ) + } + + if (uiLayoutTypeState is UiLayoutTypeState.Visible) { + TypeLayoutsScreen( + modifier = Modifier.fillMaxWidth(), + uiState = uiLayoutTypeState, + onTypeEvent = onTypeEvent + ) + } +} + +@Composable +private fun MainContent( + paddingValues: PaddingValues, + uiIconState: UiIconState, + uiTitleState: UiTitleState, + uiFieldsButtonState: UiFieldsButtonState, + uiLayoutButtonState: UiLayoutButtonState, + uiTemplatesHeaderState: UiTemplatesHeaderState, + uiTemplatesAddIconState: UiTemplatesAddIconState, + uiTemplatesListState: UiTemplatesListState, + uiObjectsHeaderState: UiObjectsHeaderState, + uiObjectsAddIconState: UiObjectsAddIconState, + uiObjectsSettingsIconState: UiObjectsSettingsIconState, + uiObjectsMenuState: UiMenuState, + objects: List, + onTypeEvent: (TypeEvent) -> Unit +) { + // Adjust content modifier based on SDK version for proper insets handling + val contentModifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) { + Modifier + .windowInsetsPadding(WindowInsets.navigationBars) + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()) + } else { + Modifier + .fillMaxSize() + .padding(paddingValues) + } + + LazyColumn(modifier = contentModifier) { + item { + IconAndTitleWidget( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 32.dp) + .padding(horizontal = 20.dp), + uiIconState = uiIconState, + uiTitleState = uiTitleState, + onTypeEvent = onTypeEvent + ) + Spacer(modifier = Modifier.height(20.dp)) + } + + item { + HorizontalButtons( + modifier = Modifier + .fillMaxWidth() + .height(36.dp) + .padding(horizontal = 20.dp), + uiFieldsButtonState = uiFieldsButtonState, + uiLayoutButtonState = uiLayoutButtonState, + onTypeEvent = onTypeEvent + ) + } + + if (uiTemplatesHeaderState is UiTemplatesHeaderState.Visible) { + item { + TemplatesScreen( + uiTemplatesHeaderState = uiTemplatesHeaderState, + uiTemplatesAddIconState = uiTemplatesAddIconState, + uiTemplatesListState = uiTemplatesListState, + onTypeEvent = onTypeEvent + ) + } + } + + item { + ObjectsHeader( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .padding(horizontal = 20.dp), + uiObjectsHeaderState = uiObjectsHeaderState, + uiObjectsAddIconState = uiObjectsAddIconState, + uiObjectsSettingsIconState = uiObjectsSettingsIconState, + uiObjectsMenuState = uiObjectsMenuState, + onTypeEvent = onTypeEvent + ) + } + + if (objects.isEmpty()) { + item { + EmptyScreen( + modifier = Modifier.padding(top = 18.dp) + ) + } + } else { + items( + count = objects.size, + key = { index -> objects[index].id }, + contentType = { index -> + when (objects[index]) { + is UiObjectsListItem.Loading -> "loading" + is UiObjectsListItem.Item -> "item" + } + } + ) { index -> + when (val item = objects[index]) { + is UiObjectsListItem.Item -> { + ObjectsListItem( + modifier = Modifier + .fillMaxWidth() + .animateItem() + .padding(horizontal = 4.dp) + .noRippleThrottledClickable { + onTypeEvent(TypeEvent.OnObjectItemClick(item)) + }, + item = item + ) + Divider(paddingStart = 20.dp, paddingEnd = 20.dp) + } + is UiObjectsListItem.Loading -> { + ListItemLoading(modifier = Modifier) + } + } + } + } + + // Objects menu actions + when (val itemSet = uiObjectsMenuState.objSetItem) { + UiMenuSetItem.CreateSet -> { + item { + ButtonSecondary( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, start = 20.dp, end = 20.dp), + text = stringResource(R.string.object_type_objects_menu_create_set), + size = ButtonSize.Large, + onClick = { onTypeEvent(TypeEvent.OnCreateSetClick) } + ) + } + } + is UiMenuSetItem.OpenSet -> { + item { + ButtonSecondary( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, start = 20.dp, end = 20.dp), + text = stringResource(R.string.object_type_objects_menu_open_set), + size = ButtonSize.Large, + onClick = { onTypeEvent(TypeEvent.OnOpenSetClick(setId = itemSet.setId)) } + ) + } + } + UiMenuSetItem.Hidden -> Unit + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBarContent( + uiSyncStatusBadgeState: UiSyncStatusBadgeState, + uiEditButtonState: UiEditButton, + uiTitleState: UiTitleState, + topBarScrollBehavior: TopAppBarScrollBehavior, + onTypeEvent: (TypeEvent) -> Unit +) { + // Use windowInsetsPadding if running on a recent SDK + val modifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) { + Modifier + .windowInsetsPadding(WindowInsets.statusBars) + .fillMaxWidth() + } else { + Modifier.fillMaxWidth() + } + + Column(modifier = modifier) { + TopToolbar( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + uiSyncStatusBadgeState = uiSyncStatusBadgeState, + uiEditButtonState = uiEditButtonState, + uiTitleState = uiTitleState, + onTypeEvent = onTypeEvent, + topBarScrollBehavior = topBarScrollBehavior + ) + } +} + +@Composable +private fun BottomSyncStatus( + uiSyncStatusState: SyncStatusWidgetState, + onDismiss: () -> Unit +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter + ) { + SpaceSyncStatusScreen( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .windowInsetsPadding(WindowInsets.navigationBars), + modifierCard = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 16.dp), + uiState = uiSyncStatusState, + onDismiss = onDismiss, + onUpdateAppClick = {} + ) + } +} + +@Composable +fun EmptyScreen(modifier: Modifier) { + Column(modifier = modifier) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + text = stringResource(R.string.object_type_empty_items_title), + color = colorResource(id = R.color.text_secondary), + style = Title2, + textAlign = TextAlign.Center + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + text = stringResource(R.string.object_type_empty_items_subtitle), + color = colorResource(id = R.color.text_secondary), + style = Relations3, + textAlign = TextAlign.Center + ) + } +} + +@DefaultPreviews +@Composable +fun ObjectTypeMainScreenPreview() { + val spaceSyncUpdate = SpaceSyncUpdate.Update( + id = "1", + status = SpaceSyncStatus.SYNCING, + network = SpaceSyncNetwork.ANYTYPE, + error = SpaceSyncError.NULL, + syncingObjectsCounter = 2 + ) + ObjectTypeMainScreen( + uiSyncStatusBadgeState = UiSyncStatusBadgeState.Visible( + status = SpaceSyncAndP2PStatusState.Success( + spaceSyncUpdate = spaceSyncUpdate, + p2PStatusUpdate = P2PStatusUpdate.Initial + ) + ), + uiSyncStatusState = SyncStatusWidgetState.Hidden, + uiIconState = UiIconState(icon = ObjectIcon.Empty.Page, isEditable = true), + uiTitleState = UiTitleState(title = "title", isEditable = true), + uiFieldsButtonState = UiFieldsButtonState.Visible(4), + uiLayoutButtonState = UiLayoutButtonState.Visible(layout = ObjectType.Layout.VIDEO), + uiTemplatesHeaderState = UiTemplatesHeaderState.Visible(count = "3"), + uiTemplatesAddIconState = UiTemplatesAddIconState.Visible, + uiTemplatesListState = UiTemplatesListState( + items = listOf( + TemplateView.Template( + id = "1", + name = "Template 1", + targetTypeId = TypeId("page"), + targetTypeKey = TypeKey("ot-page"), + layout = ObjectType.Layout.BASIC, + image = null, + emoji = ":)", + coverColor = CoverColor.RED, + coverGradient = null, + coverImage = null, + ), + TemplateView.Template( + id = "2", + name = "Template 2", + targetTypeId = TypeId("note"), + targetTypeKey = TypeKey("ot-note"), + layout = ObjectType.Layout.NOTE, + image = null, + emoji = null, + coverColor = null, + coverGradient = null, + coverImage = null, + ), + TemplateView.New( + targetTypeId = TypeId("32423"), + targetTypeKey = TypeKey("43232") + ) + ) + ), + uiObjectsAddIconState = UiObjectsAddIconState.Visible, + uiObjectsHeaderState = UiObjectsHeaderState(count = "3"), + uiObjectsSettingsIconState = UiObjectsSettingsIconState.Visible, + uiObjectsListState = UiObjectsListState(emptyList()), + uiContentState = UiContentState.Idle(), + uiObjectsMenuState = UiMenuState.EMPTY, + uiDeleteAlertState = UiDeleteAlertState.Hidden, + uiEditButtonState = UiEditButton.Visible, + uiLayoutTypeState = UiLayoutTypeState.Hidden, + onTypeEvent = {} + ) +} + diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiEvent.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiEvent.kt new file mode 100644 index 0000000000..5a8bbbf98c --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiEvent.kt @@ -0,0 +1,62 @@ +package com.anytypeio.anytype.feature_object_type.ui + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType.Layout +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState +import com.anytypeio.anytype.presentation.objects.ObjectsListSort +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem +import com.anytypeio.anytype.presentation.templates.TemplateView + +sealed class TypeEvent { + + //region TopBar + data class OnSyncStatusClick(val status: SpaceSyncAndP2PStatusState) : TypeEvent() + data object OnSyncStatusDismiss : TypeEvent() + data object OnMenuItemDeleteClick : TypeEvent() + data object OnAlertDeleteDismiss : TypeEvent() + data object OnAlertDeleteConfirm : TypeEvent() + data object OnBackClick : TypeEvent() + //endregion + + //region Object Type Header + data object OnObjectTypeIconClick : TypeEvent() + data class OnObjectTypeTitleUpdate(val title: String) : TypeEvent() + //endregion + + //region Sets + data object OnCreateSetClick : TypeEvent() + data class OnOpenSetClick(val setId: Id) : TypeEvent() + //endregion + + //region Objects Header + data class OnSortClick(val sort: ObjectsListSort) : TypeEvent() + data object OnCreateObjectIconClick : TypeEvent() + //endregion + + //region Objects list + data class OnObjectItemClick(val item: UiObjectsListItem) : TypeEvent() + //endregion + + //region Templates + data object OnTemplatesAddIconClick : TypeEvent() + data class OnTemplateItemClick(val item: TemplateView) : TypeEvent() + sealed class OnTemplateMenuClick : TypeEvent() { + data class SetAsDefault(val item: TemplateView) : OnTemplateMenuClick() + data class Edit(val item: TemplateView) : OnTemplateMenuClick() + data class Duplicate(val item: TemplateView) : OnTemplateMenuClick() + data class Delete(val item: TemplateView) : OnTemplateMenuClick() + } + //endregion + + //region Layout type + data object OnLayoutTypeDismiss : TypeEvent() + data class OnLayoutTypeItemClick(val item: Layout) : TypeEvent() + + //endregion + + data object OnLayoutButtonClick : TypeEvent() + data object OnFieldsButtonClick : TypeEvent() + + data object OnObjectsSettingsIconClick : TypeEvent() + +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiState.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiState.kt new file mode 100644 index 0000000000..c8f5e88fad --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiState.kt @@ -0,0 +1,211 @@ +package com.anytypeio.anytype.feature_object_type.ui + +import androidx.compose.runtime.Immutable +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.presentation.objects.MenuSortsItem +import com.anytypeio.anytype.presentation.objects.ObjectIcon +import com.anytypeio.anytype.presentation.objects.ObjectsListSort +import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState +import com.anytypeio.anytype.presentation.templates.TemplateView + +data class ObjectTypeVmParams( + val objectId: Id, + val spaceId: SpaceId, + val withSubscriptions: Boolean, + val showHiddenFields: Boolean +) + +sealed class ObjectTypeCommand { + + data object Back : ObjectTypeCommand() + + data class OpenTemplate( + val templateId: Id, + val typeId: Id, + val typeKey: Key, + val spaceId: Id + ) : ObjectTypeCommand() + + data object OpenEmojiPicker : ObjectTypeCommand() + + data object OpenFieldsScreen : ObjectTypeCommand() + + data class OpenAddFieldScreen(val typeId: Id, val space: Id, val isSet: Boolean = false) : ObjectTypeCommand() +} + +//region OBJECT TYPE HEADER (title + icon) +data class UiTitleState(val title: String, val isEditable: Boolean) { + companion object { + val EMPTY = UiTitleState(title = "", isEditable = false) + } +} + +data class UiIconState(val icon: ObjectIcon, val isEditable: Boolean) { + companion object { + val EMPTY = UiIconState(icon = ObjectIcon.None, isEditable = false) + } +} +//endregion + +//region LAYOUTS +sealed class UiLayoutButtonState { + data object Hidden : UiLayoutButtonState() + data class Visible(val layout: ObjectType.Layout) : UiLayoutButtonState() +} + +sealed class UiLayoutTypeState { + data object Hidden : UiLayoutTypeState() + data class Visible( + val layouts: List, + val selectedLayout: ObjectType.Layout? = null + ) : UiLayoutTypeState() +} +//endregion + +sealed class UiFieldsButtonState { + data object Hidden : UiFieldsButtonState() + data class Visible(val count: Int) : UiFieldsButtonState() + +} + +//region MENU +@Immutable +sealed class UiMenuSetItem { + data object Hidden : UiMenuSetItem() + data object CreateSet : UiMenuSetItem() + + @Immutable + data class OpenSet(val setId: Id) : UiMenuSetItem() +} + +data class UiMenuState( + val container: MenuSortsItem.Container, + val sorts: List, + val types: List, + val objSetItem: UiMenuSetItem +) { + companion object { + val EMPTY = UiMenuState( + container = MenuSortsItem.Container(sort = ObjectsListSort.ByName()), + sorts = emptyList(), + types = emptyList(), + objSetItem = UiMenuSetItem.Hidden + ) + } +} + +@Immutable +sealed class UiSettingsMenuState { + data object Hidden : UiSettingsMenuState() + + @Immutable + data class Visible( + val menuItems: List + ) : UiSettingsMenuState() +} + +@Immutable +sealed class UiTemplatesMenuState { + data object Hidden : UiTemplatesMenuState() + + @Immutable + data class Visible( + val menuItems: List + ) : UiTemplatesMenuState() +} + +@Immutable +enum class UiSettingsMenuItem { + DELETE +} + +@Immutable +enum class UiTemplatesMenuItem { + DELETE, DUPLICATE +} + +@Immutable +enum class UiObjectsMenuItem { + OPEN_SET, SORT_BY, +} +//endregion + +//region TEMPLATES HEADER +sealed class UiTemplatesHeaderState { + data object Hidden : UiTemplatesHeaderState() + data class Visible(val count: String) : UiTemplatesHeaderState() +} + +sealed class UiTemplatesAddIconState { + data object Hidden : UiTemplatesAddIconState() + data object Visible : UiTemplatesAddIconState() +} +//endregion + +//region TEMPLATES LIST +data class UiTemplatesListState( + val items: List +) { + companion object { + val EMPTY = UiTemplatesListState(items = emptyList()) + } +} +//endregion + +//region OBJECTS HEADER +data class UiObjectsHeaderState(val count: String) { + companion object { + val EMPTY = UiObjectsHeaderState(count = "") + } +} + +sealed class UiObjectsAddIconState { + data object Hidden : UiObjectsAddIconState() + data object Visible : UiObjectsAddIconState() +} + +sealed class UiObjectsSettingsIconState { + data object Hidden : UiObjectsSettingsIconState() + data object Visible : UiObjectsSettingsIconState() +} +//endregion + +sealed class UiEditButton { + data object Hidden : UiEditButton() + data object Visible : UiEditButton() +} + +//region ERRORS +sealed class UiErrorState { + data object Hidden : UiErrorState() + data class Show(val reason: Reason) : UiErrorState() + + sealed class Reason { + data class ErrorGettingObjects(val msg: String) : Reason() + data class Other(val msg: String) : Reason() + } +} +//endregion + +//region ALERTS +sealed class UiDeleteAlertState { + data object Hidden : UiDeleteAlertState() + data object Show : UiDeleteAlertState() +} +//endregion + +//region SYNC STATUS +sealed class UiSyncStatusWidgetState { + data object Hidden : UiSyncStatusWidgetState() + data class Visible(val status: SyncStatusWidgetState) : UiSyncStatusWidgetState() +} + +sealed class UiSyncStatusBadgeState { + data object Hidden : UiSyncStatusBadgeState() + data class Visible(val status: SpaceSyncAndP2PStatusState) : UiSyncStatusBadgeState() +} +//endregion \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiStateExt.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiStateExt.kt new file mode 100644 index 0000000000..9b9311523f --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/UiStateExt.kt @@ -0,0 +1,254 @@ +package com.anytypeio.anytype.feature_object_type.ui + +import com.anytypeio.anytype.core_models.CoverType +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.RelationFormat +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.primitives.TypeId +import com.anytypeio.anytype.core_models.primitives.TypeKey +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelations +import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.resources.StringResourceProvider +import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem +import com.anytypeio.anytype.feature_object_type.fields.UiFieldObjectItem +import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem +import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Item +import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem.Section +import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider +import com.anytypeio.anytype.presentation.mapper.objectIcon +import com.anytypeio.anytype.presentation.relations.BasicObjectCoverWrapper +import com.anytypeio.anytype.presentation.relations.getCover +import com.anytypeio.anytype.presentation.templates.TemplateView + +//region Mapping +fun ObjectWrapper.Basic.toTemplateView( + objType: ObjectWrapper.Type, + urlBuilder: UrlBuilder, + coverImageHashProvider: CoverImageHashProvider, +): TemplateView.Template { + val coverContainer = if (coverType != CoverType.NONE) { + BasicObjectCoverWrapper(this) + .getCover(urlBuilder, coverImageHashProvider) + } else { + null + } + return TemplateView.Template( + id = id, + name = name.orEmpty(), + targetTypeId = TypeId(targetObjectType.orEmpty()), + emoji = if (!iconEmoji.isNullOrBlank()) iconEmoji else null, + image = iconImage?.takeIf { it.isNotBlank() }?.let { urlBuilder.thumbnail(it) }, + layout = objType.recommendedLayout ?: ObjectType.Layout.BASIC, + coverColor = coverContainer?.coverColor, + coverImage = coverContainer?.coverImage, + coverGradient = coverContainer?.coverGradient, + isDefault = false, + targetTypeKey = TypeKey(objType.uniqueKey) + ) +} +//endregion + +/** + * Extension function to safely get a name for the relation. + * If the name is blank, returns a default untitled title. + */ +private fun ObjectWrapper.Relation.getName(stringResourceProvider: StringResourceProvider): String = + if (name.isNullOrBlank()) { + stringResourceProvider.getUntitledObjectTitle() + } else { + name!! + } + +suspend fun buildUiFieldsList( + objType: ObjectWrapper.Type, + stringResourceProvider: StringResourceProvider, + fieldParser: FieldParser, + urlBuilder: UrlBuilder, + storeOfObjectTypes: StoreOfObjectTypes, + storeOfRelations: StoreOfRelations, + objTypeConflictingFields: List, + showHiddenFields: Boolean +): List { + + val parsedFields = fieldParser.getObjectTypeParsedFields( + objectType = objType, + storeOfRelations = storeOfRelations, + objectTypeConflictingFieldsIds = objTypeConflictingFields + ) + + // The mapping functions already skip the Relations.DESCRIPTION key. + val headerItems = parsedFields.header.mapNotNull { + mapToUiFieldsDraggableListItem( + field = it, + stringResourceProvider = stringResourceProvider, + fieldParser = fieldParser, + urlBuilder = urlBuilder, + storeOfObjectTypes = storeOfObjectTypes + ) + } + val sidebarItems = parsedFields.sidebar.mapNotNull { + mapToUiFieldsDraggableListItem( + field = it, + stringResourceProvider = stringResourceProvider, + fieldParser = fieldParser, + urlBuilder = urlBuilder, + storeOfObjectTypes = storeOfObjectTypes + ) + } + val hiddenItems = parsedFields.hidden.mapNotNull { + mapToUiFieldsDraggableListItem( + field = it, + stringResourceProvider = stringResourceProvider, + fieldParser = fieldParser, + urlBuilder = urlBuilder, + storeOfObjectTypes = storeOfObjectTypes + ) + } + val conflictedItems = parsedFields.localWithoutSystem.mapNotNull { + mapToUiFieldsLocalListItem( + field = it, + stringResourceProvider = stringResourceProvider, + fieldParser = fieldParser, + urlBuilder = urlBuilder, + storeOfObjectTypes = storeOfObjectTypes + ) + } + + //this items goes to the Hidden section as draggable items + val conflictedSystemItems = parsedFields.localSystem.mapNotNull { + mapToUiFieldsDraggableListItem( + field = it, + stringResourceProvider = stringResourceProvider, + fieldParser = fieldParser, + urlBuilder = urlBuilder, + storeOfObjectTypes = storeOfObjectTypes + ) + } + + val fileRecommendedFields = parsedFields.file.mapNotNull { + mapToUiFieldsDraggableListItem( + field = it, + stringResourceProvider = stringResourceProvider, + fieldParser = fieldParser, + urlBuilder = urlBuilder, + storeOfObjectTypes = storeOfObjectTypes + ) + } + + return buildList { + add(Section.Header(canAdd = true)) + addAll(headerItems) + + add(Section.SideBar(canAdd = true)) + addAll(sidebarItems) + + //todo file fields are off for now +// if (fileRecommendedFields.isNotEmpty()) { +// add(Section.File(canAdd = false)) +// addAll(fileRecommendedFields) +// } + + if (showHiddenFields) { + add(Section.Hidden(canAdd = false)) + addAll(hiddenItems) + addAll(conflictedSystemItems) + } + + if (conflictedItems.isNotEmpty()) { + add(Section.Local(canAdd = false)) + addAll(conflictedItems) + } + } +} + +/** + * Shared helper to build the limit object types for a field. + */ +private suspend fun mapLimitObjectTypes( + relation: ObjectWrapper.Relation, + storeOfObjectTypes: StoreOfObjectTypes, + fieldParser: FieldParser, + urlBuilder: UrlBuilder +): List { + return if (relation.format == RelationFormat.OBJECT && relation.relationFormatObjectTypes.isNotEmpty()) { + relation.relationFormatObjectTypes.mapNotNull { key -> + storeOfObjectTypes.getByKey(key)?.let { objType -> + UiFieldObjectItem( + id = objType.id, + key = objType.uniqueKey, + title = fieldParser.getObjectName(objType), + icon = objType.objectIcon(urlBuilder) + ) + } + } + } else { + emptyList() + } +} + +/** + * Maps a field to a draggable UI list item. + * Returns null if the field key equals DESCRIPTION. + */ +private suspend fun mapToUiFieldsDraggableListItem( + field: ObjectWrapper.Relation, + stringResourceProvider: StringResourceProvider, + storeOfObjectTypes: StoreOfObjectTypes, + fieldParser: FieldParser, + urlBuilder: UrlBuilder +): UiFieldsListItem? { + if (field.key == Relations.DESCRIPTION) return null + + val limitObjectTypes = mapLimitObjectTypes(field, storeOfObjectTypes, fieldParser, urlBuilder) + return Item.Draggable( + id = field.id, + fieldKey = field.key, + fieldTitle = field.getName(stringResourceProvider), + format = field.format, + limitObjectTypes = limitObjectTypes, + isEditableField = fieldParser.isFieldEditable(field), + canDelete = fieldParser.isFieldCanBeDeletedFromType(field) + ) +} + +/** + * Maps a field to a local UI list item. + * Returns null if the field key equals DESCRIPTION. + */ +private suspend fun mapToUiFieldsLocalListItem( + field: ObjectWrapper.Relation, + stringResourceProvider: StringResourceProvider, + storeOfObjectTypes: StoreOfObjectTypes, + fieldParser: FieldParser, + urlBuilder: UrlBuilder +): UiFieldsListItem? { + if (field.key == Relations.DESCRIPTION) return null + + val limitObjectTypes = mapLimitObjectTypes(field, storeOfObjectTypes, fieldParser, urlBuilder) + return Item.Local( + id = field.id, + fieldKey = field.key, + fieldTitle = field.getName(stringResourceProvider), + format = field.format, + limitObjectTypes = limitObjectTypes, + isEditableField = fieldParser.isFieldEditable(field) + ) +} + +fun ObjectWrapper.Relation.mapToUiAddFieldListItem( + stringResourceProvider: StringResourceProvider +): UiAddFieldItem? { + val field = this + if (field.key == Relations.DESCRIPTION) return null + return UiAddFieldItem( + id = field.id, + fieldKey = field.key, + fieldTitle = field.getName(stringResourceProvider), + format = field.format + ) +} + diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/alerts/AlertsScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/alerts/AlertsScreen.kt new file mode 100644 index 0000000000..6a0f9329c2 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/alerts/AlertsScreen.kt @@ -0,0 +1,80 @@ +package com.anytypeio.anytype.feature_object_type.ui.alerts + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.foundation.AlertConfig +import com.anytypeio.anytype.core_ui.foundation.BUTTON_PRIMARY +import com.anytypeio.anytype.core_ui.foundation.BUTTON_SECONDARY +import com.anytypeio.anytype.core_ui.foundation.Dragger +import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED +import com.anytypeio.anytype.core_ui.foundation.GenericAlert +import com.anytypeio.anytype.feature_object_type.R +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeleteAlertScreen( + onTypeEvent: (TypeEvent) -> Unit +) { + + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + ModalBottomSheet( + dragHandle = { + Column { + Spacer(modifier = Modifier.height(6.dp)) + Dragger() + Spacer(modifier = Modifier.height(6.dp)) + } + }, + scrimColor = colorResource(id = R.color.modal_screen_outside_background), + containerColor = colorResource(id = R.color.background_secondary), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + sheetState = bottomSheetState, + onDismissRequest = { + onTypeEvent(TypeEvent.OnAlertDeleteDismiss) + }, + content = { + GenericAlert( + config = AlertConfig.WithTwoButtons( + title = stringResource(R.string.are_you_sure_delete_one_object), + description = stringResource(R.string.delete_irrevocably_one_object), + firstButtonText = stringResource(R.string.cancel), + secondButtonText = stringResource(R.string.delete), + icon = AlertConfig.Icon( + GRADIENT_TYPE_RED, + icon = R.drawable.ic_alert_error + ), + firstButtonType = BUTTON_SECONDARY, + secondButtonType = BUTTON_PRIMARY, + ), + onFirstButtonClicked = { + onTypeEvent(TypeEvent.OnAlertDeleteDismiss) + }, + onSecondButtonClicked = { + onTypeEvent(TypeEvent.OnAlertDeleteConfirm) + } + ) + } + ) +} + +@DefaultPreviews +@Composable +fun DeleteAlertScreenPreview() { + DeleteAlertScreen( + onTypeEvent = {} + ) +} diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/HeaderScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/HeaderScreen.kt new file mode 100644 index 0000000000..b51fa2adfd --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/HeaderScreen.kt @@ -0,0 +1,163 @@ +package com.anytypeio.anytype.feature_object_type.ui.header + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.HeadlineTitle +import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent +import com.anytypeio.anytype.feature_object_type.ui.UiIconState +import com.anytypeio.anytype.feature_object_type.ui.UiTitleState +import com.anytypeio.anytype.presentation.objects.ObjectIcon + + +@Composable +fun IconAndTitleWidget( + modifier: Modifier, + uiIconState: UiIconState, + uiTitleState: UiTitleState, + onTypeEvent: (TypeEvent) -> Unit +) { + Row(modifier = modifier) { + ListWidgetObjectIcon( + modifier = Modifier + .size(32.dp) + .noRippleThrottledClickable { + if (uiIconState.isEditable) { + onTypeEvent.invoke(TypeEvent.OnObjectTypeIconClick) + } + }, + icon = uiIconState.icon, + backgroundColor = R.color.amp_transparent + ) + NameField( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + uiTitleState = uiTitleState, + onTypeEvent = onTypeEvent, + ) + } +} + +@Composable +fun NameField( + modifier: Modifier, + uiTitleState: UiTitleState, + onTypeEvent: (TypeEvent) -> Unit +) { + var innerValue by remember(uiTitleState.title) { mutableStateOf(uiTitleState.title) } + val focusManager = LocalFocusManager.current + + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + BasicTextField( + value = innerValue, + onValueChange = { + innerValue = it + onTypeEvent.invoke( + TypeEvent.OnObjectTypeTitleUpdate( + title = innerValue + ) + ) + }, + textStyle = HeadlineTitle.copy(color = colorResource(id = R.color.text_primary)), + singleLine = false, + enabled = uiTitleState.isEditable, + cursorBrush = SolidColor(colorResource(id = R.color.text_primary)), + modifier = modifier + .padding(start = 12.dp, end = 20.dp) + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions { + keyboardController?.hide() + focusManager.clearFocus() + onTypeEvent.invoke( + TypeEvent.OnObjectTypeTitleUpdate( + title = innerValue + ) + ) + }, + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .fillMaxWidth() + .height(32.dp), + contentAlignment = Alignment.CenterStart + ) { + if (innerValue.isEmpty()) { + Text( + modifier = Modifier.wrapContentSize(), + text = stringResource(id = R.string.untitled), + style = HeadlineTitle, + color = colorResource(id = R.color.text_tertiary), + ) + } + } + innerTextField() + } + ) +} + +@DefaultPreviews +@Composable +fun IconAndTitleWidgetPreview() { + IconAndTitleWidget( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + onTypeEvent = {}, + uiIconState = UiIconState(icon = ObjectIcon.Task(isChecked = false), isEditable = true), + uiTitleState = UiTitleState( + title = "I understand that contributing to this repository will require me to agree with the", + isEditable = true + ) + ) +} + +@DefaultPreviews +@Composable +fun IconAndTitleEmptyWidgetPreview() { + IconAndTitleWidget( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + onTypeEvent = {}, + uiIconState = UiIconState(icon = ObjectIcon.Task(isChecked = false), isEditable = true), + uiTitleState = UiTitleState( + title = "", + isEditable = true + ) + ) +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/HorizontalButtons.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/HorizontalButtons.kt new file mode 100644 index 0000000000..df5f6bf2ad --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/HorizontalButtons.kt @@ -0,0 +1,117 @@ +package com.anytypeio.anytype.feature_object_type.ui.header + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent +import com.anytypeio.anytype.feature_object_type.ui.UiFieldsButtonState +import com.anytypeio.anytype.feature_object_type.ui.UiLayoutButtonState + +@Composable +fun HorizontalButtons( + modifier: Modifier, + uiLayoutButtonState: UiLayoutButtonState, + uiFieldsButtonState: UiFieldsButtonState, + onTypeEvent: (TypeEvent) -> Unit +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + val modifierButton = Modifier + .height(40.dp) + .wrapContentWidth() + .border( + width = 1.dp, + color = colorResource(R.color.shape_primary), + shape = RoundedCornerShape(size = 8.dp) + ) + + if (uiLayoutButtonState is UiLayoutButtonState.Visible) { + Row( + modifier = modifierButton.noRippleThrottledClickable { + onTypeEvent(TypeEvent.OnLayoutButtonClick) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .wrapContentSize() + .padding(start = 12.dp), + text = stringResource(R.string.button_layout), + style = PreviewTitle2Medium, + color = colorResource(R.color.text_primary) + ) + Text( + modifier = Modifier + .wrapContentSize() + .padding(start = 6.dp, end = 12.dp), + text = uiLayoutButtonState.layout.name.substring(0, 1).uppercase() + + uiLayoutButtonState.layout.name.substring(1) + .toLowerCase(Locale.current), + style = PreviewTitle2Medium, + color = colorResource(R.color.glyph_active) + ) + } + } + if (uiFieldsButtonState is UiFieldsButtonState.Visible) { + Row( + modifier = modifierButton.noRippleThrottledClickable { + onTypeEvent(TypeEvent.OnFieldsButtonClick) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .wrapContentSize() + .padding(start = 12.dp), + text = stringResource(R.string.button_fields), + style = PreviewTitle2Medium, + color = colorResource(R.color.text_primary) + ) + Text( + modifier = Modifier + .wrapContentSize() + .padding(start = 6.dp, end = 12.dp), + text = uiFieldsButtonState.count.toString(), + style = PreviewTitle2Medium, + color = colorResource(R.color.glyph_active) + ) + } + } + } +} + +@DefaultPreviews +@Composable +fun HorizontalButtonsPreview() { + HorizontalButtons( + modifier = Modifier + .height(32.dp) + .fillMaxWidth() + .padding(start = 20.dp), + uiLayoutButtonState = UiLayoutButtonState.Visible(ObjectType.Layout.BASIC), + uiFieldsButtonState = UiFieldsButtonState.Visible(3), + onTypeEvent = {} + ) +} diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/TopToolbar.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/TopToolbar.kt new file mode 100644 index 0000000000..b7399c85e7 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/header/TopToolbar.kt @@ -0,0 +1,196 @@ +package com.anytypeio.anytype.feature_object_type.ui.header + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.multiplayer.P2PStatusUpdate +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncAndP2PStatusState +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncError +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncNetwork +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncStatus +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncUpdate +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.syncstatus.StatusBadge +import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular +import com.anytypeio.anytype.feature_object_type.R +import com.anytypeio.anytype.feature_object_type.ui.UiEditButton +import com.anytypeio.anytype.feature_object_type.ui.UiSyncStatusBadgeState +import com.anytypeio.anytype.feature_object_type.ui.UiTitleState +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopToolbar( + modifier: Modifier, + uiEditButtonState: UiEditButton, + uiSyncStatusBadgeState: UiSyncStatusBadgeState, + topBarScrollBehavior: TopAppBarScrollBehavior, + uiTitleState: UiTitleState, + onTypeEvent: (TypeEvent) -> Unit +) { + val isIconMenuExpanded = remember { + mutableStateOf(false) + } + + CenterAlignedTopAppBar( + modifier = modifier.fillMaxWidth(), + expandedHeight = 48.dp, + scrollBehavior = topBarScrollBehavior, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = colorResource(id = R.color.background_primary), + scrolledContainerColor = colorResource(id = R.color.background_primary), + titleContentColor = colorResource(id = R.color.palette_system_red) + ), + title = { + if (topBarScrollBehavior.state.overlappedFraction > 0.7f) { + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.Center + ) { + Text( + text = uiTitleState.title, + style = PreviewTitle2Regular, + color = colorResource(R.color.text_primary), + textAlign = TextAlign.Center + ) + } + } + }, + navigationIcon = { + Box( + modifier = Modifier + .width(56.dp) + .height(48.dp) + .noRippleThrottledClickable { + onTypeEvent(TypeEvent.OnBackClick) + }, + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.wrapContentSize(), + painter = painterResource(R.drawable.ic_default_top_back), + contentDescription = stringResource(R.string.content_desc_back_button) + ) + } + }, + actions = { + if (uiSyncStatusBadgeState is UiSyncStatusBadgeState.Visible) { + Box( + modifier = Modifier + .size(48.dp) + .noRippleThrottledClickable { + onTypeEvent( + TypeEvent.OnSyncStatusClick( + status = uiSyncStatusBadgeState.status + ) + ) + }, + ) { + StatusBadge( + status = uiSyncStatusBadgeState.status, + modifier = Modifier + .align(Alignment.Center) + ) + } + } + if (uiEditButtonState is UiEditButton.Visible) { + IconButton( + modifier = Modifier + .size(48.dp), + onClick = { + isIconMenuExpanded.value = !isIconMenuExpanded.value + } + ) { + Image( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.ic_space_list_dots), + contentDescription = "More options" + ) + DropdownMenu( + modifier = Modifier + .width(244.dp), + expanded = isIconMenuExpanded.value, + offset = DpOffset(x = 0.dp, y = 0.dp), + onDismissRequest = { + isIconMenuExpanded.value = false + }, + shape = RoundedCornerShape(10.dp), + containerColor = colorResource(id = R.color.background_secondary), + ) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.object_type_settings_item_remove), + style = BodyRegular, + color = colorResource(id = R.color.palette_system_red) + ) + }, + onClick = { + onTypeEvent(TypeEvent.OnMenuItemDeleteClick) + isIconMenuExpanded.value = false + }, + ) + } + } + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@DefaultPreviews +fun TopToolbarPreview() { + val topAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior( + state = rememberTopAppBarState() + ) + val spaceSyncUpdate = SpaceSyncUpdate.Update( + id = "1", + status = SpaceSyncStatus.SYNCING, + network = SpaceSyncNetwork.ANYTYPE, + error = SpaceSyncError.NULL, + syncingObjectsCounter = 2 + ) + TopToolbar( + modifier = Modifier.fillMaxWidth(), + uiSyncStatusBadgeState = UiSyncStatusBadgeState.Visible( + status = SpaceSyncAndP2PStatusState.Success( + spaceSyncUpdate = spaceSyncUpdate, + p2PStatusUpdate = P2PStatusUpdate.Initial + ) + ), + uiEditButtonState = UiEditButton.Visible, + onTypeEvent = {}, + topBarScrollBehavior = topAppBarScrollBehavior, + uiTitleState = UiTitleState(title = "Page", isEditable = true) + ) +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/layouts/LayoutsScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/layouts/LayoutsScreen.kt new file mode 100644 index 0000000000..a7cd2e4d9a --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/layouts/LayoutsScreen.kt @@ -0,0 +1,341 @@ +package com.anytypeio.anytype.feature_object_type.ui.layouts + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.toLowerCase +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.foundation.Dragger +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.AvatarTitle +import com.anytypeio.anytype.core_ui.views.Title1 +import com.anytypeio.anytype.core_ui.views.fontInterRegular +import com.anytypeio.anytype.feature_object_type.R +import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TypeLayoutsScreen( + modifier: Modifier, + uiState: UiLayoutTypeState.Visible, + onTypeEvent: (TypeEvent) -> Unit +) { + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + + ModalBottomSheet( + modifier = modifier, + dragHandle = { + Column { + Spacer(modifier = Modifier.height(6.dp)) + Dragger() + Spacer(modifier = Modifier.height(6.dp)) + } + }, + scrimColor = colorResource(id = R.color.modal_screen_outside_background), + containerColor = colorResource(id = R.color.background_secondary), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + sheetState = bottomSheetState, + onDismissRequest = { + onTypeEvent(TypeEvent.OnLayoutTypeDismiss) + } + ) { + Spacer(modifier = Modifier.height(12.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = stringResource(id = R.string.layout_type), + style = Title1, + color = colorResource(id = R.color.text_primary) + ) + Spacer(modifier = Modifier.height(20.dp)) + LazyRow( + modifier = Modifier + .fillMaxWidth() + .height(252.dp), + contentPadding = PaddingValues(start = 20.dp, end = 20.dp), + horizontalArrangement = Arrangement.spacedBy( + space = 12.dp, + alignment = Alignment.End + ), + ) { + items( + count = uiState.layouts.size, + key = { index -> uiState.layouts[index].code }, + itemContent = { + val item = uiState.layouts[it] + val (borderWidth, borderColor) = if (item.code == uiState.selectedLayout?.code) { + 2.dp to colorResource(id = R.color.palette_system_amber_50) + } else { + 1.dp to colorResource(id = R.color.shape_secondary) + } + Column( + modifier = Modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + TemplateItemContent( + modifier = Modifier + .width(120.dp) + .height(224.dp) + .border( + width = borderWidth, + color = borderColor, + shape = RoundedCornerShape(12.dp) + ) + .padding(horizontal = 16.dp) + .noRippleThrottledClickable{ + onTypeEvent(TypeEvent.OnLayoutTypeItemClick(item)) + onTypeEvent(TypeEvent.OnLayoutTypeDismiss) + }, + item = item + ) + Text( + modifier = Modifier.padding(top = 4.dp), + text = item.name.substring(0, 1).uppercase() + + item.name.substring(1) + .toLowerCase(Locale.current), + style = TextStyle( + fontFamily = fontInterRegular, + fontWeight = FontWeight.W500, + fontSize = 13.sp, + letterSpacing = (-0.024).em + ), + color = colorResource(id = R.color.text_primary) + ) + Spacer(modifier = Modifier.height(32.dp)) + } + } + ) + } + } +} + +@Composable +private fun TemplateItemContent( + modifier: Modifier, + item: ObjectType.Layout +) { + when (item) { + ObjectType.Layout.BASIC -> { + Column( + modifier = modifier + ) { + Spacer(modifier = Modifier.height(28.dp)) + Box( + modifier = Modifier + .size(40.dp) + .background( + shape = RoundedCornerShape(5.dp), + color = colorResource(id = R.color.shape_tertiary) + ), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.wrapContentSize(), + painter = painterResource(R.drawable.ic_type_layout_basic_icon), + contentDescription = "Basic layout icon" + ) + } + Spacer(modifier = Modifier.height(14.dp)) + Box( + modifier = Modifier + .width(64.dp) + .height(8.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + Spacer(modifier = Modifier.height(8.dp)) + Box( + modifier = Modifier + .width(24.dp) + .height(4.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + Spacer(modifier = Modifier.height(16.dp)) + BasicBlocks() + } + } + + ObjectType.Layout.PROFILE -> { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(28.dp)) + Box( + modifier = Modifier + .size(40.dp) + .background( + shape = CircleShape, + color = colorResource(id = R.color.shape_tertiary) + ), + contentAlignment = Alignment.Center + ) { + Text( + modifier = Modifier.wrapContentSize(), + text = "N", + style = AvatarTitle.copy( + fontSize = 24.sp + ), + color = colorResource(id = R.color.glyph_active), + ) + } + Spacer(modifier = Modifier.height(10.dp)) + Box( + modifier = Modifier + .width(64.dp) + .height(8.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + Spacer(modifier = Modifier.height(8.dp)) + Box( + modifier = Modifier + .width(24.dp) + .height(4.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + Spacer(modifier = Modifier.height(16.dp)) + BasicBlocks() + } + } + + ObjectType.Layout.TODO -> { + Column( + modifier = modifier + ) { + Spacer(modifier = Modifier.height(40.dp)) + Image( + modifier = Modifier.wrapContentSize(), + painter = painterResource(R.drawable.ic_type_layout_todo_icon), + contentDescription = "Todo layout icon" + ) + Spacer(modifier = Modifier.height(8.dp)) + Box( + modifier = Modifier + .width(48.dp) + .height(4.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + Spacer(modifier = Modifier.height(16.dp)) + BasicBlocks() + } + } + + ObjectType.Layout.NOTE -> { + Column( + modifier = modifier + ) { + Spacer(modifier = Modifier.height(46.dp)) + Box( + modifier = Modifier + .width(24.dp) + .height(4.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + Spacer(modifier = Modifier.height(16.dp)) + BasicBlocks() + } + } + + else -> { + //do nothing + } + } +} + +@Composable +private fun ColumnScope.BasicBlocks() { + repeat(3) { + Box( + modifier = Modifier + .width(88.dp) + .height(6.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) + Spacer(modifier = Modifier.height(6.dp)) + } + Box( + modifier = Modifier + .width(64.dp) + .height(6.dp) + .background( + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 1.dp) + ) + ) +} + +@DefaultPreviews +@Composable +fun TypeLayoutsScreenPreview() { + TypeLayoutsScreen( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + uiState = UiLayoutTypeState.Visible( + layouts = listOf( + ObjectType.Layout.PROFILE, + ObjectType.Layout.BASIC, + ObjectType.Layout.TODO, + ObjectType.Layout.NOTE + ), + selectedLayout = ObjectType.Layout.BASIC + ), + onTypeEvent = {}) +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/objects/ObjectsHeaderScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/objects/ObjectsHeaderScreen.kt new file mode 100644 index 0000000000..d85c6d3637 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/objects/ObjectsHeaderScreen.kt @@ -0,0 +1,210 @@ +package com.anytypeio.anytype.feature_object_type.ui.objects + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.DVSortType +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.lists.objects.menu.ObjectsListMenuItem +import com.anytypeio.anytype.core_ui.lists.objects.menu.ObjectsListSortingMenuContainer +import com.anytypeio.anytype.core_ui.views.BodyBold +import com.anytypeio.anytype.core_ui.views.PreviewTitle1Regular +import com.anytypeio.anytype.feature_object_type.R +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent +import com.anytypeio.anytype.feature_object_type.ui.UiMenuSetItem +import com.anytypeio.anytype.feature_object_type.ui.UiMenuState +import com.anytypeio.anytype.feature_object_type.ui.UiObjectsAddIconState +import com.anytypeio.anytype.feature_object_type.ui.UiObjectsHeaderState +import com.anytypeio.anytype.feature_object_type.ui.UiObjectsSettingsIconState +import com.anytypeio.anytype.presentation.objects.MenuSortsItem +import com.anytypeio.anytype.presentation.objects.ObjectsListSort +import timber.log.Timber + +@Composable +fun ObjectsHeader( + modifier: Modifier, + uiObjectsHeaderState: UiObjectsHeaderState, + uiObjectsAddIconState: UiObjectsAddIconState, + uiObjectsSettingsIconState: UiObjectsSettingsIconState, + uiObjectsMenuState: UiMenuState, + onTypeEvent: (TypeEvent) -> Unit +) { + + var isMenuExpanded by remember { mutableStateOf(false) } + var isSortingExpanded by remember { mutableStateOf(false) } + + Box(modifier = modifier) { + Row( + modifier = Modifier.matchParentSize(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterVertically), + text = stringResource(R.string.objects), + style = BodyBold, + color = colorResource(R.color.text_primary) + ) + Text( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp), + text = uiObjectsHeaderState.count, + style = PreviewTitle1Regular, + color = colorResource(R.color.text_secondary) + ) + } + Row( + modifier = Modifier.align(Alignment.CenterEnd) + ) { + if (uiObjectsSettingsIconState is UiObjectsSettingsIconState.Visible) { + Box( + modifier = Modifier + .height(48.dp) + .width(40.dp) + .noRippleThrottledClickable { + isMenuExpanded = !isMenuExpanded + }, + contentAlignment = Alignment.CenterEnd + ) { + Image( + modifier = Modifier.wrapContentSize(), + painter = painterResource(R.drawable.ic_space_list_dots), + contentDescription = "Objects settings icon" + ) + } + DropdownMenu( + modifier = Modifier.width(252.dp), + expanded = isMenuExpanded, + onDismissRequest = { isMenuExpanded = false }, + shape = RoundedCornerShape(size = 16.dp), + containerColor = colorResource(id = R.color.background_primary), + shadowElevation = 5.dp + ) { + when (val item = uiObjectsMenuState.objSetItem) { + UiMenuSetItem.CreateSet -> { + ObjectsListMenuItem( + title = stringResource(R.string.object_type_objects_menu_create_set), + isSelected = false, + modifier = Modifier + .clickable { onTypeEvent(TypeEvent.OnCreateSetClick) } + ) + Divider( + height = 8.dp, + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_secondary) + ) + } + + is UiMenuSetItem.OpenSet -> { + ObjectsListMenuItem( + title = stringResource(R.string.object_type_objects_menu_open_set), + isSelected = false, + modifier = Modifier + .clickable { onTypeEvent(TypeEvent.OnOpenSetClick(setId = item.setId)) } + ) + Divider( + height = 8.dp, + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_secondary) + ) + } + + UiMenuSetItem.Hidden -> {} + } + ObjectsListSortingMenuContainer( + container = uiObjectsMenuState.container, + sorts = uiObjectsMenuState.sorts, + types = uiObjectsMenuState.types, + sortingExpanded = isSortingExpanded, + onSortClick = { + onTypeEvent(TypeEvent.OnSortClick(it)) + }, + onChangeSortExpandedState = { isSortingExpanded = it } + ) + } + } + if (uiObjectsAddIconState is UiObjectsAddIconState.Visible) { + Box( + modifier = Modifier + .padding(start = 8.dp) + .height(48.dp) + .width(32.dp) + .noRippleThrottledClickable { + onTypeEvent(TypeEvent.OnCreateObjectIconClick) + }, + contentAlignment = Alignment.CenterEnd + ) { + Image( + modifier = Modifier.wrapContentSize(), + painter = painterResource(R.drawable.ic_default_plus), + contentDescription = "Add", + ) + } + } + } + } +} + +@DefaultPreviews +@Composable +fun ObjectsHeaderPreview() { + ObjectsHeader( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + uiObjectsHeaderState = UiObjectsHeaderState("3"), + uiObjectsAddIconState = UiObjectsAddIconState.Visible, + uiObjectsSettingsIconState = UiObjectsSettingsIconState.Visible, + uiObjectsMenuState = UiMenuState( + container = MenuSortsItem.Container( + sort = ObjectsListSort.ByName(isSelected = true) + ), + sorts = listOf( + MenuSortsItem.Sort( + sort = ObjectsListSort.ByName(isSelected = true) + ), + ), + types = listOf( + MenuSortsItem.SortType( + sort = ObjectsListSort.ByName(isSelected = true), + sortType = DVSortType.DESC, + isSelected = true + ), + MenuSortsItem.SortType( + sort = ObjectsListSort.ByDateCreated(isSelected = false), + sortType = DVSortType.ASC, + isSelected = false + ), + ), + objSetItem = UiMenuSetItem.CreateSet + ), + onTypeEvent = {} + ) +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/templates/TemplatesList.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/templates/TemplatesList.kt new file mode 100644 index 0000000000..bb860f3a23 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/templates/TemplatesList.kt @@ -0,0 +1,193 @@ +package com.anytypeio.anytype.feature_object_type.ui.templates + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Text +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.widgets.TemplateItemContent +import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesListState +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent +import com.anytypeio.anytype.presentation.templates.TemplateView +import timber.log.Timber + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun TemplatesList( + uiTemplatesListState: UiTemplatesListState, + onTypeEvent: (TypeEvent) -> Unit +) { + + Timber.d("TemplatesList :$uiTemplatesListState") + + val scrollState = rememberLazyListState() + val interactionSource = remember { MutableInteractionSource() } + + LazyRow( + state = scrollState, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + contentPadding = PaddingValues(start = 20.dp, end = 20.dp), + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + items( + count = uiTemplatesListState.items.size, + key = { index -> + val item = uiTemplatesListState.items[index] + when (item) { + is TemplateView.Blank -> item.id + is TemplateView.New -> "new" + is TemplateView.Template -> item.id + } + }, + itemContent = { + var isMenuExpanded by remember { mutableStateOf(false) } + val item = uiTemplatesListState.items[it] + Box( + modifier = Modifier + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(size = 16.dp) + ) + .height(224.dp) + .width(120.dp) + .combinedClickable( + interactionSource = interactionSource, + indication = ripple(bounded = false, radius = 24.dp), + onClick = { + onTypeEvent(TypeEvent.OnTemplateItemClick(item)) + }, + onLongClick = { + if (item is TemplateView.Template) { + isMenuExpanded = true + } + }, + enabled = true, + ) + ) { + TemplateItemContent( + item = item, + showDefaultIcon = true + ) + DropdownMenu( + modifier = Modifier.width(244.dp), + expanded = isMenuExpanded, + onDismissRequest = { isMenuExpanded = false }, + shape = RoundedCornerShape(size = 10.dp), + containerColor = colorResource(id = R.color.background_primary), + shadowElevation = 5.dp, + offset = DpOffset( + x = 20.dp, + y = (-300).dp + ) + ) { + if (!item.isDefault) { + DropdownMenuItem( + modifier = Modifier.height(44.dp), + onClick = { + onTypeEvent(TypeEvent.OnTemplateMenuClick.SetAsDefault(item)) + isMenuExpanded = false + } + ) { + Text( + text = stringResource(R.string.object_type_templates_menu_set_default), + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + modifier = Modifier + ) + } + Divider( + height = 0.5.dp, + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_primary) + ) + } + DropdownMenuItem( + modifier = Modifier.height(44.dp), + onClick = { + onTypeEvent(TypeEvent.OnTemplateMenuClick.Edit(item)) + isMenuExpanded = false + } + ) { + Text( + text = stringResource(R.string.object_type_templates_menu_edit), + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + modifier = Modifier + ) + } + Divider( + height = 0.5.dp, + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_primary) + ) + DropdownMenuItem( + modifier = Modifier.height(44.dp), + onClick = { + onTypeEvent(TypeEvent.OnTemplateMenuClick.Duplicate(item)) + isMenuExpanded = false + } + ) { + Text( + text = stringResource(R.string.object_type_templates_menu_duplicate), + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + modifier = Modifier + ) + } + Divider( + height = 0.5.dp, + paddingStart = 0.dp, + paddingEnd = 0.dp, + color = colorResource(R.color.shape_primary) + ) + DropdownMenuItem( + modifier = Modifier.height(44.dp), + onClick = { + onTypeEvent(TypeEvent.OnTemplateMenuClick.Delete(item)) + isMenuExpanded = false + } + ) { + Text( + text = stringResource(R.string.object_type_templates_menu_delete), + style = BodyRegular, + color = colorResource(id = R.color.palette_system_red), + modifier = Modifier + ) + } + } + } + } + ) + } +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/templates/TemplatesScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/templates/TemplatesScreen.kt new file mode 100644 index 0000000000..9f09fa7c6c --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/templates/TemplatesScreen.kt @@ -0,0 +1,121 @@ +package com.anytypeio.anytype.feature_object_type.ui.templates + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable +import com.anytypeio.anytype.core_ui.views.BodyBold +import com.anytypeio.anytype.core_ui.views.PreviewTitle1Regular +import com.anytypeio.anytype.feature_object_type.R +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent.OnTemplatesAddIconClick +import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesAddIconState +import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesHeaderState +import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesListState + + +@Composable +fun LazyItemScope.TemplatesScreen( + uiTemplatesHeaderState: UiTemplatesHeaderState.Visible, + uiTemplatesAddIconState: UiTemplatesAddIconState, + uiTemplatesListState: UiTemplatesListState, + onTypeEvent: (TypeEvent) -> Unit +) { + Spacer( + modifier = Modifier.height(44.dp) + ) + TemplatesHeader( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + uiTemplatesHeaderState = uiTemplatesHeaderState, + uiTemplatesAddIconState = uiTemplatesAddIconState, + onTypeEvent = onTypeEvent + ) + Spacer( + modifier = Modifier.height(12.dp) + ) + TemplatesList( + uiTemplatesListState = uiTemplatesListState, + onTypeEvent = onTypeEvent + ) + Spacer( + modifier = Modifier.height(32.dp) + ) +} + +@Composable +fun TemplatesHeader( + modifier: Modifier, + uiTemplatesHeaderState: UiTemplatesHeaderState.Visible, + uiTemplatesAddIconState: UiTemplatesAddIconState, + onTypeEvent: (TypeEvent) -> Unit +) { + Box( + modifier = modifier.padding(horizontal = 20.dp), + ) { + Row(modifier = Modifier.wrapContentWidth().align(Alignment.CenterStart)) { + Text( + modifier = Modifier + .wrapContentSize() + .align(Alignment.CenterVertically), + text = stringResource(R.string.templates), + style = BodyBold, + color = colorResource(R.color.text_primary) + ) + Text( + modifier = Modifier + .wrapContentSize() + .padding(start = 8.dp), + text = uiTemplatesHeaderState.count, + style = PreviewTitle1Regular, + color = colorResource(R.color.text_secondary) + ) + } + if (uiTemplatesAddIconState is UiTemplatesAddIconState.Visible) { + Image( + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterEnd) + .noRippleThrottledClickable { + onTypeEvent(OnTemplatesAddIconClick) + }, + painter = painterResource(R.drawable.ic_default_plus), + contentDescription = "Add", + contentScale = ContentScale.Inside + ) + } + } +} + +@DefaultPreviews +@Composable +fun TemplatesHeaderPreview() { + TemplatesHeader( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + uiTemplatesHeaderState = UiTemplatesHeaderState.Visible( + count = "2" + ), + uiTemplatesAddIconState = UiTemplatesAddIconState.Visible, + ) { } +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt new file mode 100644 index 0000000000..a6c08b8f34 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt @@ -0,0 +1,1419 @@ +package com.anytypeio.anytype.feature_object_type.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.anytypeio.anytype.analytics.base.Analytics +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.ObjectOrigin +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_models.permissions.ObjectPermissions +import com.anytypeio.anytype.core_models.permissions.toObjectPermissionsForTypes +import com.anytypeio.anytype.core_models.primitives.TypeId +import com.anytypeio.anytype.core_models.primitives.TypeKey +import com.anytypeio.anytype.core_ui.lists.objects.UiContentState +import com.anytypeio.anytype.core_ui.lists.objects.UiObjectsListState +import com.anytypeio.anytype.core_utils.ext.orNull +import com.anytypeio.anytype.domain.base.fold +import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet +import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider +import com.anytypeio.anytype.domain.library.StoreSearchParams +import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.`object`.DuplicateObjects +import com.anytypeio.anytype.domain.`object`.SetObjectDetails +import com.anytypeio.anytype.domain.objects.DeleteObjects +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelations +import com.anytypeio.anytype.domain.page.CreateObject +import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields +import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields +import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields +import com.anytypeio.anytype.domain.resources.StringResourceProvider +import com.anytypeio.anytype.domain.templates.CreateTemplate +import com.anytypeio.anytype.feature_object_type.fields.FieldEvent +import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldItem +import com.anytypeio.anytype.feature_object_type.fields.UiAddFieldsScreenState +import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState +import com.anytypeio.anytype.feature_object_type.fields.UiFieldEditOrNewState.Visible.* +import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListItem +import com.anytypeio.anytype.feature_object_type.fields.UiFieldsListState +import com.anytypeio.anytype.feature_object_type.fields.UiLocalsFieldsInfoState +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeCommand.OpenEmojiPicker +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams +import com.anytypeio.anytype.feature_object_type.ui.TypeEvent +import com.anytypeio.anytype.feature_object_type.ui.UiDeleteAlertState +import com.anytypeio.anytype.feature_object_type.ui.UiEditButton +import com.anytypeio.anytype.feature_object_type.ui.UiErrorState +import com.anytypeio.anytype.feature_object_type.ui.UiFieldsButtonState +import com.anytypeio.anytype.feature_object_type.ui.UiIconState +import com.anytypeio.anytype.feature_object_type.ui.UiLayoutButtonState +import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState +import com.anytypeio.anytype.feature_object_type.ui.UiLayoutTypeState.* +import com.anytypeio.anytype.feature_object_type.ui.UiMenuSetItem +import com.anytypeio.anytype.feature_object_type.ui.UiMenuState +import com.anytypeio.anytype.feature_object_type.ui.UiObjectsAddIconState +import com.anytypeio.anytype.feature_object_type.ui.UiObjectsHeaderState +import com.anytypeio.anytype.feature_object_type.ui.UiObjectsSettingsIconState +import com.anytypeio.anytype.feature_object_type.ui.UiSyncStatusBadgeState +import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesAddIconState +import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesHeaderState +import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesListState +import com.anytypeio.anytype.feature_object_type.ui.UiTitleState +import com.anytypeio.anytype.feature_object_type.ui.buildUiFieldsList +import com.anytypeio.anytype.feature_object_type.ui.mapToUiAddFieldListItem +import com.anytypeio.anytype.feature_object_type.ui.toTemplateView +import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate +import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider +import com.anytypeio.anytype.presentation.editor.cover.UnsplashViewModel.Companion.DEBOUNCE_DURATION +import com.anytypeio.anytype.presentation.extension.sendAnalyticsScreenObjectType +import com.anytypeio.anytype.presentation.home.OpenObjectNavigation +import com.anytypeio.anytype.presentation.home.navigation +import com.anytypeio.anytype.presentation.mapper.objectIcon +import com.anytypeio.anytype.presentation.objects.ObjectsListSort +import com.anytypeio.anytype.presentation.objects.UiObjectsListItem +import com.anytypeio.anytype.presentation.objects.toDVSort +import com.anytypeio.anytype.presentation.objects.toMenuSortContainer +import com.anytypeio.anytype.presentation.objects.toSortOptions +import com.anytypeio.anytype.presentation.objects.toSortTypeOptions +import com.anytypeio.anytype.presentation.objects.toUiObjectsListItem +import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys +import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState +import com.anytypeio.anytype.presentation.sync.toSyncStatusWidgetState +import com.anytypeio.anytype.presentation.sync.updateStatus +import com.anytypeio.anytype.presentation.templates.TemplateView +import kotlin.collections.map +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import timber.log.Timber + +/** + * Fragment: @see [ObjectTypeFragment] + * Factory: @see [ObjectTypeVMFactory] + * Models: @see [ObjectViewState] + */ +class ObjectTypeViewModel( + private val vmParams: ObjectTypeVmParams, + private val analytics: Analytics, + private val urlBuilder: UrlBuilder, + private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, + private val userPermissionProvider: UserPermissionProvider, + private val storeOfRelations: StoreOfRelations, + private val storeOfObjectTypes: StoreOfObjectTypes, + private val storelessSubscriptionContainer: StorelessSubscriptionContainer, + private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider, + private val createObject: CreateObject, + private val fieldParser: FieldParser, + private val coverImageHashProvider: CoverImageHashProvider, + private val deleteObjects: DeleteObjects, + private val setObjectDetails: SetObjectDetails, + private val createObjectSet: CreateObjectSet, + private val stringResourceProvider: StringResourceProvider, + private val createTemplate: CreateTemplate, + private val duplicateObjects: DuplicateObjects, + private val getObjectTypeConflictingFields: GetObjectTypeConflictingFields, + private val objectTypeSetRecommendedFields: SetObjectTypeRecommendedFields, + private val objectTypeSetHeaderRecommendedFields: SetObjectTypeHeaderRecommendedFields +) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate { + + //region UI STATE + //top bar + val uiSyncStatusWidgetState = + MutableStateFlow(SyncStatusWidgetState.Hidden) + val uiSyncStatusBadgeState = + MutableStateFlow(UiSyncStatusBadgeState.Hidden) + val uiEditButtonState = MutableStateFlow(UiEditButton.Hidden) + + //header + val uiTitleState = MutableStateFlow(UiTitleState.Companion.EMPTY) + val uiIconState = MutableStateFlow(UiIconState.Companion.EMPTY) + + //layout and fields buttons + val uiFieldsButtonState = MutableStateFlow(UiFieldsButtonState.Hidden) + val uiLayoutButtonState = MutableStateFlow(UiLayoutButtonState.Hidden) + + //type layouts + val uiTypeLayoutsState = MutableStateFlow(Hidden) + + //templates header + val uiTemplatesHeaderState = + MutableStateFlow(UiTemplatesHeaderState.Hidden) + val uiTemplatesAddIconState = + MutableStateFlow(UiTemplatesAddIconState.Hidden) + + //templates list + val uiTemplatesListState = + MutableStateFlow(UiTemplatesListState.Companion.EMPTY) + + //objects header + val uiObjectsHeaderState = + MutableStateFlow(UiObjectsHeaderState.Companion.EMPTY) + val uiObjectsAddIconState = + MutableStateFlow(UiObjectsAddIconState.Hidden) + val uiObjectsSettingsIconState = + MutableStateFlow(UiObjectsSettingsIconState.Hidden) + val uiMenuState = MutableStateFlow(UiMenuState.Companion.EMPTY) + + //objects list + val uiObjectsListState = MutableStateFlow(UiObjectsListState.Empty) + val uiContentState = MutableStateFlow(UiContentState.Idle()) + private val restartSubscription = MutableStateFlow(0L) + private var shouldScrollToTopItems = false + private val _sortState = MutableStateFlow(ObjectsListSort.ByName()) + + //alerts + val uiAlertState = MutableStateFlow(UiDeleteAlertState.Hidden) + val uiFieldLocalInfoState = + MutableStateFlow(UiLocalsFieldsInfoState.Hidden) + + //fields + val uiFieldsListState = MutableStateFlow(UiFieldsListState.EMPTY) + val uiFieldEditOrNewState = + MutableStateFlow(UiFieldEditOrNewState.Hidden) + + //add new field + val uiAddFieldsState = MutableStateFlow(UiAddFieldsScreenState.Hidden) + + //error + val errorState = MutableStateFlow(UiErrorState.Hidden) + //endregion + + //region INNER STATE + private val _objTypeState = MutableStateFlow(null) + private val _objectTypePermissionsState = MutableStateFlow(null) + private val _objectTypeConflictingFieldIds = MutableStateFlow>(emptyList()) + //endregion + + //region INIT AND LIFE CYCLE + init { + Timber.d("init, vmParams: $vmParams") + proceedWithObservingSyncStatus() + proceedWithObservingObjectType() + proceedWithGetObjectTypeConflictingFields() + setupObjectsMenuFlow() + } + + fun onStart() { + Timber.d("onStart, vmParams: $vmParams") + if (vmParams.withSubscriptions) { + startSubscriptions() + } + viewModelScope.launch { + sendAnalyticsScreenObjectType( + analytics = analytics + ) + } + } + + fun onStop() { + Timber.d("onStop") + if (vmParams.withSubscriptions) { + stopSubscriptions() + } + uiObjectsListState.value = UiObjectsListState.Empty + } + //endregion + + //region DATA + private fun setupObjectsMenuFlow() { + viewModelScope.launch { + _sortState.map { sort -> + uiMenuState.value.copy( + container = sort.toMenuSortContainer(), + sorts = sort.toSortOptions(), + types = sort.toSortTypeOptions() + ) + } + .collect { newMenuState -> + uiMenuState.value = newMenuState + } + } + } + + private fun proceedWithObservingObjectType() { + viewModelScope.launch { + combine( + storeOfObjectTypes.trackChanges(), + storeOfRelations.trackChanges(), + userPermissionProvider.observe(space = vmParams.spaceId), + _objectTypeConflictingFieldIds, + ) { _, _, permission, conflictingFields -> + permission to conflictingFields + }.catch { + Timber.e(it, "Error while observing object type") + _objTypeState.value = null + errorState.value = + UiErrorState.Show(UiErrorState.Reason.ErrorGettingObjects(it.message ?: "")) + } + .collect { (permission, conflictingFields) -> + permission?.let { + val objType = storeOfObjectTypes.get(vmParams.objectId) + if (objType != null) { + val objectPermissions = objType.toObjectPermissionsForTypes( + participantCanEdit = it.isOwnerOrEditor() + ) + mapObjectTypeToUi( + objType = objType, + objectPermissions = objectPermissions, + conflictingFields = conflictingFields + ) + } else { + Timber.w( + "Error while observing object type [${vmParams.objectId}], " + + "objType is not present in store" + ) + _objTypeState.value = null + errorState.value = UiErrorState.Show( + UiErrorState.Reason.ErrorGettingObjects("Type details are empty") + ) + } + } + } + } + } + + private fun proceedWithObservingSyncStatus() { + viewModelScope.launch { + spaceSyncAndP2PStatusProvider + .observe() + .catch { Timber.e(it, "Error while observing sync status") } + .collect { syncAndP2pState -> + uiSyncStatusBadgeState.value = UiSyncStatusBadgeState.Visible(syncAndP2pState) + uiSyncStatusWidgetState.value = + uiSyncStatusWidgetState.value.updateStatus(syncAndP2pState) + } + } + } + + private fun startSubscriptions() { + startObjectsSubscription() + startSetSubscription() + startTemplatesSubscription() + } + + private fun stopSubscriptions() { + viewModelScope.launch { + storelessSubscriptionContainer.unsubscribe( + listOf( + objectsSubId(vmParams.objectId), + setsSubId(vmParams.objectId), + templatesSubId(vmParams.objectId), + ) + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun startObjectsSubscription() { + viewModelScope.launch { + combine( + restartSubscription, + _objTypeState, + _objectTypePermissionsState + ) { restart, objType, permission -> + objType to permission + }.flatMapLatest { (objType, permission) -> + if (objType == null || permission == null) { + emptyFlow() + } else { + loadObjects( + typeName = fieldParser.getObjectName(objectWrapper = objType), + permissions = permission + ).map { items -> items to permission } + } + }.catch { + Timber.e(it, "Error while observing objects") + errorState.value = + UiErrorState.Show(UiErrorState.Reason.ErrorGettingObjects(it.message ?: "")) + }.collect { (items, permission) -> + mapObjectsSubscriptionToUi(items, permission) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun startSetSubscription() { + viewModelScope.launch { + _objectTypePermissionsState + .flatMapLatest { permissions -> + if (permissions != null) { + loadSet().map { items -> items to permissions } + } else { + emptyFlow() + } + }.collect { (items, permissions) -> + Timber.d("items: $items, permissions: $permissions") + mapSetSubscriptionToUi(items, permissions) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun startTemplatesSubscription() { + viewModelScope.launch { + combine( + _objectTypePermissionsState, + storeOfObjectTypes.trackChanges() + ) { permissions, _ -> + permissions + }.flatMapLatest { permissions -> + val objType = storeOfObjectTypes.get(vmParams.objectId) + if (objType != null && permissions != null && permissions.canCreateTemplatesForThisType) { + loadTemplates(objType = objType) + .map { templates -> Triple(objType, templates, permissions) } + } else { + emptyFlow() + } + }.collect { (objType, templates, permissions) -> + mapTemplatesSubscriptionToUi(objType, templates, permissions) + } + } + } + + private fun loadObjects( + typeName: String, + permissions: ObjectPermissions + ): Flow> { + + val activeSort = _sortState.value + + val searchParams = StoreSearchParams( + filters = filtersForSearch(objectTypeId = vmParams.objectId), + sorts = listOf(activeSort.toDVSort()), + space = vmParams.spaceId, + limit = OBJECTS_MAX_COUNT, + keys = defaultKeys, + subscription = objectsSubId(vmParams.objectId) + ) + + return storelessSubscriptionContainer.subscribe(searchParams) + .onStart { + uiContentState.value = UiContentState.Paging + } + .map { objWrappers -> + val items = objWrappers.map { + it.toUiObjectsListItem( + space = vmParams.spaceId, + urlBuilder = urlBuilder, + typeName = typeName, + fieldParser = fieldParser, + isOwnerOrEditor = permissions.participantCanEdit + ) + } + items + }.catch { e -> + handleError(e) + } + } + + private fun loadSet(): Flow> { + + val searchParams = StoreSearchParams( + filters = filtersForSetsSearch(objectTypeId = vmParams.objectId), + sorts = listOf(sortForSetSearch()), + space = vmParams.spaceId, + limit = 1, + keys = defaultKeys, + subscription = setsSubId(vmParams.objectId) + ) + + return storelessSubscriptionContainer.subscribe(searchParams) + .catch { + handleError(it) + emit(emptyList()) + } + } + + private fun loadTemplates(objType: ObjectWrapper.Type): Flow> { + + val searchParams = StoreSearchParams( + filters = filtersForTemplatesSearch(objectTypeId = vmParams.objectId), + sorts = listOf(sortForTemplatesSearch()), + space = vmParams.spaceId, + limit = TEMPLATE_MAX_COUNT, + keys = defaultKeys, + subscription = templatesSubId(vmParams.objectId) + ) + + return storelessSubscriptionContainer.subscribe(searchParams).map { templates -> + templates.map { + it.toTemplateView( + objType = objType, + urlBuilder = urlBuilder, + coverImageHashProvider = coverImageHashProvider, + ) + } + } + } + + private fun handleError(e: Throwable) { + errorState.value = UiErrorState.Show( + reason = UiErrorState.Reason.Other(e.message ?: "") + ) + } + //endregion + + //region UI STATE + private fun updateDefaultTemplates(defaultTemplate: Id?) { + val templates = uiTemplatesListState.value.items + uiTemplatesListState.value = uiTemplatesListState.value.copy( + templates.map { template -> + when (template) { + is TemplateView.Blank -> template + is TemplateView.New -> template + is TemplateView.Template -> { + template.copy(isDefault = template.id == defaultTemplate) + } + } + } + ) + } + + private suspend fun mapObjectTypeToUi( + objType: ObjectWrapper.Type, + objectPermissions: ObjectPermissions, + conflictingFields: List + ) { + _objTypeState.value = objType + _objectTypePermissionsState.value = objectPermissions + + if (!objectPermissions.canCreateTemplatesForThisType) { + uiTemplatesHeaderState.value = UiTemplatesHeaderState.Hidden + uiTemplatesListState.value = UiTemplatesListState.EMPTY + uiTemplatesAddIconState.value = UiTemplatesAddIconState.Hidden + } + uiTitleState.value = UiTitleState( + title = objType.name.orEmpty(), + isEditable = objectPermissions.canEditDetails + ) + uiIconState.value = UiIconState( + icon = objType.objectIcon(urlBuilder), + isEditable = objectPermissions.canEditDetails + ) + if (objectPermissions.canCreateObjectThisType) { + uiObjectsAddIconState.value = UiObjectsAddIconState.Visible + } + uiObjectsSettingsIconState.value = UiObjectsSettingsIconState.Visible + if (objectPermissions.canDelete) { + uiEditButtonState.value = UiEditButton.Visible + } + objType.recommendedLayout?.let { layout -> + if (_objectTypePermissionsState.value?.canChangeRecommendedLayoutForThisType == true) { + uiLayoutButtonState.value = UiLayoutButtonState.Visible(layout = layout) + } + } + updateDefaultTemplates(defaultTemplate = objType.defaultTemplateId) + val items = buildUiFieldsList( + objType = objType, + stringResourceProvider = stringResourceProvider, + urlBuilder = urlBuilder, + fieldParser = fieldParser, + storeOfObjectTypes = storeOfObjectTypes, + storeOfRelations = storeOfRelations, + objTypeConflictingFields = conflictingFields, + showHiddenFields = vmParams.showHiddenFields + ) + uiFieldsListState.value = UiFieldsListState(items = items) + uiFieldsButtonState.value = UiFieldsButtonState.Visible( + count = items.count { it is UiFieldsListItem.Item } + ) + } + + private fun mapObjectsSubscriptionToUi( + items: List, + permission: ObjectPermissions + ) { + if (items.isEmpty()) { + uiObjectsListState.value = UiObjectsListState.Empty + uiContentState.value = UiContentState.Idle() + uiObjectsHeaderState.value = UiObjectsHeaderState(count = "0") + uiObjectsSettingsIconState.value = UiObjectsSettingsIconState.Visible + } else { + uiContentState.value = UiContentState.Idle( + scrollToTop = shouldScrollToTopItems.also { shouldScrollToTopItems = false } + ) + uiObjectsListState.value = UiObjectsListState(items = items) + uiObjectsHeaderState.value = UiObjectsHeaderState(count = "${items.size}") + uiObjectsSettingsIconState.value = UiObjectsSettingsIconState.Visible + } + if (permission.canCreateObjectThisType) { + uiObjectsAddIconState.value = UiObjectsAddIconState.Visible + } + } + + private fun mapSetSubscriptionToUi( + items: List, + permissions: ObjectPermissions + ) { + uiMenuState.value = if (!permissions.participantCanEdit) { + if (items.isEmpty()) { + uiMenuState.value.copy(objSetItem = UiMenuSetItem.Hidden) + } else { + uiMenuState.value.copy(objSetItem = UiMenuSetItem.OpenSet(setId = items[0].id)) + } + } else { + if (items.isEmpty()) { + uiMenuState.value.copy(objSetItem = UiMenuSetItem.CreateSet) + } else { + uiMenuState.value.copy(objSetItem = UiMenuSetItem.OpenSet(setId = items[0].id)) + } + } + } + + private fun mapTemplatesSubscriptionToUi( + objType: ObjectWrapper.Type, + templates: List, + permissions: ObjectPermissions + ) { + uiTemplatesHeaderState.value = UiTemplatesHeaderState.Visible(count = "${templates.size}") + + // Update each template view regarding default selection. + val updatedTemplates = templates.map { template -> + when (template) { + is TemplateView.Blank -> template + is TemplateView.New -> template + is TemplateView.Template -> template.copy( + isDefault = template.id == objType.defaultTemplateId + ) + } + } + + // Build final list with an extra "new template" item if allowed. + val finalTemplates = buildList { + addAll(updatedTemplates) + if (permissions.participantCanEdit) { + add( + TemplateView.New( + targetTypeId = TypeId(objType.id), + targetTypeKey = TypeKey(objType.uniqueKey) + ) + ) + uiTemplatesAddIconState.value = UiTemplatesAddIconState.Visible + } + } + uiTemplatesListState.value = UiTemplatesListState(items = finalTemplates) + } + + fun hideError() { + errorState.value = UiErrorState.Hidden + } + //endregion + + //region Ui EVENTS - TYPES + fun onTypeEvent(event: TypeEvent) { + Timber.d("onTypeEvent: $event") + when (event) { + TypeEvent.OnFieldsButtonClick -> { + viewModelScope.launch { + commands.emit(ObjectTypeCommand.OpenFieldsScreen) + } + } + + TypeEvent.OnLayoutButtonClick -> { + uiTypeLayoutsState.value = Visible( + layouts = listOf( + ObjectType.Layout.BASIC, + ObjectType.Layout.NOTE, + ObjectType.Layout.PROFILE, + ObjectType.Layout.TODO + ), + selectedLayout = _objTypeState.value?.recommendedLayout + ) + } + + is TypeEvent.OnSyncStatusClick -> { + uiSyncStatusWidgetState.value = + event.status.toSyncStatusWidgetState() + } + + TypeEvent.OnSyncStatusDismiss -> { + uiSyncStatusWidgetState.value = SyncStatusWidgetState.Hidden + } + + TypeEvent.OnTemplatesAddIconClick -> { + proceedWithCreateTemplate() + } + + is TypeEvent.OnObjectTypeTitleUpdate -> { + updateTitle(event.title) + } + + is TypeEvent.OnSortClick -> onSortClicked(event.sort) + TypeEvent.OnObjectsSettingsIconClick -> { + } + + TypeEvent.OnCreateSetClick -> { + proceedWithCreateSet() + } + + is TypeEvent.OnOpenSetClick -> { + proceedWithNavigation( + objectId = event.setId, + objectLayout = ObjectType.Layout.SET + ) + } + + TypeEvent.OnCreateObjectIconClick -> { + shouldScrollToTopItems = true + proceedWithCreateObjectOfThisType() + } + + TypeEvent.OnMenuItemDeleteClick -> { + uiAlertState.value = UiDeleteAlertState.Show + } + + TypeEvent.OnAlertDeleteConfirm -> { + uiAlertState.value = UiDeleteAlertState.Hidden + proceedWithObjectTypeDelete() + } + + TypeEvent.OnAlertDeleteDismiss -> { + uiAlertState.value = UiDeleteAlertState.Hidden + } + + is TypeEvent.OnObjectItemClick -> { + when (event.item) { + is UiObjectsListItem.Item -> { + proceedWithNavigation( + objectId = event.item.id, + objectLayout = event.item.layout + ) + } + + is UiObjectsListItem.Loading -> { + //do nothing + } + } + } + + TypeEvent.OnObjectTypeIconClick -> { + viewModelScope.launch { + commands.emit(OpenEmojiPicker) + } + } + + is TypeEvent.OnTemplateItemClick -> { + onTemplateItemClick(event.item) + } + + TypeEvent.OnLayoutTypeDismiss -> { + uiTypeLayoutsState.value = Hidden + } + + is TypeEvent.OnLayoutTypeItemClick -> { + proceedWithUpdatingLayout(layout = event.item) + } + + TypeEvent.OnBackClick -> { + viewModelScope.launch { + commands.emit(ObjectTypeCommand.Back) + } + } + + is TypeEvent.OnTemplateMenuClick -> proceedWithTemplateMenuClick(event) + } + } + + private fun proceedWithTemplateMenuClick(event: TypeEvent.OnTemplateMenuClick) { + when (event) { + is TypeEvent.OnTemplateMenuClick.Delete -> { + if (event.item is TemplateView.Template) { + proceedWithTemplateDelete( + template = event.item.id + ) + } + } + + is TypeEvent.OnTemplateMenuClick.Duplicate -> { + if (event.item is TemplateView.Template) { + proceedWithTemplateDuplicate( + template = event.item.id + ) + } + } + + is TypeEvent.OnTemplateMenuClick.Edit -> { + onTemplateItemClick(event.item) + } + + is TypeEvent.OnTemplateMenuClick.SetAsDefault -> { + if (event.item is TemplateView.Template) { + proceedWithSetDefaultTemplate( + template = event.item.id + ) + } + } + } + + } + + private fun onTemplateItemClick(item: TemplateView) { + when (item) { + is TemplateView.Blank -> { + //do nothing + } + + is TemplateView.New -> { + proceedWithCreateTemplate() + } + + is TemplateView.Template -> { + val typeKey = _objTypeState.value?.uniqueKey ?: return + val command = ObjectTypeCommand.OpenTemplate( + templateId = item.id, + typeId = vmParams.objectId, + typeKey = typeKey, + spaceId = vmParams.spaceId.id + ) + viewModelScope.launch { + commands.emit(command) + } + } + } + } + + private fun proceedWithUpdatingLayout(layout: ObjectType.Layout) { + viewModelScope.launch { + val params = SetObjectDetails.Params( + ctx = vmParams.objectId, + details = mapOf(Relations.RECOMMENDED_LAYOUT to layout.code.toDouble()) + ) + setObjectDetails.async(params).fold( + onFailure = { error -> + Timber.e(error, "Error while updating object type recommended layout") + }, + onSuccess = { + Timber.d("Object type recommended layout updated to layout: $layout") + } + ) + } + } + + fun onSortClicked(sort: ObjectsListSort) { + Timber.d("onSortClicked: $sort") + val newSort = when (sort) { + is ObjectsListSort.ByDateCreated -> { + sort.copy(isSelected = true) + } + + is ObjectsListSort.ByDateUpdated -> { + sort.copy(isSelected = true) + } + + is ObjectsListSort.ByName -> { + sort.copy(isSelected = true) + } + + is ObjectsListSort.ByDateUsed -> { + sort.copy(isSelected = true) + } + } + shouldScrollToTopItems = true + _sortState.value = newSort + restartSubscription.value++ + } + + private fun updateTitle(input: String) { + viewModelScope.launch { + val params = SetObjectDetails.Params( + ctx = vmParams.objectId, + details = mapOf(Relations.NAME to input) + ) + setObjectDetails.async(params).fold( + onFailure = { error -> + Timber.e(error, "Error while updating data view record") + }, + onSuccess = { + + } + ) + } + } + + fun updateIcon( + emoji: String + ) { + viewModelScope.launch { + val params = SetObjectDetails.Params( + ctx = vmParams.objectId, + details = mapOf(Relations.ICON_EMOJI to emoji) + ) + setObjectDetails.async(params).fold( + onFailure = { error -> + Timber.e(error, "Error while updating data view record") + }, + onSuccess = { + + } + ) + } + } + + fun removeIcon() { + viewModelScope.launch { + val params = SetObjectDetails.Params( + ctx = vmParams.objectId, + details = mapOf(Relations.ICON_EMOJI to null) + ) + setObjectDetails.async(params).fold( + onFailure = { error -> + Timber.e(error, "Error while updating data view record") + }, + onSuccess = { + } + ) + } + } + + fun closeObject() { + viewModelScope.launch { + commands.emit(ObjectTypeCommand.Back) + } + } + //endregion + + //region Ui EVENTS - FIELDS + fun onFieldEvent(event: FieldEvent) { + Timber.d("onFieldEvent: $event") + when (event) { + FieldEvent.OnChangeTypeClick -> TODO() + FieldEvent.OnFieldEditScreenDismiss -> { + uiFieldEditOrNewState.value = UiFieldEditOrNewState.Hidden + } + + is FieldEvent.OnFieldItemClick -> { + when (event.item) { + is UiFieldsListItem.Item -> { + val permissions = _objectTypePermissionsState.value + if (permissions?.participantCanEdit == true && event.item.isEditableField) { + uiFieldEditOrNewState.value = Edit( + event.item + ) + } else { + uiFieldEditOrNewState.value = ViewOnly( + event.item + ) + } + } + + else -> {} + } + } + + FieldEvent.OnLimitTypesClick -> TODO() + is FieldEvent.OnSaveButtonClicked -> TODO() + + is FieldEvent.FieldItemMenu -> proceedWithFieldItemMenuClick(event) + FieldEvent.FieldLocalInfo.OnDismiss -> { + uiFieldLocalInfoState.value = UiLocalsFieldsInfoState.Hidden + } + + FieldEvent.Section.OnLocalInfoClick -> { + uiFieldLocalInfoState.value = UiLocalsFieldsInfoState.Visible + } + + FieldEvent.Section.OnAddToHeaderIconClick -> { + proceedWithAddFieldToHeaderScreen() + } + + FieldEvent.Section.OnAddToSidebarIconClick -> { + proceedWithAddFieldToSidebarScreen() + } + + FieldEvent.DragEvent.OnDragEnd -> { + val newItems = uiFieldsListState.value.items + val headerItems = mutableListOf() + val sideBarItems = mutableListOf() + val hiddenItems = mutableListOf() + val filesItems = mutableListOf() + var currentSection: UiFieldsListItem.Section? = null + newItems.forEach { item -> + when (item) { + is UiFieldsListItem.Item -> { + when (currentSection) { + is UiFieldsListItem.Section.Header -> headerItems.add(item.id) + is UiFieldsListItem.Section.SideBar -> sideBarItems.add(item.id) + is UiFieldsListItem.Section.Hidden -> hiddenItems.add(item.id) + is UiFieldsListItem.Section.File -> filesItems.add(item.id) + else -> {} + } + } + + is UiFieldsListItem.Section -> currentSection = item + } + } + proceedWithUpdatingTypeFields( + headerFields = headerItems, + sidebarFields = sideBarItems, + hiddenFields = hiddenItems, + fileFields = filesItems + ) + } + + is FieldEvent.DragEvent.OnMove -> { + val currentList = uiFieldsListState.value.items.toMutableList() + val fromIndex = currentList.indexOfFirst { it.id == event.fromKey } + val toIndex = currentList.indexOfFirst { it.id == event.toKey } + if ((fromIndex == -1) || (toIndex == -1)) return + val item = currentList.removeAt(fromIndex) + currentList.add(toIndex, item) + uiFieldsListState.value = UiFieldsListState(items = currentList) + } + + FieldEvent.OnAddFieldScreenDismiss -> { + hideAddNewFieldScreen() + } + + is FieldEvent.OnAddToHeaderFieldClick -> { + onAddToHeaderFieldClicked(item = event.item) + hideAddNewFieldScreen() + } + + is FieldEvent.OnAddToSidebarFieldClick -> { + onAddToSidebarFieldClicked(item = event.item) + hideAddNewFieldScreen() + } + + is FieldEvent.OnAddFieldSearchQueryChanged -> { + onQueryChanged(query = event.query) + } + } + } + + private fun proceedWithFieldItemMenuClick(event: FieldEvent.FieldItemMenu) { + when (event) { + is FieldEvent.FieldItemMenu.OnDeleteFromTypeClick -> { + val deleteId = event.item.id + val headerItems = mutableListOf() + val sideBarItems = mutableListOf() + val hiddenItems = mutableListOf() + val filesItems = mutableListOf() + var currentSection: UiFieldsListItem.Section? = null + uiFieldsListState.value.items.forEach { item -> + when (item) { + is UiFieldsListItem.Item -> { + when (currentSection) { + is UiFieldsListItem.Section.Header -> { + if (item.id != deleteId) headerItems.add(item.id) + } + + is UiFieldsListItem.Section.SideBar -> { + if (item.id != deleteId) sideBarItems.add(item.id) + } + + is UiFieldsListItem.Section.Hidden -> { + if (item.id != deleteId) hiddenItems.add(item.id) + } + + is UiFieldsListItem.Section.File -> { + if (item.id != deleteId) filesItems.add(item.id) + } + + else -> {} + } + } + + is UiFieldsListItem.Section -> currentSection = item + } + } + proceedWithUpdatingTypeFields( + headerFields = headerItems, + sidebarFields = sideBarItems, + hiddenFields = hiddenItems, + fileFields = filesItems + ) + } + + is FieldEvent.FieldItemMenu.OnAddLocalToTypeClick -> { + val currentRecommendedFields = _objTypeState.value?.recommendedRelations.orEmpty() + val newRecommendedFields = currentRecommendedFields + event.item.id + proceedWithSetRecommendedFields(newRecommendedFields) + } + + is FieldEvent.FieldItemMenu.OnRemoveLocalClick -> TODO() + } + } + //endregion + + //region NAVIGATION + val commands = MutableSharedFlow() + val navigation = MutableSharedFlow() + + private fun proceedWithNavigation( + objectId: Id, + objectLayout: ObjectType.Layout? + ) { + Timber.d("proceedWithNavigation, objectId: $objectId, objectLayout: $objectLayout") + val destination = objectLayout?.navigation( + target = objectId, + space = vmParams.spaceId.id + ) + if (destination != null) { + viewModelScope.launch { + navigation.emit(destination) + } + } else { + Timber.w("No navigation destination found for object $objectId with layout $objectLayout") + } + } + //endregion + + //region USE CASES + private fun proceedWithUpdatingTypeFields( + headerFields: List, + sidebarFields: List, + hiddenFields: List, + fileFields: List + ) { + Timber.d("proceedWithUpdatingTypeFields") + viewModelScope.launch { + val params = SetObjectDetails.Params( + ctx = vmParams.objectId, + details = mapOf( + Relations.RECOMMENDED_FEATURED_RELATIONS to headerFields, + Relations.RECOMMENDED_RELATIONS to sidebarFields, + Relations.RECOMMENDED_HIDDEN_RELATIONS to hiddenFields, + Relations.RECOMMENDED_FILE_RELATIONS to fileFields + ) + ) + setObjectDetails.async(params).fold( + onSuccess = { + Timber.d("Fields updated") + }, + onFailure = { + Timber.e(it, "Error while updating fields") + } + ) + } + } + + private fun proceedWithGetObjectTypeConflictingFields() { + viewModelScope.launch { + getObjectTypeConflictingFields.async( + GetObjectTypeConflictingFields.Params( + objectTypeId = vmParams.objectId, + spaceId = vmParams.spaceId.id + ) + ).fold( + onSuccess = { fields -> + _objectTypeConflictingFieldIds.value = fields + }, + onFailure = { + Timber.e(it, "Error while getting conflicting fields") + } + ) + } + } + + private fun proceedWithCreateObjectOfThisType() { + val uniqueKeys = _objTypeState.value?.uniqueKey ?: return + val defaultTemplate = + uiTemplatesListState.value.items.firstOrNull { it.isDefault } as? TemplateView.Template + val params = CreateObject.Param( + space = vmParams.spaceId, + type = TypeKey(uniqueKeys), + template = defaultTemplate?.id, + prefilled = mapOf( + Relations.ORIGIN to ObjectOrigin.BUILT_IN.code.toDouble() + ) + ) + viewModelScope.launch { + createObject.async(params).fold( + onSuccess = { result -> + proceedWithNavigation( + objectId = result.objectId, + objectLayout = result.obj.layout + ) + }, + onFailure = { + Timber.e(it, "Error while creating object") + } + ) + } + } + + private fun proceedWithObjectTypeDelete() { + val params = DeleteObjects.Params( + targets = listOf(vmParams.objectId) + ) + viewModelScope.launch { + deleteObjects.async(params).fold( + onSuccess = { + Timber.d("Object ${vmParams.objectId} deleted") + commands.emit(ObjectTypeCommand.Back) + }, + onFailure = { + Timber.e(it, "Error while deleting object ${vmParams.objectId}") + } + ) + } + } + + private fun proceedWithTemplateDelete(template: Id) { + val params = DeleteObjects.Params( + targets = listOf(template) + ) + viewModelScope.launch { + deleteObjects.async(params).fold( + onSuccess = { + Timber.d("Template $template deleted") + }, + onFailure = { + Timber.e(it, "Error while deleting template $template") + } + ) + } + } + + private fun proceedWithCreateSet() { + val typeName = _objTypeState.value?.name.orEmpty() + val emoji = _objTypeState.value?.iconEmoji.orNull() + val params = CreateObjectSet.Params( + space = vmParams.spaceId.id, + type = vmParams.objectId, + details = mapOf( + Relations.NAME to "${stringResourceProvider.getSetOfObjectsTitle()} $typeName", + Relations.ICON_EMOJI to emoji + ) + ) + viewModelScope.launch { + createObjectSet.run(params).process( + failure = {}, + success = { response -> + val obj = ObjectWrapper.Basic(response.details) + proceedWithNavigation( + objectId = obj.id, + objectLayout = obj.layout + ) + } + ) + } + } + + private fun proceedWithCreateTemplate() { + val params = CreateTemplate.Params( + targetObjectTypeId = vmParams.objectId, + spaceId = vmParams.spaceId + ) + viewModelScope.launch { + createTemplate.async(params).fold( + onSuccess = { template -> + val typeKey = _objTypeState.value?.uniqueKey + if (typeKey != null) { + val command = ObjectTypeCommand.OpenTemplate( + templateId = template.id, + typeId = vmParams.objectId, + typeKey = typeKey, + spaceId = vmParams.spaceId.id + ) + commands.emit(command) + } + }, + onFailure = { + Timber.e(it, "Error while creating template") + } + ) + } + } + + private fun proceedWithTemplateDuplicate(template: Id) { + val params = DuplicateObjects.Params( + ids = listOf(template) + ) + viewModelScope.launch { + duplicateObjects.async(params).fold( + onSuccess = { + Timber.d("Template $template duplicated") + }, + onFailure = { + Timber.e(it, "Error while duplicating template $template") + } + ) + } + } + + private fun proceedWithSetDefaultTemplate(template: Id) { + val params = SetObjectDetails.Params( + ctx = vmParams.objectId, + details = mapOf(Relations.DEFAULT_TEMPLATE_ID to template) + ) + viewModelScope.launch { + setObjectDetails.async(params).fold( + onSuccess = { + Timber.d("Template $template set as default") + }, + onFailure = { + Timber.e(it, "Error while setting template $template as default") + } + ) + } + } + + private fun proceedWithSetRecommendedFields(fields: List) { + val params = SetObjectTypeRecommendedFields.Params( + objectTypeId = vmParams.objectId, + fields = fields + ) + viewModelScope.launch { + objectTypeSetRecommendedFields.async(params).fold( + onSuccess = { + Timber.d("Recommended fields set") + }, + onFailure = { + Timber.e(it, "Error while setting recommended fields") + } + ) + } + } + + private fun proceedWithSetHeaderRecommendedFields(fields: List) { + val params = SetObjectTypeHeaderRecommendedFields.Params( + objectTypeId = vmParams.objectId, + fields = fields + ) + viewModelScope.launch { + objectTypeSetHeaderRecommendedFields.async(params).fold( + onSuccess = { + Timber.d("Header recommended fields set") + }, + onFailure = { + Timber.e(it, "Error while setting header recommended fields") + } + ) + } + } + //endregion + + //region ADD NEW FIELD + private val input = MutableStateFlow("") + + @OptIn(FlowPreview::class) + private val query = input.take(1).onCompletion { + emitAll( + input.drop(1).debounce(DEBOUNCE_DURATION).distinctUntilChanged() + ) + } + + private var addFieldSearchJob: Job? = null + + /** + * Loads the available fields from type, applies filtering based on a search query, + * and then updates the UI state. + */ + private fun showAddFieldScreen(addToHeader: Boolean) { + // Collect field keys that are already present in the type fields list. + val typeFieldsKeys = + uiFieldsListState.value.items.mapNotNull { (it as? UiFieldsListItem.Item)?.fieldKey } + + addFieldSearchJob = viewModelScope.launch { + // Combine the search query flow with the list of all fields. + combine( + query, + storeOfRelations.trackChanges() + ) { queryText, _ -> + // Filter out fields by query and that already exist and are not valid. + filterFields( + allFields = storeOfRelations.getAll(), + typeKeys = typeFieldsKeys, + queryText = queryText + ) + }.collect { filteredFields -> + val items = filteredFields.mapNotNull { field -> + field.mapToUiAddFieldListItem(stringResourceProvider) + }.sortedBy { it.fieldTitle } + + uiAddFieldsState.value = UiAddFieldsScreenState.Visible( + items = items, + addToHeader = addToHeader + ) + } + } + } + + private fun filterFields( + allFields: List, + typeKeys: List, + queryText: String + ): List = allFields.filter { field -> + field.key !in typeKeys && + field.isValidToUse && + (queryText.isBlank() || field.name?.contains(queryText, ignoreCase = true) == true) + } + + private fun onQueryChanged(query: String) { + input.value = query + } + + fun hideAddNewFieldScreen() { + input.value = "" + addFieldSearchJob?.cancel() + addFieldSearchJob = null + uiAddFieldsState.value = UiAddFieldsScreenState.Hidden + } + + fun proceedWithAddFieldToHeaderScreen() { + showAddFieldScreen(addToHeader = true) + } + + fun proceedWithAddFieldToSidebarScreen() { + showAddFieldScreen(addToHeader = false) + } + + private fun updateFieldRecommendations( + currentFields: List?, + item: UiAddFieldItem, + updateAction: (List) -> Unit + ) { + val newFields = currentFields.orEmpty() + item.id + updateAction(newFields) + } + + private fun onAddToSidebarFieldClicked(item: UiAddFieldItem) { + updateFieldRecommendations( + currentFields = _objTypeState.value?.recommendedRelations, + item = item, + updateAction = ::proceedWithSetRecommendedFields + ) + } + + private fun onAddToHeaderFieldClicked(item: UiAddFieldItem) { + updateFieldRecommendations( + currentFields = _objTypeState.value?.recommendedFeaturedRelations, + item = item, + updateAction = ::proceedWithSetHeaderRecommendedFields + ) + } + //endregion + + companion object { + const val OBJECTS_MAX_COUNT = 20 + const val TEMPLATE_MAX_COUNT = 100 + + fun objectsSubId(objectId: Id) = "TYPE-OBJECTS-SUB-ID-$objectId" + fun setsSubId(objectId: Id) = "TYPE-SET-ID--$objectId" + fun templatesSubId(objectId: Id) = "TYPE-TEMPLATES-SUB-ID--$objectId" + } +} \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/SearchParams.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/SearchParams.kt new file mode 100644 index 0000000000..a6528e1d92 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/SearchParams.kt @@ -0,0 +1,126 @@ +package com.anytypeio.anytype.feature_object_type.viewmodel + +import com.anytypeio.anytype.core_models.DVFilter +import com.anytypeio.anytype.core_models.DVFilterCondition +import com.anytypeio.anytype.core_models.DVSort +import com.anytypeio.anytype.core_models.DVSortType +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.core_models.Relations + +fun filtersForSearch( + objectTypeId: Id +): List { + val filters = buildList { + addAll(buildDeletedFilter()) + add(buildTemplateFilter()) + add(buildTypeIdFilter(listOf(objectTypeId))) + } + return filters +} + +fun filtersForSetsSearch( + objectTypeId: Id +): List { + val filters = buildList { + addAll(buildDeletedFilter()) + add( + DVFilter( + relation = Relations.LAYOUT, + condition = DVFilterCondition.IN, + value = listOf(ObjectType.Layout.SET.code.toDouble()) + ) + ) + add( + DVFilter( + relation = Relations.SET_OF, + condition = DVFilterCondition.EQUAL, + value = objectTypeId + ) + ) + } + return filters +} + +fun filtersForTemplatesSearch( + objectTypeId: Id +): List { + val filters = buildList { + addAll(buildDeletedFilter()) + add( + DVFilter( + relation = Relations.TYPE_UNIQUE_KEY, + condition = DVFilterCondition.EQUAL, + value = ObjectTypeUniqueKeys.TEMPLATE + ) + ) + add( + DVFilter( + relation = Relations.TARGET_OBJECT_TYPE, + condition = DVFilterCondition.EQUAL, + value = objectTypeId + ) + ) + } + return filters +} + +fun sortForSetSearch() = DVSort( + relationKey = Relations.CREATED_DATE, + type = DVSortType.DESC, + includeTime = true, + relationFormat = RelationFormat.DATE +) + +fun sortForTemplatesSearch() = DVSort( + relationKey = Relations.LAST_MODIFIED_DATE, + type = DVSortType.DESC, + includeTime = true, + relationFormat = RelationFormat.DATE +) + +private fun buildTypeIdFilter(ids: List): DVFilter = DVFilter( + relation = Relations.TYPE, + condition = DVFilterCondition.IN, + value = ids +) + + +private fun buildDeletedFilter(): List { + return listOf( + DVFilter( + relation = Relations.IS_ARCHIVED, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.IS_HIDDEN, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.IS_DELETED, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.IS_HIDDEN_DISCOVERY, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ) + ) +} + +private fun buildSpaceIdFilter(spaces: List): DVFilter = DVFilter( + relation = Relations.SPACE_ID, + condition = DVFilterCondition.IN, + value = spaces +) + +private fun buildTemplateFilter(): DVFilter = DVFilter( + relation = Relations.TYPE_UNIQUE_KEY, + condition = DVFilterCondition.NOT_EQUAL, + value = ObjectTypeUniqueKeys.TEMPLATE +) \ No newline at end of file diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/VmFactory.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/VmFactory.kt new file mode 100644 index 0000000000..ae2b6399a1 --- /dev/null +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/VmFactory.kt @@ -0,0 +1,77 @@ +package com.anytypeio.anytype.feature_object_type.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.anytypeio.anytype.analytics.base.Analytics +import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet +import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider +import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.`object`.DuplicateObjects +import com.anytypeio.anytype.domain.`object`.SetObjectDetails +import com.anytypeio.anytype.domain.objects.DeleteObjects +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelations +import com.anytypeio.anytype.domain.page.CreateObject +import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.primitives.GetObjectTypeConflictingFields +import com.anytypeio.anytype.domain.primitives.SetObjectTypeHeaderRecommendedFields +import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields +import com.anytypeio.anytype.domain.resources.StringResourceProvider +import com.anytypeio.anytype.domain.templates.CreateTemplate +import com.anytypeio.anytype.feature_object_type.ui.ObjectTypeVmParams +import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate +import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider +import javax.inject.Inject + +class ObjectTypeVMFactory @Inject constructor( + private val vmParams: ObjectTypeVmParams, + private val analytics: Analytics, + private val urlBuilder: UrlBuilder, + private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, + private val userPermissionProvider: UserPermissionProvider, + private val storeOfRelations: StoreOfRelations, + private val storeOfObjectTypes: StoreOfObjectTypes, + private val storelessSubscriptionContainer: StorelessSubscriptionContainer, + private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider, + private val createObject: CreateObject, + private val fieldParser: FieldParser, + private val coverImageHashProvider: CoverImageHashProvider, + private val deleteObjects: DeleteObjects, + private val setObjectDetails: SetObjectDetails, + private val createObjectSet: CreateObjectSet, + private val stringResourceProvider: StringResourceProvider, + private val createTemplate: CreateTemplate, + private val duplicateObjects: DuplicateObjects, + private val getObjectTypeConflictingFields: GetObjectTypeConflictingFields, + private val objectTypeSetRecommendedFields: SetObjectTypeRecommendedFields, + private val objectTypeSetHeaderRecommendedFields: SetObjectTypeHeaderRecommendedFields +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + ObjectTypeViewModel( + vmParams = vmParams, + analytics = analytics, + urlBuilder = urlBuilder, + analyticSpaceHelperDelegate = analyticSpaceHelperDelegate, + userPermissionProvider = userPermissionProvider, + storeOfRelations = storeOfRelations, + storeOfObjectTypes = storeOfObjectTypes, + storelessSubscriptionContainer = storelessSubscriptionContainer, + spaceSyncAndP2PStatusProvider = spaceSyncAndP2PStatusProvider, + createObject = createObject, + fieldParser = fieldParser, + coverImageHashProvider = coverImageHashProvider, + deleteObjects = deleteObjects, + setObjectDetails = setObjectDetails, + createObjectSet = createObjectSet, + stringResourceProvider = stringResourceProvider, + createTemplate = createTemplate, + duplicateObjects = duplicateObjects, + getObjectTypeConflictingFields = getObjectTypeConflictingFields, + objectTypeSetRecommendedFields = objectTypeSetRecommendedFields, + objectTypeSetHeaderRecommendedFields = objectTypeSetHeaderRecommendedFields + ) as T +} \ No newline at end of file diff --git a/feature-object-type/src/test/java/com/anytypeio/anytype/feature_object_type/TestFieldsMappping.kt b/feature-object-type/src/test/java/com/anytypeio/anytype/feature_object_type/TestFieldsMappping.kt new file mode 100644 index 0000000000..bec57e3a55 --- /dev/null +++ b/feature-object-type/src/test/java/com/anytypeio/anytype/feature_object_type/TestFieldsMappping.kt @@ -0,0 +1,241 @@ +package com.anytypeio.anytype.feature_object_type + +import android.util.Log +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.StubObjectType +import com.anytypeio.anytype.core_models.StubRelationObject +import com.anytypeio.anytype.domain.config.Gateway +import com.anytypeio.anytype.domain.debugging.Logger +import com.anytypeio.anytype.domain.misc.DateProvider +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.objects.DefaultStoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations +import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelations +import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.primitives.FieldParserImpl +import com.anytypeio.anytype.domain.resources.StringResourceProvider +import com.anytypeio.anytype.test_utils.MockDataFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.coroutines.test.runTest +import net.lachlanmckee.timberjunit.TimberTestRule +import org.junit.Before +import org.junit.Rule +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +class TestFieldsMappping { + + @get:Rule + val timberTestRule: TimberTestRule = TimberTestRule.builder() + .minPriority(Log.DEBUG) + .showThread(true) + .showTimestamp(false) + .onlyLogWhenTestFails(true) + .build() + + val space = MockDataFactory.randomUuid() + + val field1 = StubRelationObject( + id = "field1_id", + key = "field1_key", + name = "Field 1, text", + format = RelationFormat.LONG_TEXT, + spaceId = space + ) + + val field2 = StubRelationObject( + id = "field2_id", + key = "field2_key", + name = "Field 2, number", + format = RelationFormat.NUMBER, + spaceId = space + ) + + val field3 = StubRelationObject( + id = "field3_id", + key = "field3_key", + name = "Field 3, date", + format = RelationFormat.DATE, + spaceId = space + ) + + val field4 = StubRelationObject( + id = "field4_id", + key = "field4_key", + name = "Field 4, checkbox", + format = RelationFormat.CHECKBOX, + spaceId = space + ) + + val field5 = StubRelationObject( + id = "field5_id", + key = "field5_key", + name = "Field 5, Status", + format = RelationFormat.STATUS, + spaceId = space + ) + + val fieldCreatedDate = StubRelationObject( + id = "bafyreihas6lc5knc67lbeohaxjgfjzi3oazs2yvh7gbotcktefjynjqndq", + key = Relations.CREATED_DATE, + name = "Field Creation date", + format = RelationFormat.DATE, + isHidden = false, + isReadOnly = true, + isReadOnlyValue = true, + spaceId = space, + sourceObject = "_brcreatedDate" + ) + + val fieldAssigneeObjType1 = StubObjectType() + val fieldAssigneeObjType2 = StubObjectType() + + val fieldAssignee = StubRelationObject( + id = "bafyreibrqycr2w5q2db76f5l6hxfljwrgkrpqbulks6ppxsfu4hq5lwmue", + key = "assignee", + name = "Field Assignee", + format = RelationFormat.OBJECT, + objectTypes = listOf( + fieldAssigneeObjType1.id, + fieldAssigneeObjType2.id + ), + isHidden = false, + isReadOnly = false, + isReadOnlyValue = false, + spaceId = space, + sourceObject = "_brassignee" + ) + + val allSpaceRelations = + listOf(field1, field2, field3, field4, field5, fieldCreatedDate, fieldAssignee) + + val featuredFields = listOf(field1, field2, field5).map { it.id } + + val sidebarFields = listOf(field3, field4, field5).map { it.id } + + val hiddenFields = listOf(field5).map { it.id } + + val testObjectType = StubObjectType( + id = "test_object_type_id", + uniqueKey = "test_object_type_unique_key", + name = "Test custom object type", + recommendedRelations = sidebarFields, + recommendedFeaturedRelations = featuredFields, + recommendedHiddenRelations = hiddenFields, + layout = ObjectType.Layout.OBJECT_TYPE.code.toDouble(), + recommendedLayout = ObjectType.Layout.TODO.code.toDouble(), + space = space + ) + + @Mock + lateinit var stringResourceProvider: StringResourceProvider + + @Mock + lateinit var dateProvider: DateProvider + + @Mock + lateinit var logger: Logger + + @Mock + lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp + + @Mock + lateinit var gateway: Gateway + + lateinit var storeOfRelations: StoreOfRelations + + lateinit var storeOfObjectTypes: StoreOfObjectTypes + + lateinit var fieldParser: FieldParser + + private lateinit var urlBuilder: UrlBuilder + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + storeOfRelations = DefaultStoreOfRelations() + storeOfObjectTypes = DefaultStoreOfObjectTypes() + urlBuilder = UrlBuilder(gateway) + fieldParser = + FieldParserImpl( + dateProvider = dateProvider, + logger = logger, + getDateObjectByTimestamp = getDateObjectByTimestamp, + stringResourceProvider = stringResourceProvider + ) + } + + @Test + fun `should not filter featured fields by hidden`() = runTest { + + storeOfRelations.apply { + merge(allSpaceRelations) + } + + storeOfObjectTypes.apply { + merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1)) + } + + val parsedFields = fieldParser.getObjectTypeParsedFields( + objectType = testObjectType, + storeOfRelations = storeOfRelations, + objectTypeConflictingFieldsIds = listOf() + ) + + assertEquals( + expected = listOf(field1, field2, field5), + actual = parsedFields.header + ) + } + + @Test + fun `should not filter sidebar fields by hidden`() = runTest { + + storeOfRelations.apply { + merge(allSpaceRelations) + } + + storeOfObjectTypes.apply { + merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1)) + } + + val parsedFields = fieldParser.getObjectTypeParsedFields( + objectType = testObjectType, + storeOfRelations = storeOfRelations, + objectTypeConflictingFieldsIds = listOf() + ) + + assertEquals( + expected = listOf(field3, field4, field5), + actual = parsedFields.sidebar + ) + } + + @Test + fun `should map hidden fields`() = runTest { + + storeOfRelations.apply { + merge(allSpaceRelations) + } + + storeOfObjectTypes.apply { + merge(listOf(testObjectType, fieldAssigneeObjType2, fieldAssigneeObjType1)) + } + + val parsedFields = fieldParser.getObjectTypeParsedFields( + objectType = testObjectType, + storeOfRelations = storeOfRelations, + objectTypeConflictingFieldsIds = listOf() + ) + + assertEquals( + expected = listOf(field5), + actual = parsedFields.hidden + ) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd4d9dcfb8..8997e50879 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,8 @@ composeConstraintLayoutVersion = '1.1.1' dokkaVersion = '1.9.20' activityComposeVersion = '1.10.1' -composeReorderableVersion = 'e9ef693f63' +composeReorderableLegacyVersion = 'e9ef693f63' +composeReorderableVersion = '2.4.3' accompanistVersion = "0.34.0" appcompatVersion = '1.7.0' fragmentVersion = "1.8.6" @@ -82,7 +83,8 @@ composeAccompanistPermissions = { module = "com.google.accompanist:accompanist-p composeAccompanistPagerIndicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanistVersion" } composeAccompanistThemeAdapter = { module = "com.google.accompanist:accompanist-themeadapter-material", version.ref = "accompanistVersion" } composeAccompanistNavigation = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanistVersion" } -composeReorderable = { module = "com.github.fat-fellow.ComposeReorderable:reorderable", version.ref = "composeReorderableVersion" } +composeReorderableLegacy = { module = "com.github.fat-fellow.ComposeReorderable:reorderable", version.ref = "composeReorderableLegacyVersion" } +composeReorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "composeReorderableVersion" } composeConstraintLayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "composeConstraintLayoutVersion" } kotlinxSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.3" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompatVersion" } diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 1eb5e75df1..7627c7cc0f 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -246,7 +246,7 @@ Turn into Turn into Page Delete - Move to Bin + To Bin Duplicate Move to Style @@ -270,6 +270,7 @@ Search Search... Relations + Fields Relation Download View @@ -396,6 +397,7 @@ Favorite Template Templates + Layout type Lock Unlock Restore @@ -607,6 +609,7 @@ Make default Default object Default template + Default This type doesn\'t support templates %1$d applied List @@ -785,6 +788,8 @@ Set Collection Layout + Fields + Layout Restore from archive Icon Emoji or image for object @@ -1849,6 +1854,35 @@ Please provide specific details of your needs here. Reply Add Reaction + It’s empty here. + Create your first objects to get started. + Delete completely + Open Set + Create Set + Set as default + Edit + Duplicate + Delete + Header + Sidebar + Hidden + File + Local fields + Fields + You editing type + Edit field + New field + Preview field + Save + Delete + Add to the current type + Remove + Local fields + Some fields are not included in the object type. Please add them if you want to see them in all objects of this type, or remove them. + Got it + + Error opening current object type + Monday Tuesday Wednesday @@ -1873,9 +1907,11 @@ Please provide specific details of your needs here. Probably someone has just removed the reaction or technical issue happened Human + Delete completely It cannot be restored after confirmation Delete this message? + Set of Migration is in progress This may take some time. Please don’t close the app until the process is complete. Try again @@ -1904,5 +1940,7 @@ Please provide specific details of your needs here. File & Media Add Select + Add to the current type + Remove from the object \ No newline at end of file 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 34fdc27095..853e052a91 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 @@ -3,6 +3,7 @@ package com.anytypeio.anytype.middleware.block import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.CBTextStyle import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Command.ObjectTypeConflictingFields import com.anytypeio.anytype.core_models.Config import com.anytypeio.anytype.core_models.CreateBlockLinkWithObjectResult import com.anytypeio.anytype.core_models.CreateObjectResult @@ -263,10 +264,12 @@ class BlockMiddleware( override suspend fun createSet( space: Id, - objectType: String? + objectType: String?, + details: Struct? ): Response.Set.Create = middleware.objectCreateSet( space = space, - objectType = objectType + objectType = objectType, + details = details ) override suspend fun setDataViewViewerPosition( @@ -1075,4 +1078,16 @@ class BlockMiddleware( override suspend fun setDeviceNetworkState(type: DeviceNetworkType) { middleware.setDeviceNetworkState(type) } + + override suspend fun objectTypeListConflictingRelations(command: ObjectTypeConflictingFields): List { + return middleware.objectTypeListConflictingRelations(command) + } + + override suspend fun objectTypeSetRecommendedHeaderFields(command: Command.ObjectTypeSetRecommendedHeaderFields) { + middleware.objectTypeSetRecommendedHeaderFields(command) + } + + override suspend fun objectTypeSetRecommendedFields(command: Command.ObjectTypeSetRecommendedFields) { + middleware.objectTypeSetRecommendedFields(command) + } } \ No newline at end of file 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 f46a1833ea..1eaa9d2af9 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 @@ -8,6 +8,7 @@ import com.anytypeio.anytype.core_models.AccountSetup import com.anytypeio.anytype.core_models.AccountStatus import com.anytypeio.anytype.core_models.CBTextStyle import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Command.ObjectTypeConflictingFields import com.anytypeio.anytype.core_models.Config import com.anytypeio.anytype.core_models.CreateBlockLinkWithObjectResult import com.anytypeio.anytype.core_models.CreateObjectResult @@ -1023,7 +1024,8 @@ class Middleware @Inject constructor( @Throws(Exception::class) fun objectCreateSet( space: Id, - objectType: String? + objectType: String?, + details: Struct? ): Response.Set.Create { val source = if (objectType != null) { listOf(objectType) @@ -1033,7 +1035,8 @@ class Middleware @Inject constructor( val request = Rpc.Object.CreateSet.Request( source = source, - spaceId = space + spaceId = space, + details = details ) logRequestIfDebug(request) @@ -1041,9 +1044,9 @@ class Middleware @Inject constructor( logResponseIfDebug(response, time) return Response.Set.Create( - targetId = response.objectId, + objectId = response.objectId, payload = response.event.toPayload(), - blockId = null + details = response.details.orEmpty() ) } @@ -2894,6 +2897,40 @@ class Middleware @Inject constructor( return response.path } + @Throws(Exception::class) + fun objectTypeListConflictingRelations(command: ObjectTypeConflictingFields): List { + val request = Rpc.ObjectType.ListConflictingRelations.Request( + spaceId = command.spaceId, + typeObjectId = command.objectTypeId + ) + logRequestIfDebug(request) + val (response, time) = measureTimedValue { service.objectTypeListConflictingRelations(request) } + logResponseIfDebug(response, time) + return response.relationIds + } + + @Throws(Exception::class) + fun objectTypeSetRecommendedHeaderFields(command: Command.ObjectTypeSetRecommendedHeaderFields) { + val request = Rpc.ObjectType.Recommended.FeaturedRelationsSet.Request( + typeObjectId = command.objectTypeId, + relationObjectIds = command.fields + ) + logRequestIfDebug(request) + val (response, time) = measureTimedValue { service.objectTypeHeaderRecommendedFieldsSet(request) } + logResponseIfDebug(response, time) + } + + @Throws(Exception::class) + fun objectTypeSetRecommendedFields(command: Command.ObjectTypeSetRecommendedFields) { + val request = Rpc.ObjectType.Recommended.RelationsSet.Request( + typeObjectId = command.objectTypeId, + relationObjectIds = command.fields + ) + logRequestIfDebug(request) + val (response, time) = measureTimedValue { service.objectTypeRecommendedFieldsSet(request) } + logResponseIfDebug(response, time) + } + private fun logRequestIfDebug(request: Any) { if (BuildConfig.DEBUG) { logger.logRequest(request).also { diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt index bc63e30d52..08512c3b0c 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt @@ -621,4 +621,13 @@ interface MiddlewareService { @Throws(Exception::class) fun debugExportLogs(request: Rpc.Debug.ExportLog.Request): Rpc.Debug.ExportLog.Response + + @Throws(Exception::class) + fun objectTypeListConflictingRelations(request: Rpc.ObjectType.ListConflictingRelations.Request) : Rpc.ObjectType.ListConflictingRelations.Response + + @Throws(Exception::class) + fun objectTypeHeaderRecommendedFieldsSet(request: Rpc.ObjectType.Recommended.FeaturedRelationsSet.Request) : Rpc.ObjectType.Recommended.FeaturedRelationsSet.Response + + @Throws(Exception::class) + fun objectTypeRecommendedFieldsSet(request: Rpc.ObjectType.Recommended.RelationsSet.Request) : Rpc.ObjectType.Recommended.RelationsSet.Response } \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt index d0c718d7f0..6121f8448b 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt @@ -2508,4 +2508,43 @@ class MiddlewareServiceImplementation @Inject constructor( return response } } + + override fun objectTypeListConflictingRelations(request: Rpc.ObjectType.ListConflictingRelations.Request): Rpc.ObjectType.ListConflictingRelations.Response { + val encoded = Service.objectTypeListConflictingRelations( + Rpc.ObjectType.ListConflictingRelations.Request.ADAPTER.encode(request) + ) + val response = Rpc.ObjectType.ListConflictingRelations.Response.ADAPTER.decode(encoded) + val error = response.error + if (error != null && error.code != Rpc.ObjectType.ListConflictingRelations.Response.Error.Code.NULL) { + throw Exception(error.description) + } else { + return response + } + } + + override fun objectTypeHeaderRecommendedFieldsSet(request: Rpc.ObjectType.Recommended.FeaturedRelationsSet.Request): Rpc.ObjectType.Recommended.FeaturedRelationsSet.Response { + val encoded = Service.objectTypeRecommendedFeaturedRelationsSet( + Rpc.ObjectType.Recommended.FeaturedRelationsSet.Request.ADAPTER.encode(request) + ) + val response = Rpc.ObjectType.Recommended.FeaturedRelationsSet.Response.ADAPTER.decode(encoded) + val error = response.error + if (error != null && error.code != Rpc.ObjectType.Recommended.FeaturedRelationsSet.Response.Error.Code.NULL) { + throw Exception(error.description) + } else { + return response + } + } + + override fun objectTypeRecommendedFieldsSet(request: Rpc.ObjectType.Recommended.RelationsSet.Request): Rpc.ObjectType.Recommended.RelationsSet.Response { + val encoded = Service.objectTypeRecommendedRelationsSet( + Rpc.ObjectType.Recommended.RelationsSet.Request.ADAPTER.encode(request) + ) + val response = Rpc.ObjectType.Recommended.RelationsSet.Response.ADAPTER.decode(encoded) + val error = response.error + if (error != null && error.code != Rpc.ObjectType.Recommended.RelationsSet.Response.Error.Code.NULL) { + throw Exception(error.description) + } else { + return response + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt index f69226bb9a..bdf85682eb 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt @@ -87,19 +87,24 @@ class DefaultBlockViewRenderer @Inject constructor( isPreviousBlockMedia = false when (content.style) { Content.Text.Style.TITLE -> { - mCounter = 0 - result.add( - title( - context = context, - mode = mode, - block = block, - content = content, - focus = focus, - root = root, - details = details, - restrictions = restrictions + val obj = details.getObject(id = context) + if (obj?.layout == ObjectType.Layout.NOTE) { + // Workaround for skipping title block in objects with note layout + Timber.d("Skipping title rendering for object with note layout or nullable object") + } else { + mCounter = 0 + result.add( + title( + mode = mode, + block = block, + content = content, + focus = focus, + root = root, + restrictions = restrictions, + currentObject = obj + ) ) - ) + } } Content.Text.Style.P -> { mCounter = 0 @@ -1402,18 +1407,15 @@ class DefaultBlockViewRenderer @Inject constructor( } private fun title( - context: Id, mode: EditorMode, block: Block, content: Content.Text, root: Block, focus: Focus, - details: ObjectViewDetails, - restrictions: List + restrictions: List, + currentObject: ObjectWrapper.Basic? ): BlockView.Title { - val currentObject = details.getObject(id = context) - val focusTarget = focus.target val cursor: Int? = if (focusTarget is Focus.Target.Block && focusTarget.id == block.id) { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/AnalyticsExt.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/AnalyticsExt.kt index db107ff823..91d9a89f2b 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/AnalyticsExt.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/AnalyticsExt.kt @@ -2287,3 +2287,20 @@ fun CoroutineScope.sendAnalyticsObjectListSort( } //endregion +//region Object Type +//setScreenShow or CreateObject objectType: "_otset" +//sendAnalyticsAllContentChangeType +//objectSetTitle +//relationAdd +//changeRecommendedLayout + +fun CoroutineScope.sendAnalyticsScreenObjectType( + analytics: Analytics +) { + sendEvent( + analytics = analytics, + eventName = EventsDictionary.screenObjectType + ) +} +//endregion + diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt index 455054192f..2b0ea40df2 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt @@ -39,6 +39,26 @@ fun ObjectWrapper.Basic.objectIcon(builder: UrlBuilder): ObjectIcon { return layout.emptyType() } +fun ObjectWrapper.Type.objectIcon(builder: UrlBuilder): ObjectIcon { + + if (isDeleted == true) { + return ObjectIcon.Deleted + } + + val objectIcon = layout?.icon( + image = null, + emoji = iconEmoji, + builder = builder, + name = name.orEmpty() + ) + + if (objectIcon != null) { + return objectIcon + } + + return layout.emptyType() +} + fun ObjectType.Layout?.emptyType(): ObjectIcon.Empty { if (this == null) { return ObjectIcon.Empty.Page 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 959fa1fbdc..155ac5f0a4 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 @@ -40,6 +40,16 @@ interface AppNavigation { space: Id ) + fun openObjectType( + objectId: Id, + space: Id + ) + + fun openCurrentObjectTypeFields( + objectId: Id, + space: Id + ) + fun launchDocument(target: String, space: Id) fun launchCollections(subscription: Subscription, space: Id) fun launchObjectSet(target: Id, space: Id) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/Models.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/Models.kt new file mode 100644 index 0000000000..f0d95274cb --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/Models.kt @@ -0,0 +1,190 @@ +package com.anytypeio.anytype.presentation.objects + +import com.anytypeio.anytype.core_models.DVSort +import com.anytypeio.anytype.core_models.DVSortType +import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds +import com.anytypeio.anytype.core_models.ObjectType +import com.anytypeio.anytype.core_models.ObjectTypeUniqueKeys +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.primitives.RelationKey +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.presentation.mapper.objectIcon + +sealed class MenuSortsItem { + data class Container(val sort: ObjectsListSort) : MenuSortsItem() + data class Sort(val sort: ObjectsListSort) : MenuSortsItem() + data object Spacer : MenuSortsItem() + data class SortType( + val sort: ObjectsListSort, + val sortType: DVSortType, + val isSelected: Boolean + ) : MenuSortsItem() +} + +sealed class ObjectsListSort { + abstract val relationKey: RelationKey + abstract val sortType: DVSortType + abstract val canGroupByDate: Boolean + abstract val isSelected: Boolean + + data class ByName( + override val relationKey: RelationKey = RelationKey(Relations.NAME), + override val sortType: DVSortType = DVSortType.ASC, + override val canGroupByDate: Boolean = false, + override val isSelected: Boolean = false + ) : ObjectsListSort() + + data class ByDateUpdated( + override val relationKey: RelationKey = RelationKey(Relations.LAST_MODIFIED_DATE), + override val sortType: DVSortType = DVSortType.DESC, + override val canGroupByDate: Boolean = true, + override val isSelected: Boolean = false + ) : ObjectsListSort() + + data class ByDateCreated( + override val relationKey: RelationKey = RelationKey(Relations.CREATED_DATE), + override val sortType: DVSortType = DVSortType.DESC, + override val canGroupByDate: Boolean = true, + override val isSelected: Boolean = false + ) : ObjectsListSort() + + data class ByDateUsed( + override val relationKey: RelationKey = RelationKey(Relations.LAST_USED_DATE), + override val sortType: DVSortType = DVSortType.DESC, + override val canGroupByDate: Boolean = false, + override val isSelected: Boolean = false + ) : ObjectsListSort() +} + +fun ObjectsListSort.toMenuSortContainer(): MenuSortsItem.Container = + MenuSortsItem.Container(sort = this) + +fun ObjectsListSort.toSortOptions(): List = listOf( + MenuSortsItem.Sort( + sort = ObjectsListSort.ByDateUpdated(isSelected = this is ObjectsListSort.ByDateUpdated) + ), + MenuSortsItem.Sort( + sort = ObjectsListSort.ByDateCreated(isSelected = this is ObjectsListSort.ByDateCreated) + ), + MenuSortsItem.Sort( + sort = ObjectsListSort.ByName(isSelected = this is ObjectsListSort.ByName) + ) +) + +fun ObjectsListSort.toSortTypeOptions(): List = listOf( + MenuSortsItem.SortType( + sort = this, + sortType = DVSortType.ASC, + isSelected = this.sortType == DVSortType.ASC + ), + MenuSortsItem.SortType( + sort = this, + sortType = DVSortType.DESC, + isSelected = this.sortType == DVSortType.DESC + ) +) + +fun ObjectsListSort.toDVSort(): DVSort { + return when (this) { + is ObjectsListSort.ByDateCreated -> DVSort( + relationKey = relationKey.key, + type = sortType, + relationFormat = RelationFormat.DATE, + includeTime = true, + ) + + is ObjectsListSort.ByDateUpdated -> DVSort( + relationKey = relationKey.key, + type = sortType, + relationFormat = RelationFormat.DATE, + includeTime = true, + ) + + is ObjectsListSort.ByName -> DVSort( + relationKey = relationKey.key, + type = sortType, + relationFormat = RelationFormat.LONG_TEXT, + includeTime = false + ) + + is ObjectsListSort.ByDateUsed -> DVSort( + relationKey = relationKey.key, + type = sortType, + relationFormat = RelationFormat.DATE, + includeTime = true, + ) + } +} + +sealed class UiObjectsListItem { + + abstract val id: String + + data class Loading(override val id: String) : UiObjectsListItem() + + data class Item( + override val id: String, + val name: String, + val space: SpaceId, + val type: String? = null, + val typeName: String? = null, + val createdBy: String? = null, + val layout: ObjectType.Layout? = null, + val icon: ObjectIcon = ObjectIcon.None, + val isPossibleToDelete: Boolean = false + ) : UiObjectsListItem() +} + +fun ObjectWrapper.Basic.toUiObjectsListItem( + space: SpaceId, + urlBuilder: UrlBuilder, + objectTypes: List, + fieldParser: FieldParser, + isOwnerOrEditor: Boolean +): UiObjectsListItem { + val obj = this + val typeUrl = obj.getProperType() + val isProfile = typeUrl == MarketplaceObjectTypeIds.PROFILE + val layout = obj.layout ?: ObjectType.Layout.BASIC + return UiObjectsListItem.Item( + id = obj.id, + space = space, + name = fieldParser.getObjectName(obj), + type = typeUrl, + typeName = objectTypes.firstOrNull { type -> + if (isProfile) { + type.uniqueKey == ObjectTypeUniqueKeys.PROFILE + } else { + type.id == typeUrl + } + }?.name, + layout = layout, + icon = obj.objectIcon(builder = urlBuilder), + isPossibleToDelete = isOwnerOrEditor + ) +} + +fun ObjectWrapper.Basic.toUiObjectsListItem( + space: SpaceId, + urlBuilder: UrlBuilder, + typeName: String?, + fieldParser: FieldParser, + isOwnerOrEditor: Boolean +): UiObjectsListItem { + val obj = this + val typeUrl = obj.getProperType() + return UiObjectsListItem.Item( + id = obj.id, + space = space, + name = fieldParser.getObjectName(obj), + type = typeUrl, + typeName = typeName, + layout = obj.layout, + icon = obj.objectIcon(builder = urlBuilder), + isPossibleToDelete = isOwnerOrEditor + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProvider.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProvider.kt index 0ab0c7a43e..9863c8bd98 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProvider.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProvider.kt @@ -8,27 +8,27 @@ interface ObjectMenuOptionsProvider { data class Options( val hasIcon: Boolean, val hasCover: Boolean, - val hasLayout: Boolean, val hasRelations: Boolean, val hasDiagnosticsVisibility: Boolean, - val hasHistory: Boolean + val hasHistory: Boolean, + val hasDescriptionShow: Boolean ) { companion object { val ALL = Options( hasIcon = true, hasCover = true, - hasLayout = true, hasRelations = true, hasDiagnosticsVisibility = true, - hasHistory = true + hasHistory = true, + hasDescriptionShow = true ) val NONE = Options( hasIcon = false, hasCover = false, - hasLayout = false, hasRelations = false, hasDiagnosticsVisibility = false, - hasHistory = false + hasHistory = false, + hasDescriptionShow = false ) } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImpl.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImpl.kt index 6a5c8289ea..2b3eb3d30d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImpl.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImpl.kt @@ -5,6 +5,7 @@ import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.SupportedLayouts import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction import com.anytypeio.anytype.core_models.ObjectViewDetails +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.presentation.extension.getObject import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuOptionsProvider.Options import kotlinx.coroutines.flow.Flow @@ -28,82 +29,101 @@ class ObjectMenuOptionsProviderImpl( details.getObject(ctx)?.layout } + private fun observeFeatureFieldsContainsDescription(ctx: Id): Flow = + objectViewDetailsFlow + .filter { details -> + details.details.containsKey(ctx).also { isValuePresent -> + if (!isValuePresent) Timber.w("Details missing for object: $ctx") + } + } + .map { details -> + val featuredRelations = details.getObject(ctx)?.featuredRelations + return@map featuredRelations?.any { it == Relations.DESCRIPTION } == true + } + override fun provide(ctx: Id, isLocked: Boolean, isReadOnly: Boolean): Flow { - return combine(observeLayout(ctx), restrictions) { layout, restrictions -> + return combine( + observeLayout(ctx), + observeFeatureFieldsContainsDescription(ctx) + ) { layout, featuredContainsDescription -> createOptions( layout = layout, - restrictions = restrictions, isLocked = isLocked, - isReadOnly = isReadOnly + isReadOnly = isReadOnly, + featuredContainsDescription = featuredContainsDescription ) } } private fun createOptions( layout: ObjectType.Layout?, - restrictions: List, isLocked: Boolean, - isReadOnly: Boolean + isReadOnly: Boolean, + featuredContainsDescription: Boolean ): Options { val hasIcon = !isLocked && !isReadOnly val hasCover = !isLocked && !isReadOnly - val hasLayout = !isLocked && !restrictions.contains(ObjectRestriction.LAYOUT_CHANGE) && !isReadOnly val options = if (layout != null) { when (layout) { ObjectType.Layout.PARTICIPANT -> Options.ALL.copy( hasIcon = false, hasCover = false, - hasLayout = false, hasDiagnosticsVisibility = true, hasHistory = false, - hasRelations = false + hasRelations = false, + hasDescriptionShow = !featuredContainsDescription ) + in SupportedLayouts.systemLayouts -> Options.NONE in SupportedLayouts.fileLayouts -> { Options.ALL.copy( hasIcon = false, hasCover = false, - hasLayout = false, hasDiagnosticsVisibility = true, - hasHistory = false + hasHistory = false, + hasDescriptionShow = !featuredContainsDescription ) } + ObjectType.Layout.SET, ObjectType.Layout.COLLECTION -> { Options.ALL.copy( hasIcon = hasIcon, hasCover = hasCover, - hasLayout = false, hasDiagnosticsVisibility = true, - hasHistory = !isLocked && !isReadOnly + hasHistory = !isLocked && !isReadOnly, + hasDescriptionShow = !featuredContainsDescription ) } + ObjectType.Layout.BASIC, ObjectType.Layout.PROFILE, ObjectType.Layout.BOOKMARK -> Options.ALL.copy( hasIcon = hasIcon, hasCover = hasCover, - hasLayout = hasLayout, hasDiagnosticsVisibility = true, - hasHistory = !isLocked && !isReadOnly + hasHistory = !isLocked && !isReadOnly, + hasDescriptionShow = !featuredContainsDescription ) + ObjectType.Layout.TODO -> Options( hasIcon = false, hasCover = hasCover, - hasLayout = hasLayout, hasRelations = true, hasDiagnosticsVisibility = true, - hasHistory = !isLocked && !isReadOnly + hasHistory = !isLocked && !isReadOnly, + hasDescriptionShow = !featuredContainsDescription ) ObjectType.Layout.NOTE -> Options( hasIcon = false, hasCover = false, - hasLayout = hasLayout, hasRelations = true, hasDiagnosticsVisibility = true, - hasHistory = !isLocked && !isReadOnly + hasHistory = !isLocked && !isReadOnly, + hasDescriptionShow = !featuredContainsDescription ) + else -> Options.NONE.copy( hasDiagnosticsVisibility = true ) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt index eb39ae8d08..3c04ef7c3e 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModel.kt @@ -38,6 +38,9 @@ import com.anytypeio.anytype.core_models.SupportedLayouts.fileLayouts import com.anytypeio.anytype.core_models.SupportedLayouts.systemLayouts import com.anytypeio.anytype.domain.multiplayer.GetSpaceInviteLink import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations +import com.anytypeio.anytype.domain.relations.RemoveFromFeaturedRelations import com.anytypeio.anytype.presentation.extension.getObject import com.anytypeio.anytype.presentation.extension.getTypeObject import com.anytypeio.anytype.presentation.objects.getProperType @@ -54,8 +57,8 @@ class ObjectMenuViewModel( addBackLinkToObject: AddBackLinkToObject, delegator: Delegator, urlBuilder: UrlBuilder, - dispatcher: Dispatcher, - menuOptionsProvider: ObjectMenuOptionsProvider, + private val dispatcher: Dispatcher, + private val menuOptionsProvider: ObjectMenuOptionsProvider, duplicateObject: DuplicateObject, createWidget: CreateWidget, payloadDelegator: PayloadDelegator, @@ -74,7 +77,10 @@ class ObjectMenuViewModel( private val setObjectIsArchived: SetObjectListIsArchived, private val fieldParser: FieldParser, private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, - private val getSpaceInviteLink: GetSpaceInviteLink + private val getSpaceInviteLink: GetSpaceInviteLink, + private val addToFeaturedRelations: AddToFeaturedRelations, + private val removeFromFeaturedRelations: RemoveFromFeaturedRelations, + private val userPermissionProvider: UserPermissionProvider ) : ObjectMenuViewModelBase( setObjectIsArchived = setObjectIsArchived, addBackLinkToObject = addBackLinkToObject, @@ -122,6 +128,19 @@ class ObjectMenuViewModel( add(ObjectAction.DOWNLOAD_FILE) } } else { + + if (isArchived) { + add(ObjectAction.RESTORE) + } else { + if (objectRestrictions.none { it == ObjectRestriction.DELETE }) { + add(ObjectAction.MOVE_TO_BIN) + } + } + + if (!isTemplate && !systemLayouts.contains(layout) && !fileLayouts.contains(layout)) { + add(ObjectAction.CREATE_WIDGET) + } + if (!isTemplate) { if (isFavorite) { add(ObjectAction.REMOVE_FROM_FAVOURITE) @@ -130,28 +149,16 @@ class ObjectMenuViewModel( } } - if (isArchived) { - add(ObjectAction.RESTORE) - } else { - if (objectRestrictions.none { it == ObjectRestriction.DELETE }) { - add(ObjectAction.DELETE) - } - } - - if (!isTemplate && !systemLayouts.contains(layout) && !fileLayouts.contains(layout)) { - add(ObjectAction.CREATE_WIDGET) - } - - if (isTemplate) { - add(ObjectAction.SET_AS_DEFAULT) - } - - if (!objectRestrictions.contains(ObjectRestriction.DUPLICATE) && !isTemplate) { + if (!objectRestrictions.contains(ObjectRestriction.DUPLICATE)) { add(ObjectAction.DUPLICATE) } add(ObjectAction.UNDO_REDO) + if (isTemplate) { + add(ObjectAction.SET_AS_DEFAULT) + } + val objTypeId = wrapper?.getProperType() if (objTypeId != null) { val objType = storage.details.current().getTypeObject(objTypeId) @@ -181,7 +188,7 @@ class ObjectMenuViewModel( if (layout in fileLayouts) { clear() - add(ObjectAction.DELETE) + add(ObjectAction.MOVE_TO_BIN) add(ObjectAction.DOWNLOAD_FILE) if (isFavorite) { add(ObjectAction.REMOVE_FROM_FAVOURITE) @@ -255,20 +262,48 @@ class ObjectMenuViewModel( } } - override fun onLayoutClicked(ctx: Id, space: Id) { + override fun onDescriptionClicked(ctx: Id, space: Id) { viewModelScope.launch { - if (objectRestrictions.contains(ObjectRestriction.LAYOUT_CHANGE)) { + if (userPermissionProvider.get(space = SpaceId(space))?.isOwnerOrEditor() != true) { _toasts.emit(NOT_ALLOWED) - } else { - try { - if (!isThisObjectLocked(ctx)) { - commands.emit(Command.OpenObjectLayout) - } else { - _toasts.emit("Your object is locked.") + return@launch + } + val isDescriptionAlreadyInFeatured = + storage.details.current().getObject(ctx)?.featuredRelations?.contains( + Relations.DESCRIPTION + ) == true + if (isDescriptionAlreadyInFeatured) { + removeFromFeaturedRelations( + params = RemoveFromFeaturedRelations.Params( + ctx = ctx, + relations = listOf(Relations.DESCRIPTION) + ) + ).proceed( + success = { payload -> + dispatcher.send(payload) + Timber.d("Description was removed from featured relations") + }, + failure = { + Timber.e(it, "Error while removing description from featured relations") + _toasts.emit(SOMETHING_WENT_WRONG_MSG) } - } catch (e: Exception) { - _toasts.emit("Something went wrong. Please, try again later.") - } + ) + } else { + addToFeaturedRelations( + params = AddToFeaturedRelations.Params( + ctx = ctx, + relations = listOf(Relations.DESCRIPTION) + ) + ).proceed( + success = { payload -> + dispatcher.send(payload) + Timber.d("Description was added to featured relations") + }, + failure = { + Timber.e(it, "Error while adding description to featured relations") + _toasts.emit(SOMETHING_WENT_WRONG_MSG) + } + ) } } } @@ -281,7 +316,7 @@ class ObjectMenuViewModel( override fun onActionClicked(ctx: Id, space: Id, action: ObjectAction) { when (action) { - ObjectAction.DELETE -> { + ObjectAction.MOVE_TO_BIN -> { proceedWithUpdatingArchivedStatus(ctx = ctx, isArchived = true) } ObjectAction.DUPLICATE -> { @@ -344,7 +379,7 @@ class ObjectMenuViewModel( if (wrapper != null) proceedWithCreatingWidget(obj = wrapper) } ObjectAction.MOVE_TO, - ObjectAction.MOVE_TO_BIN, + ObjectAction.DELETE, ObjectAction.DELETE_FILES -> { throw IllegalStateException("$action is unsupported") } @@ -511,7 +546,10 @@ class ObjectMenuViewModel( private val setObjectIsArchived: SetObjectListIsArchived, private val fieldParser: FieldParser, private val getSpaceInviteLink: GetSpaceInviteLink, - private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer + private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, + private val addToFeaturedRelations: AddToFeaturedRelations, + private val removeFromFeaturedRelations: RemoveFromFeaturedRelations, + private val userPermissionProvider: UserPermissionProvider ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return ObjectMenuViewModel( @@ -538,7 +576,10 @@ class ObjectMenuViewModel( setObjectListIsFavorite = setObjectListIsFavorite, fieldParser = fieldParser, getSpaceInviteLink = getSpaceInviteLink, - spaceViewSubscriptionContainer = spaceViewSubscriptionContainer + spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, + addToFeaturedRelations = addToFeaturedRelations, + removeFromFeaturedRelations = removeFromFeaturedRelations, + userPermissionProvider = userPermissionProvider ) as T } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt index 9bb68a50d9..91fdb6f3d5 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import timber.log.Timber @@ -54,7 +55,7 @@ abstract class ObjectMenuViewModelBase( private val addBackLinkToObject: AddBackLinkToObject, protected val delegator: Delegator, protected val urlBuilder: UrlBuilder, - protected val dispatcher: Dispatcher, + private val dispatcher: Dispatcher, private val analytics: Analytics, private val menuOptionsProvider: ObjectMenuOptionsProvider, private val duplicateObject: DuplicateObject, @@ -82,17 +83,17 @@ abstract class ObjectMenuViewModelBase( ObjectMenuOptionsProvider.Options( hasIcon = false, hasCover = false, - hasLayout = false, hasRelations = false, hasDiagnosticsVisibility = false, - hasHistory = false + hasHistory = false, + hasDescriptionShow = false ) ) val options: Flow = _options abstract fun onIconClicked(ctx: Id, space: Id) abstract fun onCoverClicked(ctx: Id, space: Id) - abstract fun onLayoutClicked(ctx: Id, space: Id) + abstract fun onDescriptionClicked(ctx: Id, space: Id) abstract fun onRelationsClicked() fun onHistoryClicked(ctx: Id, space: Id) { @@ -126,6 +127,7 @@ abstract class ObjectMenuViewModelBase( jobs += viewModelScope.launch { menuOptionsProvider .provide(ctx = ctx, isLocked = isLocked, isReadOnly = isReadOnly) + .distinctUntilChanged() .collect(_options) } } @@ -481,7 +483,6 @@ abstract class ObjectMenuViewModelBase( data object OpenSetIcons : Command() data object OpenObjectCover : Command() data object OpenSetCover : Command() - data object OpenObjectLayout : Command() data object OpenSetLayout : Command() data object OpenObjectRelations : Command() data object OpenSetRelations : Command() diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectSetMenuViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectSetMenuViewModel.kt index 7d0ac9dfc9..5d9c314e03 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectSetMenuViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectSetMenuViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Payload +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction import com.anytypeio.anytype.domain.collections.AddObjectToCollection @@ -14,10 +15,13 @@ import com.anytypeio.anytype.domain.misc.DeepLinkResolver import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.GetSpaceInviteLink import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.`object`.DuplicateObject import com.anytypeio.anytype.domain.objects.SetObjectListIsArchived import com.anytypeio.anytype.domain.page.AddBackLinkToObject import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations +import com.anytypeio.anytype.domain.relations.RemoveFromFeaturedRelations import com.anytypeio.anytype.domain.widgets.CreateWidget import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate @@ -41,7 +45,7 @@ class ObjectSetMenuViewModel( duplicateObject: DuplicateObject, delegator: Delegator, urlBuilder: UrlBuilder, - dispatcher: Dispatcher, + private val dispatcher: Dispatcher, menuOptionsProvider: ObjectMenuOptionsProvider, createWidget: CreateWidget, spaceManager: SpaceManager, @@ -55,7 +59,10 @@ class ObjectSetMenuViewModel( setObjectListIsFavorite: SetObjectListIsFavorite, fieldParser: FieldParser, spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, - getSpaceInviteLink: GetSpaceInviteLink + getSpaceInviteLink: GetSpaceInviteLink, + private val addToFeaturedRelations: AddToFeaturedRelations, + private val removeFromFeaturedRelations: RemoveFromFeaturedRelations, + private val userPermissionProvider: UserPermissionProvider ) : ObjectMenuViewModelBase( setObjectIsArchived = setObjectIsArchived, addBackLinkToObject = addBackLinkToObject, @@ -103,7 +110,10 @@ class ObjectSetMenuViewModel( private val setObjectListIsArchived: SetObjectListIsArchived, private val fieldParser: FieldParser, private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, - private val getSpaceInviteLink: GetSpaceInviteLink + private val getSpaceInviteLink: GetSpaceInviteLink, + private val addToFeaturedRelations: AddToFeaturedRelations, + private val removeFromFeaturedRelations: RemoveFromFeaturedRelations, + private val userPermissionProvider: UserPermissionProvider ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return ObjectSetMenuViewModel( @@ -126,7 +136,10 @@ class ObjectSetMenuViewModel( setObjectListIsFavorite = setObjectListIsFavorite, fieldParser = fieldParser, spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, - getSpaceInviteLink = getSpaceInviteLink + getSpaceInviteLink = getSpaceInviteLink, + addToFeaturedRelations = addToFeaturedRelations, + removeFromFeaturedRelations = removeFromFeaturedRelations, + userPermissionProvider = userPermissionProvider ) as T } } @@ -153,13 +166,49 @@ class ObjectSetMenuViewModel( } } - override fun onLayoutClicked(ctx: Id, space: Id) { - val dataViewState = objectState.value.dataViewState() ?: return + override fun onDescriptionClicked(ctx: Id, space: Id) { + val details = objectState.value.dataViewState()?.details ?: return viewModelScope.launch { - if (dataViewState.objectRestrictions.contains(ObjectRestriction.LAYOUT_CHANGE)) { + if (userPermissionProvider.get(space = SpaceId(space))?.isOwnerOrEditor() != true) { _toasts.emit(NOT_ALLOWED) + return@launch + } + val isDescriptionAlreadyInFeatured = + details.getObject(ctx)?.featuredRelations?.contains( + Relations.DESCRIPTION + ) == true + if (isDescriptionAlreadyInFeatured) { + removeFromFeaturedRelations.run( + params = RemoveFromFeaturedRelations.Params( + ctx = ctx, + relations = listOf(Relations.DESCRIPTION) + ) + ).proceed( + success = { payload -> + dispatcher.send(payload) + Timber.d("Description was removed from featured relations") + }, + failure = { + Timber.e(it, "Error while removing description from featured relations") + _toasts.emit(SOMETHING_WENT_WRONG_MSG) + } + ) } else { - commands.emit(Command.OpenSetLayout) + addToFeaturedRelations.run( + params = AddToFeaturedRelations.Params( + ctx = ctx, + relations = listOf(Relations.DESCRIPTION) + ) + ).proceed( + success = { payload -> + dispatcher.send(payload) + Timber.d("Description was added to featured relations") + }, + failure = { + Timber.e(it, "Error while adding description to featured relations") + _toasts.emit(SOMETHING_WENT_WRONG_MSG) + } + ) } } } @@ -187,14 +236,14 @@ class ObjectSetMenuViewModel( if (isArchived) { add(ObjectAction.RESTORE) } else { - add(ObjectAction.DELETE) + add(ObjectAction.MOVE_TO_BIN) } + add(ObjectAction.CREATE_WIDGET) if (isFavorite) { add(ObjectAction.REMOVE_FROM_FAVOURITE) } else { add(ObjectAction.ADD_TO_FAVOURITE) } - add(ObjectAction.CREATE_WIDGET) val dataViewState = objectState.value.dataViewState() if (dataViewState != null && !dataViewState.objectRestrictions.contains( ObjectRestriction.DUPLICATE @@ -210,7 +259,7 @@ class ObjectSetMenuViewModel( override fun onActionClicked(ctx: Id, space: Id, action: ObjectAction) { val state = objectState.value.dataViewState() ?: return when (action) { - ObjectAction.DELETE -> { + ObjectAction.MOVE_TO_BIN -> { proceedWithUpdatingArchivedStatus(ctx = ctx, isArchived = true) } ObjectAction.RESTORE -> { @@ -250,7 +299,7 @@ class ObjectSetMenuViewModel( ObjectAction.UNDO_REDO, ObjectAction.LOCK, ObjectAction.UNLOCK, - ObjectAction.MOVE_TO_BIN, + ObjectAction.DELETE, ObjectAction.USE_AS_TEMPLATE, ObjectAction.SET_AS_DEFAULT, ObjectAction.DELETE_FILES -> throw IllegalStateException("$action is unsupported") diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectRelationListViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectRelationListViewModelFactory.kt index ebc6e83937..c30079037d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectRelationListViewModelFactory.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/ObjectRelationListViewModelFactory.kt @@ -5,9 +5,12 @@ import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields import com.anytypeio.anytype.domain.relations.AddRelationToObject import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations import com.anytypeio.anytype.domain.relations.DeleteRelationFromObject @@ -30,9 +33,12 @@ class ObjectRelationListViewModelFactory( private val deleteRelationFromObject: DeleteRelationFromObject, private val analytics: Analytics, private val storeOfRelations: StoreOfRelations, + private val storeOfObjectTypes: StoreOfObjectTypes, private val addRelationToObject: AddRelationToObject, private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, - private val fieldParser: FieldParser + private val fieldParser: FieldParser, + private val userPermissionProvider: UserPermissionProvider, + private val setObjectTypeRecommendedFields: SetObjectTypeRecommendedFields ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -49,9 +55,12 @@ class ObjectRelationListViewModelFactory( deleteRelationFromObject = deleteRelationFromObject, analytics = analytics, storeOfRelations = storeOfRelations, + storeOfObjectTypes = storeOfObjectTypes, addRelationToObject = addRelationToObject, analyticSpaceHelperDelegate = analyticSpaceHelperDelegate, - fieldParser = fieldParser + fieldParser = fieldParser, + userPermissionProvider = userPermissionProvider, + setObjectTypeRecommendedFields = setObjectTypeRecommendedFields ) as T } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationListViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationListViewModel.kt index c1195f37d0..240abc55d1 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationListViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/relations/RelationListViewModel.kt @@ -9,17 +9,22 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary.relationsScreenShow import com.anytypeio.anytype.analytics.base.sendEvent import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Key +import com.anytypeio.anytype.core_models.ObjectViewDetails import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Payload import com.anytypeio.anytype.core_models.RelationFormat +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.TimeInMillis import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_utils.diff.DefaultObjectDiffIdentifier import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.`object`.UpdateDetail +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import com.anytypeio.anytype.domain.objects.StoreOfRelations import com.anytypeio.anytype.domain.primitives.FieldParser +import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields import com.anytypeio.anytype.domain.relations.AddRelationToObject import com.anytypeio.anytype.domain.relations.AddToFeaturedRelations import com.anytypeio.anytype.domain.relations.DeleteRelationFromObject @@ -27,10 +32,10 @@ import com.anytypeio.anytype.domain.relations.RemoveFromFeaturedRelations import com.anytypeio.anytype.presentation.BuildConfig import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate import com.anytypeio.anytype.presentation.common.BaseViewModel -import com.anytypeio.anytype.core_models.ObjectViewDetails -import com.anytypeio.anytype.presentation.extension.getStruct import com.anytypeio.anytype.presentation.extension.getObjRelationsViews +import com.anytypeio.anytype.presentation.extension.getObject import com.anytypeio.anytype.presentation.extension.getRecommendedRelations +import com.anytypeio.anytype.presentation.extension.getStruct import com.anytypeio.anytype.presentation.extension.getTypeForObject import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationEvent import com.anytypeio.anytype.presentation.objects.LockedStateProvider @@ -56,17 +61,24 @@ class RelationListViewModel( private val deleteRelationFromObject: DeleteRelationFromObject, private val analytics: Analytics, private val storeOfRelations: StoreOfRelations, + private val storeOfObjectTypes: StoreOfObjectTypes, private val addRelationToObject: AddRelationToObject, private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate, - private val fieldParser: FieldParser + private val fieldParser: FieldParser, + private val userPermissionProvider: UserPermissionProvider, + private val setObjectTypeRecommendedFields: SetObjectTypeRecommendedFields ) : BaseViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate { val isEditMode = MutableStateFlow(false) private val jobs = mutableListOf() + private var _currentObjectTypeId: Id? = null val commands = MutableSharedFlow(replay = 0) val views = MutableStateFlow>(emptyList()) + val showLocalInfo = MutableStateFlow(false) + + private val permission = MutableStateFlow(userPermissionProvider.get(vmParams.spaceId)) init { Timber.i("RelationListViewModel, init") @@ -80,14 +92,110 @@ class RelationListViewModel( ) jobs += viewModelScope.launch { combine( + storeOfObjectTypes.trackChanges(), storeOfRelations.trackChanges(), objectRelationListProvider.details - ) { _, details -> - constructViews(ctx, details) + ) { _, _, details -> + parseToViews(ctx, details) }.collect { views.value = it } } } + private suspend fun parseToViews( + ctx: Id, + details: ObjectViewDetails + ): List { + + val objType = details.getTypeForObject(ctx) + _currentObjectTypeId = objType?.id + + if (objType == null) { + Timber.w("Failed to get object type for object: $ctx from types store") + return emptyList() + } + + val parsedFields = fieldParser.getObjectParsedFields( + objectType = objType, + storeOfRelations = storeOfRelations, + objFieldKeys = details.getObject(ctx)?.map?.keys?.toList().orEmpty() + ) + + val headerFields = parsedFields.header.mapNotNull { + if (it.key == Relations.DESCRIPTION) return@mapNotNull null + it.view( + details = details, + values = details.getObject(ctx)?.map.orEmpty(), + urlBuilder = urlBuilder, + fieldParser = fieldParser, + isFeatured = true + ) + }.map { + Model.Item(it, isLocal = false) + } + + val sidebarFields = parsedFields.sidebar.mapNotNull { + if (it.key == Relations.DESCRIPTION) return@mapNotNull null + it.view( + details = details, + values = details.getObject(ctx)?.map.orEmpty(), + urlBuilder = urlBuilder, + fieldParser = fieldParser, + isFeatured = false + ) + }.map { + Model.Item(it, isLocal = false) + } + + val filesFields = parsedFields.file.mapNotNull { + if (it.key == Relations.DESCRIPTION) return@mapNotNull null + it.view( + details = details, + values = details.getObject(ctx)?.map.orEmpty(), + urlBuilder = urlBuilder, + fieldParser = fieldParser, + ) + }.map { + Model.Item(it, isLocal = false) + } + + val localFields = parsedFields.localWithoutSystem.mapNotNull { + if (it.key == Relations.DESCRIPTION) return@mapNotNull null + it.view( + details = details, + values = details.getObject(ctx)?.map.orEmpty(), + urlBuilder = urlBuilder, + fieldParser = fieldParser, + ) + }.map { + Model.Item(it, isLocal = true) + } + + return buildList { + if (headerFields.isNotEmpty()) { + add(Model.Section.Header) + addAll(headerFields) + } + + //todo file fields are off for now + if (false) { + if (sidebarFields.isNotEmpty() || filesFields.isNotEmpty()) { + add(Model.Section.SideBar) + addAll(sidebarFields + filesFields) + } + } else { + if (sidebarFields.isNotEmpty()) { + add(Model.Section.SideBar) + addAll(sidebarFields) + } + } + + if (localFields.isNotEmpty()) { + add(Model.Section.Local) + addAll(localFields) + } + } + } + private suspend fun constructViews( ctx: Id, details: ObjectViewDetails @@ -101,7 +209,7 @@ class RelationListViewModel( ).map { view -> Model.Item( view = view, - isRemovable = isPossibleToRemoveRelation(view) + isLocal = false ) } @@ -113,7 +221,7 @@ class RelationListViewModel( ).map { view -> Model.Item( view = view, - isRecommended = true + isLocal = false ) } @@ -134,15 +242,12 @@ class RelationListViewModel( return mutableListOf().apply { val (isFeatured, other) = objectRelations.partition { it.view.featured } if (isFeatured.isNotEmpty()) { - add(Model.Section.Featured) addAll(isFeatured) } if (other.isNotEmpty()) { - add(Model.Section.Other) addAll(other) } if (recommendedRelations.isNotEmpty()) { - add(Model.Section.TypeFrom(objectTypeWrapper?.name.orEmpty())) addAll(recommendedRelations) } } @@ -155,6 +260,47 @@ class RelationListViewModel( } } + fun onDismissLocalInfo() { + showLocalInfo.value = false + } + + fun onShowLocalInfo() { + showLocalInfo.value = true + } + + fun onTypeIconClicked() { + val objTypeId = _currentObjectTypeId ?: return + viewModelScope.launch { + commands.emit(Command.NavigateToObjectType(objTypeId)) + } + } + + fun onAddToTypeClicked(item: Model.Item) { + val currentObjTypeId = _currentObjectTypeId ?: return + viewModelScope.launch { + val objType = storeOfObjectTypes.get(currentObjTypeId) + if (objType != null) { + val params = SetObjectTypeRecommendedFields.Params( + objectTypeId = objType.id, + fields = objType.recommendedRelations + listOf(item.view.id) + ) + setObjectTypeRecommendedFields.async(params).fold( + onFailure = { Timber.e(it, "Error while setting recommended fields") }, + onSuccess = { + Timber.d("Successfully set recommended fields") + } + ) + } + } + } + + fun onRemoveFromObjectClicked(item: Model.Item) { + onDeleteClicked( + ctx = vmParams.objectId, + view = item.view + ) + } + fun onRelationClicked(ctx: Id, target: Id?, view: ObjectRelationView) { Timber.d("onRelationClicked, ctx: $ctx, target: $target, view: $view") viewModelScope.launch { @@ -187,7 +333,8 @@ class RelationListViewModel( } private fun checkRelationIsInObject(view: ObjectRelationView): Boolean { - val objectRelations = objectRelationListProvider.getDetails().getStruct(vmParams.objectId)?.keys + val objectRelations = + objectRelationListProvider.getDetails().getStruct(vmParams.objectId)?.keys return objectRelations?.any { it == view.key } == true } @@ -288,18 +435,19 @@ class RelationListViewModel( } fun onEditOrDoneClicked(isLocked: Boolean) { - if (isLocked) { - sendToast(RelationOperationError.LOCKED_OBJECT_MODIFICATION_ERROR) - } else { - isEditMode.value = !isEditMode.value - views.value = views.value.map { view -> - if (view is Model.Item && !view.isRecommended) { - view.copy(isRemovable = isPossibleToRemoveRelation(view.view)) - } else { - view - } - } - } + //todo legacy, remove +// if (isLocked) { +// sendToast(RelationOperationError.LOCKED_OBJECT_MODIFICATION_ERROR) +// } else { +// isEditMode.value = !isEditMode.value +// views.value = views.value.map { view -> +// if (view is Model.Item && !view.isRecommended) { +// view.copy(isRemovable = isPossibleToRemoveRelation(view.view)) +// } else { +// view +// } +// } +// } } private fun isPossibleToRemoveRelation(view: ObjectRelationView): Boolean { @@ -347,6 +495,7 @@ class RelationListViewModel( ) ) } + RelationFormat.CHECKBOX -> { if (isLocked || relation.isReadonlyValue) { _toasts.emit(NOT_ALLOWED_FOR_RELATION) @@ -355,6 +504,7 @@ class RelationListViewModel( } proceedWithTogglingRelationCheckboxValue(view, ctx) } + RelationFormat.DATE -> { if (view.readOnly || isLocked) { handleReadOnlyValue(view, relation, ctx, isLocked) @@ -362,6 +512,7 @@ class RelationListViewModel( openRelationDateScreen(relation, ctx, isLocked) } } + RelationFormat.TAG, RelationFormat.STATUS -> { commands.emit( Command.EditTagOrStatusRelationValue( @@ -373,6 +524,7 @@ class RelationListViewModel( ) ) } + RelationFormat.FILE, RelationFormat.OBJECT -> { commands.emit( @@ -386,12 +538,14 @@ class RelationListViewModel( ) ) } + RelationFormat.EMOJI, RelationFormat.RELATIONS, RelationFormat.UNDEFINED -> { _toasts.emit(NOT_SUPPORTED_UPDATE_VALUE) Timber.d("Update value of relation with format:[${relation.format}] is not supported") } + else -> {} } } @@ -439,7 +593,9 @@ class RelationListViewModel( } private fun resolveIsLockedStateOrDetailsRestriction(ctx: Id): Boolean = - lockedStateProvider.isLocked(ctx) || lockedStateProvider.isContainsDetailsRestriction() + permission.value?.isOwnerOrEditor() != true || + lockedStateProvider.isLocked(ctx) || + lockedStateProvider.isContainsDetailsRestriction() private fun proceedWithTogglingRelationCheckboxValue(view: ObjectRelationView, ctx: Id) { viewModelScope.launch { @@ -513,23 +669,22 @@ class RelationListViewModel( sealed class Model : DefaultObjectDiffIdentifier { sealed class Section : Model() { - object Featured : Section() { - override val identifier: String get() = "Section_Featured" + data object Header : Section() { + override val identifier: String get() = "Section_Header" } - object Other : Section() { - override val identifier: String get() = "Section_Other" + data object SideBar : Section() { + override val identifier: String get() = "Section_SideBar" } - data class TypeFrom(val typeName: String) : Section() { - override val identifier: String get() = "Section_TypeFrom" + data object Local : Section() { + override val identifier: String get() = "Section_Local" } } data class Item( val view: ObjectRelationView, - val isRemovable: Boolean = false, - val isRecommended: Boolean = false + val isLocal: Boolean ) : Model() { override val identifier: String get() = view.identifier } @@ -577,6 +732,10 @@ class RelationListViewModel( data class NavigateToDateObject( val objectId: Id ) : Command() + + data class NavigateToObjectType( + val objectTypeId: Id + ) : Command() } data class VmParams( diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchConstants.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchConstants.kt index d9b162cbca..e450e64f12 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchConstants.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/search/ObjectSearchConstants.kt @@ -679,6 +679,9 @@ object ObjectSearchConstants { Relations.LAST_OPENED_DATE, Relations.LAST_MODIFIED_DATE, Relations.CREATED_DATE, + Relations.COVER_TYPE, + Relations.COVER_ID, + Relations.PAGE_COVER, Relations.LINKS, Relations.BACKLINKS, Relations.LAST_USED_DATE, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt index 2a911c9d50..4ee1337cbd 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt @@ -27,4 +27,8 @@ class StringResourceProviderImpl @Inject constructor(private val context: Contex override fun getUntitledObjectTitle(): String { return context.getString(R.string.untitled) } + + override fun getSetOfObjectsTitle(): String { + return context.getString(R.string.object_set_of_title) + } } \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/MockRelationFactory.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/MockRelationFactory.kt index d257853a6f..6b9acc49b4 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/MockRelationFactory.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/MockRelationFactory.kt @@ -3,6 +3,7 @@ package com.anytypeio.anytype.presentation import com.anytypeio.anytype.core_models.ObjectTypeIds import com.anytypeio.anytype.core_models.ObjectTypeIds.OBJECT_TYPE import com.anytypeio.anytype.core_models.Relation +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.StubRelationObject import com.anytypeio.anytype.test_utils.MockDataFactory @@ -30,7 +31,7 @@ object MockRelationFactory { //LAYOUT val relationLayout = StubRelationObject( - key = "layout", + key = Relations.LAYOUT, name = "Layout", format = Relation.Format.NUMBER, isHidden = true, diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/DefaultBlockViewRendererTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/DefaultBlockViewRendererTest.kt index db243c536d..d07bd7b9ce 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/DefaultBlockViewRendererTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/DefaultBlockViewRendererTest.kt @@ -37,6 +37,7 @@ import com.anytypeio.anytype.presentation.MockBlockFactory.link import com.anytypeio.anytype.presentation.MockTypicalDocumentFactory import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider import com.anytypeio.anytype.core_models.ObjectViewDetails +import com.anytypeio.anytype.core_models.Struct import com.anytypeio.anytype.presentation.editor.editor.Markup import com.anytypeio.anytype.presentation.editor.editor.Markup.Companion.NON_EXISTENT_OBJECT_MENTION_NAME import com.anytypeio.anytype.presentation.editor.editor.model.Alignment @@ -50,6 +51,7 @@ import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.util.TXT import com.anytypeio.anytype.presentation.widgets.collection.ResourceProvider import com.anytypeio.anytype.test_utils.MockDataFactory +import kotlin.random.Random import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import net.lachlanmckee.timberjunit.TimberTestRule @@ -1824,7 +1826,7 @@ class DefaultBlockViewRendererTest { Relations.ID to mentionTarget1, "name" to "XmN34", "snippet" to mentionTextUpdated1, - "layout" to 9.0 + Relations.LAYOUT to 9.0 ) val randomEmoji2 = DefaultDocumentEmojiIconProvider.DOCUMENT_SET.random() @@ -1833,7 +1835,7 @@ class DefaultBlockViewRendererTest { Relations.ID to mentionTarget2, "name" to mentionTextUpdated2, "iconEmoji" to randomEmoji2, - "layout" to 0.0 + Relations.LAYOUT to 0.0 ) val detailsAmend = mapOf( @@ -2589,7 +2591,7 @@ class DefaultBlockViewRendererTest { "name" to name, "description" to "", "snippet" to snippet, - "layout" to ObjectType.Layout.BASIC.code.toDouble() + Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble() ) ) ) @@ -5874,4 +5876,63 @@ class DefaultBlockViewRendererTest { assertEquals(expected = expected, actual = result) } + + @Test + fun `don't render title block in case of Layout Note`() { + + val title = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Text( + text = MockDataFactory.randomString(), + style = Block.Content.Text.Style.TITLE, + marks = emptyList() + ), + children = emptyList(), + fields = Block.Fields.empty() + ) + + val header = Block( + id = MockDataFactory.randomUuid(), + content = Block.Content.Layout( + type = Block.Content.Layout.Type.HEADER + ), + fields = Block.Fields.empty(), + children = listOf(title.id) + ) + + val page = Block( + id = MockDataFactory.randomUuid(), + children = listOf(header.id), + fields = Block.Fields.empty(), + content = Block.Content.Smart + ) + + val blocks = listOf(page, header, title) + + val map = blocks.asMap() + + wrapper = BlockViewRenderWrapper( + blocks = map, + renderer = renderer + ) + + val result = runBlocking { + wrapper.render( + root = page, + anchor = page.id, + focus = Editor.Focus.empty(), + indent = 0, + details = ObjectViewDetails( + details = mapOf( + page.id to mapOf( + Relations.ID to page.id, + Relations.LAYOUT to Layout.NOTE.code.toDouble() + ) + ) + ) + ) + } + + assertEquals(expected = emptyList(), actual = result) + } } \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorFeaturedRelationsTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorFeaturedRelationsTest.kt index 0ae5f0a467..8a6274b657 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorFeaturedRelationsTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorFeaturedRelationsTest.kt @@ -995,7 +995,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { key = backlinksRelation.key, name = backlinksRelation.name.orEmpty(), featured = true, - system = false, + system = true, count = 3 ), ObjectRelationView.Links.From( @@ -1202,7 +1202,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { name = globalNameRelation.name!!, value = globalNameValue, featured = true, - system = false, + system = true, readOnly = false, format = Relation.Format.SHORT_TEXT ) @@ -1296,7 +1296,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { name = identityRelation.name!!, value = identityValue, featured = true, - system = false, + system = true, readOnly = false, format = Relation.Format.SHORT_TEXT ) @@ -1387,7 +1387,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { name = globalNameRelation.name!!, value = globalNameValue, featured = true, - system = false, + system = true, readOnly = false, format = Relation.Format.SHORT_TEXT ), @@ -1491,7 +1491,7 @@ class EditorFeaturedRelationsTest : EditorPresentationTestSetup() { name = identityRelation.name!!, value = identityValue, featured = true, - system = false, + system = true, readOnly = false, format = Relation.Format.SHORT_TEXT ), diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorObjectTypeChangeWidgetTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorObjectTypeChangeWidgetTest.kt index 862dedf114..95cbb15180 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorObjectTypeChangeWidgetTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorObjectTypeChangeWidgetTest.kt @@ -172,7 +172,7 @@ class EditorObjectTypeChangeWidgetTest : EditorPresentationTestSetup() { val objectDetails = mapOf( "type" to ObjectTypeIds.NOTE, - "layout" to ObjectType.Layout.NOTE.code.toDouble() + Relations.LAYOUT to ObjectType.Layout.NOTE.code.toDouble() ) val detailsList = ObjectViewDetails(details = mapOf(root to objectDetails)) diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/mapper/ObjectWrapperExtensionsKtTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/mapper/ObjectWrapperExtensionsKtTest.kt index 1bedfe98bd..04a45dd1fa 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/mapper/ObjectWrapperExtensionsKtTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/mapper/ObjectWrapperExtensionsKtTest.kt @@ -104,11 +104,11 @@ class ObjectWrapperExtensionsKtTest { val obj = ObjectWrapper.Basic( mapOf( - "id" to "Ef6", + Relations.ID to "Ef6", Relations.SPACE_ID to MockDataFactory.randomUuid(), - "name" to "LmL7R", - "snippet" to "OMr2Y", - "layout" to ObjectType.Layout.NOTE.code.toDouble() + Relations.NAME to "LmL7R", + Relations.SNIPPET to "OMr2Y", + Relations.LAYOUT to ObjectType.Layout.NOTE.code.toDouble() ) ) @@ -132,11 +132,11 @@ class ObjectWrapperExtensionsKtTest { val obj = ObjectWrapper.Basic( mapOf( - "id" to "Ef6", + Relations.ID to "Ef6", Relations.SPACE_ID to MockDataFactory.randomUuid(), - "name" to "LmL7R", - "snippet" to "OMr2Y", - "layout" to ObjectType.Layout.BASIC.code.toDouble() + Relations.NAME to "LmL7R", + Relations.SNIPPET to "OMr2Y", + Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble() ) ) @@ -158,14 +158,14 @@ class ObjectWrapperExtensionsKtTest { val obj = ObjectWrapper.Basic( mapOf( - "id" to "Ef6", + Relations.ID to "Ef6", Relations.SPACE_ID to MockDataFactory.randomUuid(), - "name" to "LmL7R", - "snippet" to "Anytype\nis\nnext-generation software that\n" + + Relations.NAME to "LmL7R", + Relations.SNIPPET to "Anytype\nis\nnext-generation software that\n" + "works like\nyour brain does. It solves everyday\n" + "problems\nwhile respecting your privacy and\n" + "data rights.", - "layout" to ObjectType.Layout.NOTE.code.toDouble() + Relations.LAYOUT to ObjectType.Layout.NOTE.code.toDouble() ) ) @@ -173,7 +173,7 @@ class ObjectWrapperExtensionsKtTest { val result = listOf(obj).toViews( urlBuilder = urlBuilder, objectTypes = listOf(), - fieldParser + fieldParser = fieldParser ) assertEquals( diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImplTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImplTest.kt index 63bd9d349b..33fe219bbe 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImplTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuOptionsProviderImplTest.kt @@ -32,7 +32,7 @@ class ObjectMenuOptionsProviderImplTest { val expected = ObjectMenuOptionsProvider.Options( hasIcon = false, hasCover = false, - hasLayout = true, + hasDescriptionShow = true, hasRelations = true, hasDiagnosticsVisibility = true, hasHistory = true @@ -56,10 +56,10 @@ class ObjectMenuOptionsProviderImplTest { val expected = ObjectMenuOptionsProvider.Options( hasIcon = false, hasCover = true, - hasLayout = true, hasRelations = true, hasDiagnosticsVisibility = true, - hasHistory = true + hasHistory = true, + hasDescriptionShow = true ) assertOptions( @@ -99,26 +99,6 @@ class ObjectMenuOptionsProviderImplTest { ) } - @Test - fun `when restricts layout_change - layout options is invisible`() { - details.value = ObjectViewDetails( - mapOf( - objectId to mapOf( - Relations.ID to objectId, - Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble() - ) - ) - ) - restrictions.value = listOf(ObjectRestriction.LAYOUT_CHANGE) - - assertOptions( - expected = ObjectMenuOptionsProvider.Options.ALL.copy( - hasLayout = false, - hasDiagnosticsVisibility = true - ) - ) - } - @Test fun `when object is Locked - show only relations`() { details.value = ObjectViewDetails( @@ -135,10 +115,35 @@ class ObjectMenuOptionsProviderImplTest { expected = ObjectMenuOptionsProvider.Options( hasIcon = false, hasCover = false, - hasLayout = false, hasRelations = true, hasDiagnosticsVisibility = true, - hasHistory = false + hasHistory = false, + hasDescriptionShow = true + ) + ) + } + + @Test + fun `when description is already in featured - show hide description`() { + details.value = ObjectViewDetails( + mapOf( + objectId to mapOf( + Relations.ID to objectId, + Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble(), + Relations.FEATURED_RELATIONS to listOf(Relations.DESCRIPTION) + ) + ) + ) + + assertOptions( + isLocked = true, + expected = ObjectMenuOptionsProvider.Options( + hasIcon = false, + hasCover = false, + hasRelations = true, + hasDiagnosticsVisibility = true, + hasHistory = false, + hasDescriptionShow = false ) ) } diff --git a/sample/build.gradle b/sample/build.gradle index c013b9aa40..3b8ca153d1 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -65,7 +65,7 @@ dependencies { implementation libs.composeAccompanistPagerIndicators implementation libs.preference implementation libs.activityCompose - implementation libs.composeReorderable + implementation libs.composeReorderableLegacy debugImplementation libs.composeTooling //implementation 'com.github.gregcockroft:AndroidMath:ALPHA' diff --git a/settings.gradle b/settings.gradle index dc3173e98b..8e8d4d701c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -66,3 +66,4 @@ include ':gallery-experience' include ':feature-chats' include ':feature-all-content' include ':feature-date' +include ':feature-object-type' diff --git a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt index 15e4ffbb9d..236f283c94 100644 --- a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt +++ b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt @@ -215,16 +215,6 @@ fun StubCallout( backgroundColor = backgroundColor ) -fun StubRelation( - relationKey: String = MockDataFactory.randomString(), - format: RelationFormat = Relation.Format.SHORT_TEXT -): Relation = Relation( - key = relationKey, - name = MockDataFactory.randomString(), - format = format, - source = Relation.Source.values().random() -) - fun StubBookmark( id: Id = MockDataFactory.randomString(), url: Url? = MockDataFactory.randomString(), diff --git a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Object.kt b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Object.kt index 34b79cf637..dabc41910f 100644 --- a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Object.kt +++ b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Object.kt @@ -24,6 +24,7 @@ fun StubObject( targetObjectType: Id? = null, identity: Id? = null, fileExt: String? = null, + extraFields: Map = emptyMap() ): ObjectWrapper.Basic = ObjectWrapper.Basic( map = mapOf( Relations.ID to id, @@ -43,7 +44,7 @@ fun StubObject( Relations.UNIQUE_KEY to uniqueKey, Relations.IDENTITY to identity, Relations.FILE_EXT to fileExt - ) + ) + extraFields ) fun StubSpaceMember( @@ -104,7 +105,11 @@ fun StubObjectType( isReadOnly: Boolean? = null, isHidden: Boolean? = null, sourceObject: Id? = null, - recommendedLayout: Double? = null + recommendedLayout: Double? = null, + recommendedRelations: List = emptyList(), + recommendedHiddenRelations: List = emptyList(), + recommendedFeaturedRelations: List = emptyList(), + space: Id? = null ): ObjectWrapper.Type = ObjectWrapper.Type( map = mapOf( Relations.ID to id, @@ -121,5 +126,9 @@ fun StubObjectType( Relations.SOURCE_OBJECT to sourceObject, Relations.RECOMMENDED_LAYOUT to recommendedLayout, Relations.UNIQUE_KEY to uniqueKey, + Relations.RECOMMENDED_RELATIONS to recommendedRelations, + Relations.RECOMMENDED_HIDDEN_RELATIONS to recommendedHiddenRelations, + Relations.RECOMMENDED_FEATURED_RELATIONS to recommendedFeaturedRelations, + Relations.SPACE_ID to space ) ) \ No newline at end of file diff --git a/versioning.gradle b/versioning.gradle index efc8075083..ec94bd3931 100644 --- a/versioning.gradle +++ b/versioning.gradle @@ -98,7 +98,7 @@ ext.getBuildVersionName = { def date = getCurrentDate() return "${versionMajor}.${versionMinor}.${versionPatch}-${date}" } else { - return "${versionMajor}.${versionMinor}.${versionPatch}-alpha" + return "${versionMajor}.${versionMinor}.${versionPatch}" } }