diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1817fa400d..deaa02d2c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,6 +55,9 @@ jobs: -Pcom.anytype.ci=true \ -Dorg.gradle.unsafe.configuration-cache=false" + - name: Prepare Android Manifest for APKs + run: ./scripts/release/apk.sh + - name: Build release APKS run: ./gradlew :app:assembleRelease diff --git a/Makefile b/Makefile index 29ce2c0d3f..3f6f0c72e6 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ enable_dated_version_name: ./gradlew -q :app:enableDatedVersionName distribute_debug: - ./gradlew bundleDebug appDistributionUploadDebug + ./gradlew assembleDebug appDistributionUploadDebug pr_check: compile_android_test_sources test_debug_all @@ -47,4 +47,7 @@ clean_protos: update_mw: download_mw_artefacts normalize_mw_imports clean_protos # Update mw from custom build (download only library, you have to update your proto files manually) -update_mw_custom: download_mw_artefacts_custom \ No newline at end of file +update_mw_custom: download_mw_artefacts_custom + +prepare_app_manifest_for_release_apk: + ./scripts/release/apk.sh 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 3b8843eb6b..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 @@ -191,6 +191,8 @@ object EventsDictionary { const val CLICK_ONBOARDING_TOOLTIP_TYPE_CLOSE = "Close" // Sharing spaces + + const val clickQuote = "ClickQuote" const val shareSpace = "ShareSpace" const val screenSettingsSpaceShare = "ScreenSettingsSpaceShare" const val screenStopShare = "ScreenStopShare" @@ -232,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 8787b8b41b..4c27f63cf3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -112,7 +112,7 @@ android { buildConfigField("String", "AMPLITUDE_KEY", apikeyProperties['amplitude.debug']) //signingConfig signingConfigs.debug firebaseAppDistribution { - artifactType = "AAB" + artifactType = "APK" groups = "anytype-q&a, product-review, nightly" serviceCredentialsFile = "$rootDir/scripts/distribution/anytype-debug-service-account-key.json" } @@ -143,7 +143,7 @@ android { // Configures multiple APKs based on ABI. abi { // Enables building multiple APKs per ABI. - enable true + enable false reset() include "armeabi-v7a", "arm64-v8a" universalApk true @@ -177,6 +177,8 @@ dependencies { implementation project(':gallery-experience') implementation project(':feature-all-content') implementation project(':feature-date') + implementation project(':feature-object-type') + implementation project(':feature-properties') //Compile time dependencies ksp libs.daggerCompiler @@ -224,7 +226,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/gradle.properties b/app/gradle.properties index b6e8f5a1a9..71fd3854a2 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -1,4 +1,4 @@ version.versionMajor=0 -version.versionMinor=36 +version.versionMinor=37 version.versionPatch=0 version.useDatedVersionName=false \ No newline at end of file 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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c35facb7c0..25535540d3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,9 @@ + + + @@ -50,6 +53,19 @@ + + + + + + + + + + + + + diff --git a/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt b/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt index 0b4735a503..01626c1c89 100644 --- a/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt +++ b/app/src/main/java/com/anytypeio/anytype/app/Notifications.kt @@ -83,11 +83,14 @@ class AnytypeNotificationService @Inject constructor( ) } is NotificationPayload.ParticipantRemove -> { + val placeholder = context.resources.getString(R.string.untitled) val body = context.resources.getString( - R.string.multiplayer_notification_member_removed_from_space + R.string.multiplayer_notification_member_removed_from_space, + payload.spaceName.ifEmpty { placeholder } ) val title = context.resources.getString( - R.string.multiplayer_notification_member_removed_from_space_title + R.string.multiplayer_notification_member_removed_from_space_title, + payload.spaceName.ifEmpty { placeholder } ) showBasicNotification( tag = notification.id, @@ -143,7 +146,7 @@ class AnytypeNotificationService @Inject constructor( R.string.multiplayer_notification_request_declined ) val body = context.resources.getString( - com.anytypeio.anytype.core_ui.R.string.multiplayer_notification_member_join_request_declined, + R.string.multiplayer_notification_member_join_request_declined, payload.spaceName.ifEmpty { placeholder } ) showBasicNotification( diff --git a/app/src/main/java/com/anytypeio/anytype/device/DeviceCoverCollectionProvider.kt b/app/src/main/java/com/anytypeio/anytype/device/DeviceCoverCollectionProvider.kt deleted file mode 100644 index e8798f32df..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/device/DeviceCoverCollectionProvider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.anytypeio.anytype.device - -import android.content.Context -import com.anytypeio.anytype.core_utils.ext.getJsonDataFromAsset -import com.anytypeio.anytype.domain.cover.CoverCollectionProvider -import com.anytypeio.anytype.domain.cover.CoverImage -import com.google.gson.Gson - -class DeviceCoverCollectionProvider( - private val context: Context, - private val gson: Gson -) : CoverCollectionProvider { - - override fun provide(): List { - val json = context.getJsonDataFromAsset(COVER_FILE) - return if (json != null) { - gson.fromJson(json, Array::class.java).toList() - } else { - emptyList() - } - } - - companion object { - const val COVER_FILE = "covers.json" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/device/PhotoPickerExt.kt b/app/src/main/java/com/anytypeio/anytype/device/PhotoPickerExt.kt new file mode 100644 index 0000000000..ad314aa369 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/device/PhotoPickerExt.kt @@ -0,0 +1,56 @@ +package com.anytypeio.anytype.device + +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType +import androidx.fragment.app.Fragment +import com.anytypeio.anytype.core_utils.ext.Mimetype +import com.anytypeio.anytype.other.MediaPermissionHelper +import com.anytypeio.anytype.ui.editor.PickerDelegate +import timber.log.Timber + +/** + * Launches a media picker (for images or videos) in a [Fragment]. + * + * This function checks if the device supports the photo picker. If available, + * it launches the [pickMedia] launcher with a request for the specified [mediaType]. + * If the picker is not available, it falls back to opening a file picker using [pickerDelegate] + * with the provided [fallbackMimeType]. + * + * @param pickMedia The [ActivityResultLauncher] used to launch the media picker. + * @param pickerDelegate A delegate to open a fallback file picker when the media picker is unavailable. + * @param mediaType The type of media to pick (e.g. [PickVisualMedia.ImageOnly] or [PickVisualMedia.VideoOnly]). + * @param fallbackMimeType The MIME type to use with the fallback file picker (e.g. [Mimetype.MIME_IMAGE_ALL] or [Mimetype.MIME_VIDEO_ALL]). + */ +fun Fragment.launchMediaPicker( + pickMedia: ActivityResultLauncher, + pickerDelegate: PickerDelegate, + mediaType: VisualMediaType, + fallbackMimeType: Mimetype +) { + context?.let { ctx -> + if (PickVisualMedia.isPhotoPickerAvailable(ctx)) { + pickMedia.launch(PickVisualMediaRequest(mediaType)) + } else { + Timber.w("$mediaType picker is not available, using pickerDelegate") + pickerDelegate.openFilePicker(fallbackMimeType, null) + } + } +} + +fun Fragment.launchMediaPicker( + pickMedia: ActivityResultLauncher, + permissionHelper: MediaPermissionHelper, + mediaType: VisualMediaType, + fallbackMimeType: Mimetype +) { + context?.let { ctx -> + if (PickVisualMedia.isPhotoPickerAvailable(ctx)) { + pickMedia.launch(PickVisualMediaRequest(mediaType)) + } else { + Timber.w("$mediaType picker is not available, using pickerDelegate") + permissionHelper.openFilePicker(fallbackMimeType, null) + } + } +} \ No newline at end of file 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 73319f70e3..94095c1603 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 @@ -9,8 +9,10 @@ import com.anytypeio.anytype.di.feature.DaggerAllContentComponent import com.anytypeio.anytype.di.feature.DaggerAppPreferencesComponent import com.anytypeio.anytype.di.feature.DaggerBacklinkOrAddToObjectComponent import com.anytypeio.anytype.di.feature.DaggerDateObjectComponent +import com.anytypeio.anytype.di.feature.DaggerEditTypePropertiesComponent 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 @@ -49,9 +51,9 @@ import com.anytypeio.anytype.di.feature.ViewerFilterModule import com.anytypeio.anytype.di.feature.ViewerSortModule import com.anytypeio.anytype.di.feature.auth.DaggerDeletedAccountComponent import com.anytypeio.anytype.di.feature.chats.DaggerChatComponent -import com.anytypeio.anytype.di.feature.cover.UnsplashModule import com.anytypeio.anytype.di.feature.chats.DaggerChatReactionComponent import com.anytypeio.anytype.di.feature.chats.DaggerSelectChatReactionComponent +import com.anytypeio.anytype.di.feature.cover.UnsplashModule import com.anytypeio.anytype.di.feature.gallery.DaggerGalleryInstallationComponent import com.anytypeio.anytype.di.feature.home.DaggerHomeScreenComponent import com.anytypeio.anytype.di.feature.membership.DaggerMembershipComponent @@ -82,6 +84,7 @@ import com.anytypeio.anytype.di.feature.sets.PickConditionModule import com.anytypeio.anytype.di.feature.sets.SelectFilterRelationModule import com.anytypeio.anytype.di.feature.settings.DaggerAboutAppComponent import com.anytypeio.anytype.di.feature.settings.DaggerAppearanceComponent +import com.anytypeio.anytype.di.feature.settings.DaggerDebugComponent import com.anytypeio.anytype.di.feature.settings.DaggerFilesStorageComponent import com.anytypeio.anytype.di.feature.settings.DaggerSpacesStorageComponent import com.anytypeio.anytype.di.feature.settings.LogoutWarningModule @@ -96,17 +99,18 @@ import com.anytypeio.anytype.di.feature.templates.DaggerTemplateSelectComponent import com.anytypeio.anytype.di.feature.types.DaggerCreateObjectTypeComponent import com.anytypeio.anytype.di.feature.types.DaggerTypeEditComponent import com.anytypeio.anytype.di.feature.types.DaggerTypeIconPickComponent -import com.anytypeio.anytype.di.feature.update.DaggerMigrationErrorComponent import com.anytypeio.anytype.di.feature.vault.DaggerVaultComponent import com.anytypeio.anytype.di.feature.wallpaper.WallpaperSelectModule import com.anytypeio.anytype.di.feature.widgets.DaggerSelectWidgetSourceComponent import com.anytypeio.anytype.di.feature.widgets.DaggerSelectWidgetTypeComponent import com.anytypeio.anytype.di.main.MainComponent import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel -import com.anytypeio.anytype.feature_date.viewmodel.DateObjectVmParams 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.feature_properties.add.EditTypePropertiesVmParams import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel import com.anytypeio.anytype.presentation.editor.EditorViewModel import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel @@ -149,6 +153,12 @@ class ComponentManager( .build() } + val debugComponent = Component { + DaggerDebugComponent + .factory() + .create(findComponentDependencies()) + } + val splashLoginComponent = Component { DaggerSplashComponent .factory() @@ -834,12 +844,6 @@ class ComponentManager( .create(findComponentDependencies()) } - val migrationErrorComponent = Component { - DaggerMigrationErrorComponent - .factory() - .create(findComponentDependencies()) - } - val onboardingComponent = Component { DaggerOnboardingComponent .factory() @@ -1133,6 +1137,18 @@ class ComponentManager( .create(findComponentDependencies()) } + val objectTypeComponent = ComponentWithParams { params: ObjectTypeVmParams -> + DaggerObjectTypeComponent + .factory() + .create(params, findComponentDependencies()) + } + + val editTypePropertiesComponent = ComponentWithParams { params: EditTypePropertiesVmParams -> + DaggerEditTypePropertiesComponent + .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/MainEntryDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt index 6629a9b244..956ced5904 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/MainEntryDI.kt @@ -75,7 +75,9 @@ object MainEntryModule { awaitAccountStartManager: AwaitAccountStartManager, membershipProvider: MembershipProvider, globalSubscriptionManager: GlobalSubscriptionManager, - spaceInviteResolver: SpaceInviteResolver + spaceInviteResolver: SpaceInviteResolver, + spaceManager: SpaceManager, + spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer ): MainViewModelFactory = MainViewModelFactory( resumeAccount = resumeAccount, analytics = analytics, @@ -93,7 +95,9 @@ object MainEntryModule { awaitAccountStartManager = awaitAccountStartManager, membershipProvider = membershipProvider, globalSubscriptionManager = globalSubscriptionManager, - spaceInviteResolver = spaceInviteResolver + spaceInviteResolver = spaceInviteResolver, + spaceManager = spaceManager, + spaceViews = spaceViewSubscriptionContainer ) @JvmStatic 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..0bcd0f3b0b --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/PrimitivesObjectTypeDI.kt @@ -0,0 +1,166 @@ +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.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.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.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.ui.ObjectTypeVmParams +import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeVMFactory +import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate +import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider +import com.anytypeio.anytype.providers.DefaultCoverImageHashProvider +import com.anytypeio.anytype.ui.primitives.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 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 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/feature/PropertiesDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/PropertiesDI.kt new file mode 100644 index 0000000000..c588f67aeb --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/PropertiesDI.kt @@ -0,0 +1,88 @@ +package com.anytypeio.anytype.di.feature + +import androidx.lifecycle.ViewModelProvider +import com.anytypeio.anytype.core_utils.di.scope.PerModal +import com.anytypeio.anytype.di.common.ComponentDependencies +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.`object`.SetObjectDetails +import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes +import com.anytypeio.anytype.domain.objects.StoreOfRelations +import com.anytypeio.anytype.domain.primitives.SetObjectTypeRecommendedFields +import com.anytypeio.anytype.domain.relations.CreateRelation +import com.anytypeio.anytype.domain.resources.StringResourceProvider +import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModelFactory +import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams +import com.anytypeio.anytype.ui.primitives.EditTypePropertiesFragment +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides + +//region EDIT OBJECT TYPE PROPERTIES SCREEN +@PerModal +@Component( + modules = [ + EditTypePropertiesModule::class, + EditTypePropertiesModule.Declarations::class + ], + dependencies = [EditTypePropertiesDependencies::class] +) +interface EditTypePropertiesComponent { + @Component.Factory + interface Factory { + fun create( + @BindsInstance vmParams: EditTypePropertiesVmParams, + dependencies: EditTypePropertiesDependencies + ): EditTypePropertiesComponent + } + + fun inject(fragment: EditTypePropertiesFragment) +} + +@Module +object EditTypePropertiesModule { + + @JvmStatic + @Provides + @PerModal + fun provideTypeSetRecommendedFields( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): SetObjectTypeRecommendedFields = SetObjectTypeRecommendedFields(repo, dispatchers) + + @JvmStatic + @Provides + @PerModal + fun createRelation( + repo: BlockRepository, + storeOfRelations: StoreOfRelations, + ) = CreateRelation(repo, storeOfRelations) + + @JvmStatic + @Provides + @PerModal + fun provideSetObjectDetails( + repo: BlockRepository, + dispatchers: AppCoroutineDispatchers + ): SetObjectDetails = SetObjectDetails(repo, dispatchers) + + @Module + interface Declarations { + @PerModal + @Binds + fun bindViewModelFactory( + factory: EditTypePropertiesViewModelFactory + ): ViewModelProvider.Factory + } +} + +interface EditTypePropertiesDependencies : ComponentDependencies { + fun provideStringResourceProvider(): StringResourceProvider + fun provideStoreOfRelations(): StoreOfRelations + fun provideStoreOfObjectTypes(): StoreOfObjectTypes + fun provideBlockRepository(): BlockRepository + fun provideAppCoroutineDispatchers(): AppCoroutineDispatchers +} +//endregion \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/SplashDi.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/SplashDi.kt index 62cdf8fffe..784dd274fc 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/SplashDi.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/SplashDi.kt @@ -33,6 +33,7 @@ import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager import com.anytypeio.anytype.domain.templates.GetTemplates import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate +import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate import com.anytypeio.anytype.presentation.splash.SplashViewModelFactory import com.anytypeio.anytype.ui.splash.SplashFragment import dagger.Binds @@ -178,6 +179,12 @@ object SplashModule { @PerScreen @Binds fun bindViewModelFactory(factory: SplashViewModelFactory): ViewModelProvider.Factory + + @Binds + @PerScreen + fun bindMigrationHelperDelegate( + impl: MigrationHelperDelegate.Impl + ): MigrationHelperDelegate } } diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatReactionDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatReactionDI.kt index 5bb8654b8f..fee6b9ad75 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatReactionDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatReactionDI.kt @@ -3,6 +3,7 @@ package com.anytypeio.anytype.di.feature.chats import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.core_utils.di.scope.PerScreen import com.anytypeio.anytype.di.common.ComponentDependencies +import com.anytypeio.anytype.domain.auth.repo.AuthRepository import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.misc.UrlBuilder @@ -50,6 +51,7 @@ object ChatReactionModule { interface ChatReactionDependencies : ComponentDependencies { fun dispatchers(): AppCoroutineDispatchers fun repo(): BlockRepository + fun auth(): AuthRepository fun urlBuilder(): UrlBuilder fun members(): ActiveSpaceMemberSubscriptionContainer } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/login/OnboardingMnemonicLoginDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/login/OnboardingMnemonicLoginDI.kt index 65b6d247a8..a2d8ee263c 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/login/OnboardingMnemonicLoginDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/onboarding/login/OnboardingMnemonicLoginDI.kt @@ -22,6 +22,7 @@ import com.anytypeio.anytype.domain.platform.InitialParamsProvider import com.anytypeio.anytype.domain.spaces.SpaceDeletedStatusWatcher import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager import com.anytypeio.anytype.domain.workspace.SpaceManager +import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider import com.anytypeio.anytype.providers.DefaultUriFileProvider @@ -74,6 +75,12 @@ object OnboardingMnemonicLoginModule { defaultProvider: DefaultUriFileProvider ): UriFileProvider + @Binds + @PerScreen + fun bindMigrationHelperDelegate( + impl: MigrationHelperDelegate.Impl + ): MigrationHelperDelegate + @Binds @PerScreen fun bindViewModelFactory( diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/settings/DebugDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/DebugDI.kt new file mode 100644 index 0000000000..720f511042 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/DebugDI.kt @@ -0,0 +1,49 @@ +package com.anytypeio.anytype.di.feature.settings + +import androidx.lifecycle.ViewModelProvider +import com.anytypeio.anytype.core_utils.di.scope.PerScreen +import com.anytypeio.anytype.di.common.ComponentDependencies +import com.anytypeio.anytype.domain.auth.repo.AuthRepository +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.device.PathProvider +import com.anytypeio.anytype.presentation.settings.DebugViewModel +import com.anytypeio.anytype.ui.settings.DebugFragment +import dagger.Binds +import dagger.Component +import dagger.Module + +@Component( + dependencies = [DebugDependencies::class], + modules = [ + DebugModule::class, + DebugModule.Declarations::class + ] +) +@PerScreen +interface DebugComponent { + + @Component.Factory + interface Factory { + fun create(dependencies: DebugDependencies): DebugComponent + } + + fun inject(fragment: DebugFragment) +} + +@Module +object DebugModule { + @Module + interface Declarations { + @PerScreen + @Binds + fun bindViewModelFactory( + factory: DebugViewModel.Factory + ): ViewModelProvider.Factory + } +} + +interface DebugDependencies : ComponentDependencies { + fun path(): PathProvider + fun auth(): AuthRepository + fun dispatchers(): AppCoroutineDispatchers +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/update/MigrationErrorDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/update/MigrationErrorDI.kt deleted file mode 100644 index 2757859829..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/update/MigrationErrorDI.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.anytypeio.anytype.di.feature.update - -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.presentation.update.MigrationErrorViewModel -import com.anytypeio.anytype.ui.update.MigrationErrorFragment -import dagger.Binds -import dagger.Component -import dagger.Module - -@Component( - dependencies = [MigrationErrorDependencies::class], - modules = [ - MigrationErrorModule::class, - MigrationErrorModule.Declarations::class - ] -) -@PerScreen -interface MigrationErrorComponent { - - @Component.Factory - interface Factory { - fun create(dependencies: MigrationErrorDependencies): MigrationErrorComponent - } - - fun inject(fragment: MigrationErrorFragment) -} - -@Module -object MigrationErrorModule { - - @Module - interface Declarations { - - @PerScreen - @Binds - fun bindViewModelFactory( - factory: MigrationErrorViewModel.Factory - ): ViewModelProvider.Factory - - } -} - -interface MigrationErrorDependencies : ComponentDependencies { - fun analytics(): Analytics -} \ 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 d8e7fc314a..a49364bf25 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 @@ -3,6 +3,7 @@ package com.anytypeio.anytype.di.main import com.anytypeio.anytype.app.AndroidApplication import com.anytypeio.anytype.di.common.ComponentDependencies import com.anytypeio.anytype.di.common.ComponentDependenciesKey +import com.anytypeio.anytype.di.feature.EditTypePropertiesDependencies import com.anytypeio.anytype.di.feature.AllContentDependencies import com.anytypeio.anytype.di.feature.AppPreferencesDependencies import com.anytypeio.anytype.di.feature.BacklinkOrAddToObjectDependencies @@ -17,12 +18,13 @@ 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 +import com.anytypeio.anytype.di.feature.chats.ChatComponentDependencies import com.anytypeio.anytype.di.feature.chats.ChatReactionDependencies import com.anytypeio.anytype.di.feature.chats.SelectChatReactionDependencies -import com.anytypeio.anytype.di.feature.chats.ChatComponentDependencies import com.anytypeio.anytype.di.feature.gallery.GalleryInstallationComponentDependencies import com.anytypeio.anytype.di.feature.home.HomeScreenDependencies import com.anytypeio.anytype.di.feature.membership.MembershipComponentDependencies @@ -43,6 +45,7 @@ import com.anytypeio.anytype.di.feature.relations.RelationEditDependencies import com.anytypeio.anytype.di.feature.search.GlobalSearchDependencies import com.anytypeio.anytype.di.feature.settings.AboutAppDependencies import com.anytypeio.anytype.di.feature.settings.AppearanceDependencies +import com.anytypeio.anytype.di.feature.settings.DebugDependencies import com.anytypeio.anytype.di.feature.settings.FilesStorageDependencies import com.anytypeio.anytype.di.feature.settings.LogoutWarningSubComponent import com.anytypeio.anytype.di.feature.settings.ProfileSubComponent @@ -57,7 +60,6 @@ import com.anytypeio.anytype.di.feature.templates.TemplateSelectDependencies import com.anytypeio.anytype.di.feature.types.CreateObjectTypeDependencies import com.anytypeio.anytype.di.feature.types.TypeEditDependencies import com.anytypeio.anytype.di.feature.types.TypeIconPickDependencies -import com.anytypeio.anytype.di.feature.update.MigrationErrorDependencies import com.anytypeio.anytype.di.feature.vault.VaultComponentDependencies import com.anytypeio.anytype.di.feature.wallpaper.WallpaperSelectSubComponent import com.anytypeio.anytype.di.feature.widgets.SelectWidgetSourceDependencies @@ -104,7 +106,6 @@ interface MainComponent : RelationEditDependencies, SplashDependencies, DeletedAccountDependencies, - MigrationErrorDependencies, BacklinkOrAddToObjectDependencies, FilesStorageDependencies, OnboardingDependencies, @@ -139,9 +140,12 @@ interface MainComponent : LinkToObjectDependencies, MoveToDependencies, DateObjectDependencies, + ObjectTypeDependencies, SelectChatReactionDependencies, ChatReactionDependencies, - ParticipantComponentDependencies + ParticipantComponentDependencies, + EditTypePropertiesDependencies, + DebugDependencies { fun inject(app: AndroidApplication) @@ -218,11 +222,6 @@ abstract class ComponentDependenciesModule { @ComponentDependenciesKey(DeletedAccountDependencies::class) abstract fun provideDeletedAccountDependencies(component: MainComponent): ComponentDependencies - @Binds - @IntoMap - @ComponentDependenciesKey(MigrationErrorDependencies::class) - abstract fun migrationErrorDependencies(component: MainComponent): ComponentDependencies - @Binds @IntoMap @ComponentDependenciesKey(BacklinkOrAddToObjectDependencies::class) @@ -393,6 +392,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) @@ -407,4 +411,14 @@ abstract class ComponentDependenciesModule { @IntoMap @ComponentDependenciesKey(ParticipantComponentDependencies::class) abstract fun provideParticipantComponentDependencies(component: MainComponent): ComponentDependencies + + @Binds + @IntoMap + @ComponentDependenciesKey(DebugDependencies::class) + abstract fun provideDebugDependencies(component: MainComponent): ComponentDependencies + + @Binds + @IntoMap + @ComponentDependenciesKey(EditTypePropertiesDependencies::class) + abstract fun provideEditTypePropertiesDependencies(component: MainComponent): ComponentDependencies } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt index 055260ce89..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 @@ -258,18 +260,6 @@ class Navigator : AppNavigation { navController?.navigate(R.id.actionLogout) } - override fun migrationErrorScreen() { - navController?.navigate(R.id.migrationNeededScreen) - } - - override fun exitFromMigrationScreen() { - navController?.navigate(R.id.onboarding_nav, null, navOptions { - popUpTo(R.id.migrationNeededScreen) { - inclusive = true - } - }) - } - override fun openRemoteFilesManageScreen(subscription: Id, space: Id) { navController?.navigate( resId = R.id.remoteStorageFragment, @@ -369,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/other/Deeplinks.kt b/app/src/main/java/com/anytypeio/anytype/other/Deeplinks.kt index cc10d296a9..977c80d1e6 100644 --- a/app/src/main/java/com/anytypeio/anytype/other/Deeplinks.kt +++ b/app/src/main/java/com/anytypeio/anytype/other/Deeplinks.kt @@ -6,29 +6,31 @@ import com.anytypeio.anytype.core_models.Url import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.domain.misc.DeepLinkResolver import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver +import timber.log.Timber const val DEEP_LINK_PATTERN = "anytype://" const val DEEP_LINK_INVITE_DOMAIN = "invite.any.coop" +const val DEEP_LINK_TO_OBJECT_BASE_URL = "https://object.any.coop" + /** * Regex pattern for matching */ const val DEEP_LINK_INVITE_REG_EXP = "invite.any.coop/([a-zA-Z0-9]+)#([a-zA-Z0-9]+)" +const val DEEP_LINK_TO_OBJECT_REG_EXP = """object\.any\.coop/([a-zA-Z0-9?=&._-]+)""" const val DEE_LINK_INVITE_CUSTOM_REG_EXP = "anytype://invite/\\?cid=([a-zA-Z0-9]+)&key=([a-zA-Z0-9]+)" const val MAIN_PATH = "main" const val OBJECT_PATH = "object" const val IMPORT_PATH = "import" -const val INVITE_PATH = "invite" const val MEMBERSHIP_PATH = "membership" const val TYPE_PARAM = "type" const val OBJECT_ID_PARAM = "objectId" const val SPACE_ID_PARAM = "spaceId" -const val CONTENT_ID_PARAM = "cid" -const val ENCRYPTION_KEY_PARAM = "key" +const val INVITE_ID_PARAM = "inviteID" const val SOURCE_PARAM = "source" const val TYPE_VALUE_EXPERIENCE = "experience" const val TIER_ID_PARAM = "tier" @@ -38,62 +40,82 @@ const val IMPORT_EXPERIENCE_DEEPLINK = "$DEEP_LINK_PATTERN$MAIN_PATH/$IMPORT_PAT object DefaultDeepLinkResolver : DeepLinkResolver { private val defaultInviteRegex = Regex(DEEP_LINK_INVITE_REG_EXP) + private val defaultLinkToObjectRegex = Regex(DEEP_LINK_TO_OBJECT_REG_EXP) - override fun resolve( - deeplink: String - ): DeepLinkResolver.Action = when { - deeplink.contains(IMPORT_EXPERIENCE_DEEPLINK) -> { - try { - val type = Uri.parse(deeplink).getQueryParameter(TYPE_PARAM) - val source = Uri.parse(deeplink).getQueryParameter(SOURCE_PARAM) - DeepLinkResolver.Action.Import.Experience( - type = type.orEmpty(), - source = source.orEmpty() - ) - } catch (e: Exception) { - DeepLinkResolver.Action.Unknown - } + override fun resolve(deeplink: String): DeepLinkResolver.Action { + val uri = Uri.parse(deeplink) + + return when { + deeplink.contains(IMPORT_EXPERIENCE_DEEPLINK) -> resolveImportExperience(uri) + defaultInviteRegex.containsMatchIn(deeplink) -> DeepLinkResolver.Action.Invite(deeplink) + defaultLinkToObjectRegex.containsMatchIn(deeplink) -> resolveDeepLinkToObject(uri) + deeplink.contains(OBJECT_PATH) -> resolveObjectPath(uri) + deeplink.contains(MEMBERSHIP_PATH) -> resolveMembershipPath(uri) + else -> DeepLinkResolver.Action.Unknown + }.also { + Timber.d("Resolving deep link: $deeplink") } - deeplink.contains(INVITE_PATH) -> { - DeepLinkResolver.Action.Invite(deeplink) + } + + private fun resolveImportExperience(uri: Uri): DeepLinkResolver.Action { + return try { + val type = uri.getQueryParameter(TYPE_PARAM).orEmpty() + val source = uri.getQueryParameter(SOURCE_PARAM).orEmpty() + DeepLinkResolver.Action.Import.Experience(type, source) + } catch (e: Exception) { + DeepLinkResolver.Action.Unknown } - defaultInviteRegex.containsMatchIn(deeplink) -> { - DeepLinkResolver.Action.Invite(deeplink) - } - deeplink.contains(OBJECT_PATH) -> { - val uri = Uri.parse(deeplink) - val obj = uri.getQueryParameter(OBJECT_ID_PARAM) - val space = uri.getQueryParameter(SPACE_ID_PARAM) - if (!obj.isNullOrEmpty() && !space.isNullOrEmpty()) { - val cid = uri.getQueryParameter(CONTENT_ID_PARAM) - val key = uri.getQueryParameter(ENCRYPTION_KEY_PARAM) - DeepLinkResolver.Action.DeepLinkToObject( - obj = obj, - space = SpaceId(space), - invite = if (!cid.isNullOrEmpty() && !key.isNullOrEmpty()) { - DeepLinkResolver.Action.DeepLinkToObject.Invite( - cid = cid, - key = key - ) - } else { - null - } - ) - } else { - DeepLinkResolver.Action.Unknown - } - } - deeplink.contains(MEMBERSHIP_PATH) -> { - val uri = Uri.parse(deeplink) - DeepLinkResolver.Action.DeepLinkToMembership( - tierId = uri.getQueryParameter(TIER_ID_PARAM) + } + + private fun resolveDeepLinkToObject(uri: Uri): DeepLinkResolver.Action { + val obj = uri.pathSegments.getOrNull(0) ?: return DeepLinkResolver.Action.Unknown + val space = uri.getQueryParameter(SPACE_ID_PARAM)?.takeIf { it.isNotEmpty() } + ?: return DeepLinkResolver.Action.Unknown // Ensure spaceId is required + + return DeepLinkResolver.Action.DeepLinkToObject( + obj = obj, + space = SpaceId(space), + invite = parseInvite(uri) + ) + } + + private fun resolveObjectPath(uri: Uri): DeepLinkResolver.Action { + val obj = uri.getQueryParameter(OBJECT_ID_PARAM)?.takeIf { it.isNotEmpty() } + val space = uri.getQueryParameter(SPACE_ID_PARAM)?.takeIf { it.isNotEmpty() } + ?: return DeepLinkResolver.Action.Unknown // Ensure spaceId is required + + return if (obj != null) { + DeepLinkResolver.Action.DeepLinkToObject( + obj = obj, + space = SpaceId(space), + invite = parseInvite(uri) ) + } else { + DeepLinkResolver.Action.Unknown + } + } + + private fun resolveMembershipPath(uri: Uri): DeepLinkResolver.Action { + return DeepLinkResolver.Action.DeepLinkToMembership( + tierId = uri.getQueryParameter(TIER_ID_PARAM) + ) + } + + private fun parseInvite(uri: Uri): DeepLinkResolver.Action.DeepLinkToObject.Invite? { + val inviteId = uri.getQueryParameter(INVITE_ID_PARAM)?.takeIf { it.isNotEmpty() } + val encryption = uri.fragment?.takeIf { it.isNotEmpty() } + return if (inviteId != null && encryption != null) { + DeepLinkResolver.Action.DeepLinkToObject.Invite( + key = encryption, + cid = inviteId + ) + } else { + null } - else -> DeepLinkResolver.Action.Unknown } override fun createObjectDeepLink(obj: Id, space: SpaceId): Url { - return "${DEEP_LINK_PATTERN}${OBJECT_PATH}?${OBJECT_ID_PARAM}=$obj&${SPACE_ID_PARAM}=${space.id}" + return "$DEEP_LINK_TO_OBJECT_BASE_URL/$obj?$SPACE_ID_PARAM=${space.id}" } override fun createObjectDeepLinkWithInvite( @@ -102,7 +124,7 @@ object DefaultDeepLinkResolver : DeepLinkResolver { invite: Id, encryptionKey: String ): Url { - return "${DEEP_LINK_PATTERN}${OBJECT_PATH}?${OBJECT_ID_PARAM}=$obj&${SPACE_ID_PARAM}=${space.id}&${DefaultSpaceInviteResolver.CONTENT_ID_KEY}=$invite&${DefaultSpaceInviteResolver.FILE_KEY_KEY}=$encryptionKey" + return "${DEEP_LINK_TO_OBJECT_BASE_URL}/$obj?${SPACE_ID_PARAM}=${space.id}&${INVITE_ID_PARAM}=$invite#$encryptionKey" } override fun isDeepLink(link: String): Boolean { @@ -139,6 +161,7 @@ object DefaultSpaceInviteResolver : SpaceInviteResolver { return "https://$DEEP_LINK_INVITE_DOMAIN/$contentId#$encryptionKey" } + private const val CONTENT_INDEX = 1 private const val KEY_INDEX = 2 const val CONTENT_ID_KEY = "cid" diff --git a/app/src/main/java/com/anytypeio/anytype/other/MediaPermissionHelper.kt b/app/src/main/java/com/anytypeio/anytype/other/MediaPermissionHelper.kt index 8119086069..761cabd4e6 100644 --- a/app/src/main/java/com/anytypeio/anytype/other/MediaPermissionHelper.kt +++ b/app/src/main/java/com/anytypeio/anytype/other/MediaPermissionHelper.kt @@ -44,6 +44,7 @@ class MediaPermissionHelper( } fun openFilePicker(mimeType: Mimetype, requestCode: Int?) { + Timber.d("openFilePicker, mimeType:$mimeType, requestCode:$requestCode") if (isRequestInProgress) { Timber.w("Permission request already in progress") return @@ -62,10 +63,12 @@ class MediaPermissionHelper( val hasPermission = mimeType.hasPermission(context) if (hasPermission) { + Timber.d("Permission already granted") onPermissionSuccess(mimeType, requestCode) isRequestInProgress = false } else { val permissions = mimeType.getPermissionToRequestByMime() + Timber.d("Requesting permissions: $permissions") if (permissions.isNotEmpty()) { permissionReadStorage.launch(permissions) } else { 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/base/NavigationRouter.kt b/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationRouter.kt index 9a1146ec54..3f359498c8 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationRouter.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationRouter.kt @@ -11,7 +11,6 @@ class NavigationRouter( Timber.d("Navigate to $command") try { when (command) { - is AppNavigation.Command.ExitFromMigrationScreen -> navigation.exitFromMigrationScreen() is AppNavigation.Command.OpenSettings -> navigation.openSpaceSettings() is AppNavigation.Command.OpenObject -> navigation.openDocument( target = command.target, @@ -58,7 +57,6 @@ class NavigationRouter( is AppNavigation.Command.OpenTemplates -> navigation.openTemplatesModal( typeId = command.typeId ) - is AppNavigation.Command.MigrationErrorScreen -> navigation.migrationErrorScreen() is AppNavigation.Command.OpenDateObject -> navigation.openDateObject( objectId = command.objectId, space = command.space diff --git a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt index da01f3b6b1..7ca853a84b 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt @@ -251,6 +251,17 @@ class ChatFragment : BaseComposeFragment() { Timber.e(it, "Error while opening space member card") } } + is ChatViewModel.ViewModelCommand.Browse -> { + runCatching { + proceedWithAction( + SystemAction.OpenUrl( + command.url + ) + ) + }.onFailure { + Timber.e(it, "Error while opening bookmark from chat") + } + } } } } 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 998ce32882..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 @@ -121,6 +121,7 @@ import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.ext.visible import com.anytypeio.anytype.core_utils.ui.showActionableSnackBar import com.anytypeio.anytype.databinding.FragmentEditorBinding +import com.anytypeio.anytype.device.launchMediaPicker import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.di.feature.DefaultComponentParam import com.anytypeio.anytype.ext.extractMarks @@ -166,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 @@ -956,22 +957,20 @@ open class EditorFragment : NavigationFragment(R.layout.f ).showChildFragment() } is Command.OpenPhotoPicker -> { - try { - pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) - } catch (e: Exception) { - Timber.w(e, "Error while opening photo picker") - toast("Error while opening photo picker") - pickerDelegate.openFilePicker(Mimetype.MIME_IMAGE_ALL, null) - } + launchMediaPicker( + pickMedia = pickMedia, + pickerDelegate = pickerDelegate, + mediaType = PickVisualMedia.ImageOnly, + fallbackMimeType = Mimetype.MIME_IMAGE_ALL + ) } is Command.OpenVideoPicker -> { - try { - pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.VideoOnly)) - } catch (e: Exception) { - Timber.e(e, "Error while opening video picker") - toast("Error while opening video picker") - pickerDelegate.openFilePicker(Mimetype.MIME_VIDEO_ALL, null) - } + launchMediaPicker( + pickMedia = pickMedia, + pickerDelegate = pickerDelegate, + mediaType = PickVisualMedia.VideoOnly, + fallbackMimeType = Mimetype.MIME_VIDEO_ALL + ) } is Command.OpenFilePicker -> { pickerDelegate.openFilePicker(Mimetype.MIME_FILE_ALL, null) @@ -1075,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/cover/SelectCoverGalleryFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/cover/SelectCoverGalleryFragment.kt index 4e12c7cd32..4ae6ed4c5c 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/editor/cover/SelectCoverGalleryFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/cover/SelectCoverGalleryFragment.kt @@ -28,6 +28,7 @@ import com.anytypeio.anytype.core_utils.ext.subscribe import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment import com.anytypeio.anytype.databinding.FragmentDocCoverGalleryBinding +import com.anytypeio.anytype.device.launchMediaPicker import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.di.feature.DefaultComponentParam import com.anytypeio.anytype.other.MediaPermissionHelper @@ -104,13 +105,12 @@ abstract class SelectCoverGalleryFragment : binding.btnUpload.clicks() .onEach { - try { - pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) - } catch (e: Exception) { - Timber.w(e, "Error while opening photo picker") - toast("Error while opening photo picker") - permissionHelper.openFilePicker(Mimetype.MIME_IMAGE_ALL, null) - } + launchMediaPicker( + pickMedia = pickMedia, + permissionHelper = permissionHelper, + mediaType = PickVisualMedia.ImageOnly, + fallbackMimeType = Mimetype.MIME_IMAGE_ALL + ) } .launchIn(lifecycleScope) 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/modals/IconPickerFragmentBase.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/modals/IconPickerFragmentBase.kt index b9b99025b1..44cd15ee19 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/editor/modals/IconPickerFragmentBase.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/modals/IconPickerFragmentBase.kt @@ -23,6 +23,7 @@ import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.ext.visible import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetTextInputFragment import com.anytypeio.anytype.databinding.FragmentPageIconPickerBinding +import com.anytypeio.anytype.device.launchMediaPicker import com.anytypeio.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerAdapter import com.anytypeio.anytype.other.MediaPermissionHelper import com.anytypeio.anytype.presentation.editor.picker.EmojiPickerView.Companion.HOLDER_EMOJI_CATEGORY_HEADER @@ -85,13 +86,12 @@ abstract class IconPickerFragmentBase : btnRemoveIcon.setOnClickListener { vm.onRemoveClicked(target) } tvTabRandom.setOnClickListener { vm.onRandomEmoji(target) } tvTabUpload.setOnClickListener { - try { - pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) - } catch (e: Exception) { - Timber.w(e, "Error while opening photo picker") - toast("Error while opening photo picker") - permissionHelper.openFilePicker(Mimetype.MIME_IMAGE_ALL, 0) - } + launchMediaPicker( + pickMedia = pickMedia, + permissionHelper = permissionHelper, + mediaType = PickVisualMedia.ImageOnly, + fallbackMimeType = Mimetype.MIME_IMAGE_ALL + ) } } skipCollapsed() 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/home/HomeScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreen.kt index 178b3e66f5..d1e684c1e0 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreen.kt @@ -91,11 +91,11 @@ fun HomeScreen( onHomeButtonClicked: () -> Unit, onCreateNewObjectClicked: () -> Unit, onCreateNewObjectLongClicked: () -> Unit, - onShareButtonClicked: () -> Unit, + onNavBarShareButtonClicked: () -> Unit, onObjectCheckboxClicked: (Id, Boolean) -> Unit, onSpaceWidgetClicked: () -> Unit, onMove: (List, FromIndex, ToIndex) -> Unit, - onSpaceShareIconClicked: (ObjectWrapper.SpaceView) -> Unit, + onSpaceWidgetShareIconClicked: (ObjectWrapper.SpaceView) -> Unit, onSeeAllObjectsClicked: (WidgetView.Gallery) -> Unit, onCreateObjectInsideWidget: (Id) -> Unit, onCreateDataViewObject: (WidgetId, ViewId?) -> Unit @@ -116,7 +116,7 @@ fun HomeScreen( onSpaceWidgetClicked = onSpaceWidgetClicked, onMove = onMove, onObjectCheckboxClicked = onObjectCheckboxClicked, - onSpaceShareIconClicked = onSpaceShareIconClicked, + onSpaceWidgetShareIconClicked = onSpaceWidgetShareIconClicked, onSeeAllObjectsClicked = onSeeAllObjectsClicked, onCreateWidget = onCreateWidget, onCreateObjectInsideWidget = onCreateObjectInsideWidget, @@ -162,7 +162,7 @@ fun HomeScreen( searchClick = onSearchClicked, addDocClick = onCreateNewObjectClicked, addDocLongClick = onCreateNewObjectLongClicked, - onShareButtonClicked = onShareButtonClicked, + onShareButtonClicked = onNavBarShareButtonClicked, onHomeButtonClicked = onHomeButtonClicked ) } @@ -187,7 +187,7 @@ private fun WidgetList( onMove: (List, FromIndex, ToIndex) -> Unit, onObjectCheckboxClicked: (Id, Boolean) -> Unit, onSpaceWidgetClicked: () -> Unit, - onSpaceShareIconClicked: (ObjectWrapper.SpaceView) -> Unit, + onSpaceWidgetShareIconClicked: (ObjectWrapper.SpaceView) -> Unit, onSeeAllObjectsClicked: (WidgetView.Gallery) -> Unit, onCreateWidget: () -> Unit, onCreateObjectInsideWidget: (Id) -> Unit, @@ -229,7 +229,7 @@ private fun WidgetList( name = item.space.name.orEmpty(), icon = item.icon, spaceType = item.type, - onSpaceShareIconClicked = { onSpaceShareIconClicked(item.space) }, + onSpaceShareIconClicked = { onSpaceWidgetShareIconClicked(item.space) }, isShared = item.isShared, membersCount = item.membersCount ) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt index 1a60584c8a..52740891da 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt @@ -112,7 +112,7 @@ class HomeScreenFragment : BaseComposeFragment(), ) { HomeScreenToolbar( spaceIconView = view?.icon ?: SpaceIconView.Loading, - onSpaceIconClicked = vm::onSpaceSettingsClicked, + onSpaceIconClicked = vm::onSpaceWidgetClicked, membersCount = view?.membersCount ?: 0, name = view?.space?.name.orEmpty(), onBackButtonClicked = { @@ -140,7 +140,6 @@ class HomeScreenFragment : BaseComposeFragment(), ) } } - BackHandler { vm.onBackClicked( isSpaceRoot = isSpaceRootScreen() @@ -177,16 +176,16 @@ class HomeScreenFragment : BaseComposeFragment(), onClick = { vm.onCreateNewObjectLongClicked() } ), onSpaceWidgetClicked = throttledClick( - onClick = vm::onSpaceSettingsClicked + onClick = vm::onSpaceWidgetClicked ), onBundledWidgetClicked = vm::onBundledWidgetClicked, onMove = vm::onMove, onObjectCheckboxClicked = vm::onObjectCheckboxClicked, - onSpaceShareIconClicked = vm::onSpaceShareIconClicked, + onSpaceWidgetShareIconClicked = vm::onSpaceWidgetShareIconClicked, onSeeAllObjectsClicked = vm::onSeeAllObjectsClicked, onCreateObjectInsideWidget = vm::onCreateObjectInsideWidget, onCreateDataViewObject = vm::onCreateDataViewObject, - onShareButtonClicked = vm::onSpaceShareIconClicked, + onNavBarShareButtonClicked = vm::onNavBarShareIconClicked, navPanelState = vm.navPanelState.collectAsStateWithLifecycle().value, onHomeButtonClicked = vm::onHomeButtonClicked, ) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenToolbar.kt b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenToolbar.kt index 6b471213ca..c7aa3c6bcd 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenToolbar.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenToolbar.kt @@ -95,7 +95,7 @@ fun HomeScreenToolbar( } else stringResource(id = R.string.three_dots_text_placeholder), style = Relations2, - color = colorResource(R.color.text_secondary), + color = colorResource(R.color.transparent_active), modifier = Modifier .align(Alignment.BottomStart) .padding( diff --git a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt index 6cce4ba1b6..0e5677f353 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt @@ -52,6 +52,7 @@ import com.anytypeio.anytype.presentation.notifications.NotificationAction import com.anytypeio.anytype.presentation.notifications.NotificationCommand import com.anytypeio.anytype.presentation.wallpaper.WallpaperColor import com.anytypeio.anytype.presentation.wallpaper.WallpaperView +import com.anytypeio.anytype.ui.chats.ChatFragment import com.anytypeio.anytype.ui.date.DateObjectFragment import com.anytypeio.anytype.ui.editor.CreateObjectFragment import com.anytypeio.anytype.ui.editor.EditorFragment @@ -196,82 +197,38 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr } } is Command.Navigate -> { - when(val dest = command.destination) { - is OpenObjectNavigation.OpenDataView -> { - runCatching { - findNavController(R.id.fragment).navigate( - R.id.dataViewNavigation, - args = ObjectSetFragment.args( - ctx = dest.target, - space = dest.space - ), - navOptions = NavOptions.Builder() - .setPopUpTo(R.id.homeScreen, true) - .build() - ) - }.onFailure { - Timber.e(it, "Error while data view navigation") - } - } - is OpenObjectNavigation.OpenParticipant -> { - runCatching { - findNavController(R.id.fragment).navigate( - R.id.participantScreen, - ParticipantFragment.args( - objectId = dest.target, - space = dest.space - ) - ) - }.onFailure { - Timber.w("Error while opening participant screen") - } - } - is OpenObjectNavigation.OpenEditor -> { - runCatching { - findNavController(R.id.fragment).navigate( - R.id.objectNavigation, - args = EditorFragment.args( - ctx = dest.target, - space = dest.space - ), - navOptions = NavOptions.Builder() - .setPopUpTo(R.id.homeScreen, true) - .build() - ) - }.onFailure { - Timber.e(it, "Error while editor navigation") - } - } - is OpenObjectNavigation.OpenChat -> { - toast("Cannot open chat from here") - } - is OpenObjectNavigation.UnexpectedLayoutError -> { - toast(getString(R.string.error_unexpected_layout)) - } - OpenObjectNavigation.NonValidObject -> { - toast(getString(R.string.error_non_valid_object)) - } - is OpenObjectNavigation.OpenDateObject -> { - runCatching { - findNavController(R.id.fragment).navigate( - R.id.dateObjectScreen, - args = DateObjectFragment.args( - objectId = dest.target, - space = dest.space - ), - navOptions = Builder() - .setPopUpTo(R.id.homeScreen, true) - .build() - ) - }.onFailure { - Timber.e(it, "Error while date object navigation") - } - } - } + proceedWithOpenObjectNavigation(command.destination) } is Command.Deeplink.DeepLinkToObjectNotWorking -> { toast(getString(R.string.multiplayer_deeplink_to_your_object_error)) } + is Command.Deeplink.DeepLinkToObject -> { + when(val effect = command.sideEffect) { + is Command.Deeplink.DeepLinkToObject.SideEffect.SwitchSpace -> { + runCatching { + val controller = findNavController(R.id.fragment) + controller.popBackStack(R.id.vaultScreen, false) + if (effect.chat != null) { + controller.navigate( + R.id.actionOpenChatFromVault, + ChatFragment.args( + space = command.space, + ctx = effect.chat.orEmpty() + ) + ) + } else { + controller.navigate(R.id.actionOpenSpaceFromVault) + } + proceedWithOpenObjectNavigation(command.navigation) + }.onFailure { + Timber.e(it, "Error while switching space when handling deep link to object") + } + } + null -> { + proceedWithOpenObjectNavigation(command.navigation) + } + } + } is Command.Deeplink.GalleryInstallation -> { runCatching { findNavController(R.id.fragment).navigate( @@ -322,6 +279,84 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr } } + private fun proceedWithOpenObjectNavigation(dest: OpenObjectNavigation) { + when (dest) { + is OpenObjectNavigation.OpenDataView -> { + runCatching { + findNavController(R.id.fragment).navigate( + R.id.dataViewNavigation, + args = ObjectSetFragment.args( + ctx = dest.target, + space = dest.space + ), + navOptions = Builder() + .setPopUpTo(R.id.homeScreen, true) + .build() + ) + }.onFailure { + Timber.e(it, "Error while data view navigation") + } + } + + is OpenObjectNavigation.OpenParticipant -> { + runCatching { + findNavController(R.id.fragment).navigate( + R.id.participantScreen, + ParticipantFragment.args( + objectId = dest.target, + space = dest.space + ) + ) + }.onFailure { + Timber.w("Error while opening participant screen") + } + } + + is OpenObjectNavigation.OpenEditor -> { + runCatching { + findNavController(R.id.fragment).navigate( + R.id.objectNavigation, + args = EditorFragment.args( + ctx = dest.target, + space = dest.space + ) + ) + }.onFailure { + Timber.e(it, "Error while editor navigation") + } + } + + is OpenObjectNavigation.OpenChat -> { + toast("Cannot open chat from here") + } + + is OpenObjectNavigation.UnexpectedLayoutError -> { + toast(getString(R.string.error_unexpected_layout)) + } + + OpenObjectNavigation.NonValidObject -> { + toast(getString(R.string.error_non_valid_object)) + } + + is OpenObjectNavigation.OpenDateObject -> { + runCatching { + findNavController(R.id.fragment).navigate( + R.id.dateObjectScreen, + args = DateObjectFragment.args( + objectId = dest.target, + space = dest.space + ), + navOptions = Builder() + .setPopUpTo(R.id.homeScreen, true) + .build() + ) + }.onFailure { + Timber.e(it, "Error while date object navigation") + } + } + } + } + private fun startAppUpdater() { if (featureToggles.isAutoUpdateEnabled) { AppUpdater(this) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/OnboardingFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/OnboardingFragment.kt index eaf08c12be..e492b42cb3 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/OnboardingFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/OnboardingFragment.kt @@ -427,21 +427,6 @@ class OnboardingFragment : Fragment() { Timber.e(it, "Error while trying to open vault screen from onboarding") } } - OnboardingMnemonicLoginViewModel.Command.NavigateToMigrationErrorScreen -> { - runCatching { - findNavController().navigate( - R.id.migrationNeededScreen, - null, - navOptions { - popUpTo(R.id.onboarding_nav) { - inclusive = false - } - } - ) - }.onFailure { - Timber.e(it, "Error while trying to open migration screen from onboarding") - } - } is OnboardingMnemonicLoginViewModel.Command.ShareDebugGoroutines -> { try { this@OnboardingFragment.shareFirstFileFromPath(command.path, command.uriFileProvider) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt index 41709c39b3..b98afbd6f4 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.anytypeio.anytype.BuildConfig import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_ui.ColorButtonRegular import com.anytypeio.anytype.core_ui.MnemonicPhrasePaletteColors import com.anytypeio.anytype.core_ui.OnBoardingTextPrimaryColor @@ -54,6 +55,8 @@ import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel.SetupState import com.anytypeio.anytype.ui.onboarding.OnboardingMnemonicInput +import com.anytypeio.anytype.ui.update.MigrationFailedScreen +import com.anytypeio.anytype.ui.update.MigrationInProgressScreen import kotlin.Unit @Composable @@ -67,11 +70,12 @@ fun RecoveryScreenWrapper( onNextClicked = vm::onLoginClicked, onActionDoneClicked = vm::onActionDone, onScanQrClicked = onScanQrClick, - isLoading = vm.state.collectAsState().value is SetupState.InProgress, + state = vm.state.collectAsState().value, onEnterMyVaultClicked = vm::onEnterMyVaultClicked, onDebugAccountTraceClicked = { vm.onAccountThraceButtonClicked() - } + }, + onRetryMigrationClicked = vm::onRetryMigrationClicked ) } @@ -81,9 +85,10 @@ fun RecoveryScreen( onNextClicked: (Mnemonic) -> Unit, onActionDoneClicked: (Mnemonic) -> Unit, onScanQrClicked: () -> Unit, - isLoading: Boolean, + state: SetupState, onEnterMyVaultClicked: () -> Unit, - onDebugAccountTraceClicked: () -> Unit + onDebugAccountTraceClicked: () -> Unit, + onRetryMigrationClicked: (Id) -> Unit ) { val focus = LocalFocusManager.current val context = LocalContext.current @@ -186,7 +191,7 @@ fun RecoveryScreen( modifier = Modifier .fillMaxWidth() .padding(horizontal = 18.dp), - isLoading = isLoading + isLoading = state is SetupState.InProgress ) } item { @@ -207,7 +212,7 @@ fun RecoveryScreen( onClick = { onScanQrClicked.invoke() }, - enabled = !isLoading, + enabled = state !is SetupState.InProgress, disabledBackgroundColor = Color.Transparent, size = ButtonSize.Large, modifier = Modifier @@ -218,6 +223,16 @@ fun RecoveryScreen( } } ) + if (state is SetupState.Migration.InProgress) { + MigrationInProgressScreen() + } else if(state is SetupState.Migration.Failed) { + MigrationFailedScreen( + state = state.state, + onRetryClicked = { + onRetryMigrationClicked(state.account) + } + ) + } } } @@ -267,9 +282,10 @@ fun RecoveryScreenPreview() { onNextClicked = {}, onActionDoneClicked = {}, onScanQrClicked = {}, - isLoading = false, + state = SetupState.Idle, onEnterMyVaultClicked = {}, - onDebugAccountTraceClicked = {} + onDebugAccountTraceClicked = {}, + onRetryMigrationClicked = {} ) } @@ -282,8 +298,9 @@ fun RecoveryScreenLoadingPreview() { onNextClicked = {}, onActionDoneClicked = {}, onScanQrClicked = {}, - isLoading = true, + state = SetupState.InProgress, onEnterMyVaultClicked = {}, - onDebugAccountTraceClicked = {} + onDebugAccountTraceClicked = {}, + onRetryMigrationClicked = {} ) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/primitives/EditTypePropertiesFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/primitives/EditTypePropertiesFragment.kt new file mode 100644 index 0000000000..f72a23b72d --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/EditTypePropertiesFragment.kt @@ -0,0 +1,146 @@ +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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +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.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.setupBottomSheetBehavior +import com.anytypeio.anytype.core_utils.ext.subscribe +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModelFactory +import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModel +import com.anytypeio.anytype.feature_properties.EditTypePropertiesViewModel.EditTypePropertiesCommand +import com.anytypeio.anytype.feature_properties.add.EditTypePropertiesVmParams +import com.anytypeio.anytype.feature_properties.add.UiEditTypePropertiesErrorState +import com.anytypeio.anytype.feature_properties.add.ui.AddFieldScreen +import javax.inject.Inject + +class EditTypePropertiesFragment : BaseBottomSheetComposeFragment() { + + @Inject + lateinit var viewModelFactory: EditTypePropertiesViewModelFactory + private val vm by viewModels { viewModelFactory } + 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 { + AddFieldScreen( + state = vm.uiState.collectAsStateWithLifecycle().value, + uiStateEditProperty = vm.uiPropertyEditState.collectAsStateWithLifecycle().value, + event = vm::onEvent + ) + ErrorScreen() + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun ErrorScreen() { + val errorStateScreen = vm.errorState.collectAsStateWithLifecycle().value + if (errorStateScreen is UiEditTypePropertiesErrorState.Show) { + when (val r = errorStateScreen.reason) { + is UiEditTypePropertiesErrorState.Reason.ErrorAddingProperty -> { + BaseAlertDialog( + dialogText = stringResource(id = R.string.add_property_error_add), + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::hideError, + onDismissRequest = vm::hideError + ) + } + + is UiEditTypePropertiesErrorState.Reason.ErrorCreatingProperty -> { + BaseAlertDialog( + dialogText = stringResource(id = R.string.add_property_error_create_new), + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::hideError, + onDismissRequest = vm::hideError + ) + } + + is UiEditTypePropertiesErrorState.Reason.ErrorUpdatingProperty -> { + BaseAlertDialog( + dialogText = stringResource(id = R.string.add_property_error_update), + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::hideError, + onDismissRequest = vm::hideError + ) + } + + is UiEditTypePropertiesErrorState.Reason.Other -> { + BaseAlertDialog( + dialogText = r.msg, + buttonText = stringResource(id = R.string.membership_error_button_text_dismiss), + onButtonClick = vm::hideError, + onDismissRequest = vm::hideError + ) + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupBottomSheetBehavior(DEFAULT_PADDING_TOP) + } + + override fun onStart() { + super.onStart() + jobs += lifecycleScope.subscribe(vm.commands) { command -> execute(command) } + } + + private fun execute(command: EditTypePropertiesCommand) { + when (command) { + is EditTypePropertiesCommand.Exit -> { + findNavController().popBackStack() + } + } + } + + override fun injectDependencies() { + val params = EditTypePropertiesVmParams( + objectTypeId = typeId, + spaceId = SpaceId(space) + + ) + componentManager().editTypePropertiesComponent.get(params).inject(this) + } + + override fun releaseDependencies() { + componentManager().editTypePropertiesComponent.release() + } + + companion object { + + fun args(objectId: Id, space: Id) = bundleOf( + ARG_OBJECT_ID to objectId, + ARG_SPACE to space + ) + + const val ARG_OBJECT_ID = "arg.primitives.edit.type.property.object.id" + const val ARG_SPACE = "arg.primitives.edit.type.property.space" + + const val DEFAULT_PADDING_TOP = 10 + } +} \ No newline at end of file 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..bb93600585 --- /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.primitives.properties.ctx" + const val ARG_SPACE = "arg.primitives.properties.space" + const val ARG_TARGET = "arg.primitives.properties.target" + const val ARG_LOCKED = "arg.primitives.properties.locked" + const val ARG_SET_FLOW = "arg.primitives.properties.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..79ab82face --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFieldsFragment.kt @@ -0,0 +1,91 @@ +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, + uiEditPropertyState = vm.uiEditPropertyScreen.collectAsStateWithLifecycle().value, + uiFieldLocalInfoState = vm.uiFieldLocalInfoState.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, + 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..8217d3322e --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/ObjectTypeFragment.kt @@ -0,0 +1,235 @@ +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.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.ui.BaseComposeFragment +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.ObjectTypeCommand +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.viewmodel.ObjectTypeVMFactory +import com.anytypeio.anytype.feature_object_type.viewmodel.ObjectTypeViewModel +import com.anytypeio.anytype.ui.editor.EditorModalFragment +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.OpenEditTypePropertiesScreen -> { + runCatching { + findNavController().navigate( + R.id.editTypePropertiesScreen, + EditTypePropertiesFragment.args( + objectId = command.typeId, + space = command.space + ) + ) + }.onFailure { + Timber.e(it, "Error while opening edit object type properties screen") + } + } + } + } + } + + 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) { + WithSetScreen( + uiEditButtonState = vm.uiEditButtonState.collectAsStateWithLifecycle().value, + uiSyncStatusBadgeState = vm.uiSyncStatusBadgeState.collectAsStateWithLifecycle().value, + uiIconState = vm.uiIconState.collectAsStateWithLifecycle().value, + uiTitleState = vm.uiTitleState.collectAsStateWithLifecycle().value, + uiFieldsButtonState = vm.uiFieldsButtonState.collectAsStateWithLifecycle().value, + uiLayoutButtonState = vm.uiLayoutButtonState.collectAsStateWithLifecycle().value, + uiTemplatesButtonState = vm.uiTemplatesButtonState.collectAsStateWithLifecycle().value, + uiTemplatesModalListState = vm.uiTemplatesModalListState.collectAsStateWithLifecycle().value, + uiLayoutTypeState = vm.uiTypeLayoutsState.collectAsStateWithLifecycle().value, + uiSyncStatusState = vm.uiSyncStatusWidgetState.collectAsStateWithLifecycle().value, + uiDeleteAlertState = vm.uiAlertState.collectAsStateWithLifecycle().value, + objectId = objectId, + space = space, + onTypeEvent = vm::onTypeEvent + ) + } + composable(route = OBJ_TYPE_FIELDS) { + FieldsMainScreen( + uiFieldsListState = vm.uiFieldsListState.collectAsStateWithLifecycle().value, + uiTitleState = vm.uiTitleState.collectAsStateWithLifecycle().value, + uiIconState = vm.uiIconState.collectAsStateWithLifecycle().value, + uiEditPropertyState = vm.uiEditPropertyScreen.collectAsStateWithLifecycle().value, + uiFieldLocalInfoState = vm.uiFieldLocalInfoState.collectAsStateWithLifecycle().value, + fieldEvent = vm::onFieldEvent + ) + } + } + } + + @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 + ) + } + } + } + } + + override fun injectDependencies() { + val params = ObjectTypeVmParams( + spaceId = SpaceId(space), + objectId = objectId, + 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/primitives/WithSetScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/primitives/WithSetScreen.kt new file mode 100644 index 0000000000..557cfec3ee --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/primitives/WithSetScreen.kt @@ -0,0 +1,194 @@ +package com.anytypeio.anytype.ui.primitives + +import android.os.Build +import android.view.View +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.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import androidx.fragment.compose.AndroidFragment +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.BottomSyncStatus +import com.anytypeio.anytype.feature_object_type.ui.TopBarContent +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.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.UiSyncStatusBadgeState +import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesButtonState +import com.anytypeio.anytype.feature_object_type.ui.UiTemplatesModalListState +import com.anytypeio.anytype.feature_object_type.ui.UiTitleState +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.layouts.TypeLayoutsScreen +import com.anytypeio.anytype.feature_object_type.ui.templates.TemplatesModalList +import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState +import com.anytypeio.anytype.ui.sets.ObjectSetFragment + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WithSetScreen( + //top bar + uiEditButtonState: UiEditButton, + uiSyncStatusBadgeState: UiSyncStatusBadgeState, + uiSyncStatusState: SyncStatusWidgetState, + //header + uiIconState: UiIconState, + uiTitleState: UiTitleState, + //layout and fields buttons + uiFieldsButtonState: UiFieldsButtonState, + uiLayoutButtonState: UiLayoutButtonState, + uiLayoutTypeState: UiLayoutTypeState, + uiTemplatesButtonState: UiTemplatesButtonState, + //templates modal list + uiTemplatesModalListState: UiTemplatesModalListState, + //delete alert + uiDeleteAlertState: UiDeleteAlertState, + //events + onTypeEvent: (TypeEvent) -> Unit, + objectId: String, + space: String, +) { + 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 -> + MainContentSet( + paddingValues = paddingValues, + uiIconState = uiIconState, + uiTitleState = uiTitleState, + uiFieldsButtonState = uiFieldsButtonState, + uiLayoutButtonState = uiLayoutButtonState, + uiTemplatesButtonState = uiTemplatesButtonState, + objectId = objectId, + space = space, + 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 + ) + } + + if (uiTemplatesModalListState is UiTemplatesModalListState.Visible) { + TemplatesModalList( + modifier = Modifier.fillMaxWidth(), + uiState = uiTemplatesModalListState, + onTypeEvent = onTypeEvent + ) + } +} + + +@Composable +private fun MainContentSet( + paddingValues: PaddingValues, + uiIconState: UiIconState, + uiTitleState: UiTitleState, + uiFieldsButtonState: UiFieldsButtonState, + uiLayoutButtonState: UiLayoutButtonState, + uiTemplatesButtonState: UiTemplatesButtonState, + objectId: String, + space: String, + onTypeEvent: (TypeEvent) -> Unit +) { + 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) + } + + Column(modifier = contentModifier) { + IconAndTitleWidget( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 35.dp) + .padding(horizontal = 20.dp), + uiIconState = uiIconState, + uiTitleState = uiTitleState, + onTypeEvent = onTypeEvent + ) + Spacer(modifier = Modifier.height(20.dp)) + HorizontalButtons( + modifier = Modifier + .fillMaxWidth() + .height(36.dp) + .padding(horizontal = 20.dp), + uiFieldsButtonState = uiFieldsButtonState, + uiLayoutButtonState = uiLayoutButtonState, + uiTemplatesButtonState = uiTemplatesButtonState, + onTypeEvent = onTypeEvent + ) + Spacer(modifier = Modifier.height(24.dp)) + AndroidFragment( + modifier = Modifier + .fillMaxSize(), + arguments = ObjectSetFragment.args( + ctx = objectId, + space = space + ) + ) { fragment -> + fragment.view?.findViewById(R.id.objectHeader)?.visibility = + View.GONE + } + } +} \ 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/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt index 433da47e82..32c4263c67 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt @@ -705,6 +705,57 @@ open class ObjectSetFragment : dataViewInfo.hide() setViewer(viewer = null) } + is DataViewViewState.TypeSet.Default -> { + topToolbarThreeDotsButton.gone() + topToolbarStatusContainer.gone() + topBackButton.gone() + initView.gone() + header.gone() + dataViewHeader.visible() + viewerTitle.isEnabled = true + setupNewButtonsForTypeSet(state.isCreateObjectAllowed) + if (state.isEditingViewAllowed) { + customizeViewButton.visible() + } else { + customizeViewButton.invisible() + } + customizeViewButton.isEnabled = true + setCurrentViewerName(state.viewer?.title) + setViewer(viewer = state.viewer) + dataViewInfo.hide() + } + is DataViewViewState.TypeSet.NoItems -> { + topToolbarThreeDotsButton.gone() + topToolbarStatusContainer.gone() + topBackButton.gone() + initView.gone() + header.gone() + dataViewHeader.visible() + viewerTitle.isEnabled = true + setupNewButtonsForTypeSet(state.isCreateObjectAllowed) + if (state.isEditingViewAllowed) { + customizeViewButton.visible() + } else { + customizeViewButton.invisible() + } + customizeViewButton.isEnabled = true + setCurrentViewerName(state.title) + dataViewInfo.show( + type = DataViewInfo.TYPE.SET_NO_ITEMS, + isReadOnlyAccess = !state.isCreateObjectAllowed + ) + setViewer(viewer = null) + } + + is DataViewViewState.TypeSet.Error -> { + topToolbarThreeDotsButton.gone() + topToolbarStatusContainer.gone() + topBackButton.gone() + initView.gone() + header.gone() + dataViewHeader.visible() + setViewer(viewer = null) + } } } @@ -726,6 +777,16 @@ open class ObjectSetFragment : } } + private fun setupNewButtonsForTypeSet(isCreateObjectAllowed: Boolean) { + if (isCreateObjectAllowed) { + addNewButton.visible() + addNewIconButton.gone() + } else { + addNewButton.gone() + addNewIconButton.gone() + } + } + private fun setViewer(viewer: Viewer?) { when (viewer) { is Viewer.GridView -> { @@ -824,29 +885,28 @@ open class ObjectSetFragment : .launchIn(lifecycleScope) } - binding.objectHeader.root.findViewById(R.id.imageIcon).apply { - if (header.title.image != null) visible() else gone() + binding.objectHeader.root.findViewById(R.id.imageIcon).apply { jobs += this.clicks() .throttleFirst() .onEach { vm.onObjectIconClicked() } .launchIn(lifecycleScope) + + if (header.title.image != null) { + this.visible() + Glide + .with(this) + .load(header.title.image) + .centerCrop() + .into(this) + } else { + this.gone() + this.setImageDrawable(null) + } } binding.objectHeader.root.findViewById(R.id.emojiIcon) .setEmojiOrNull(header.title.emoji) - if (header.title.image != null) { - binding.objectHeader.root.findViewById(R.id.imageIcon).apply { - Glide - .with(this) - .load(header.title.image) - .centerCrop() - .into(this) - } - } else { - binding.objectHeader.root.findViewById(R.id.imageIcon).setImageDrawable(null) - } - setCover( coverColor = header.title.coverColor, coverGradient = header.title.coverGradient, @@ -1439,34 +1499,44 @@ open class ObjectSetFragment : } private fun observeSelectingTemplate() { - val navController = findNavController() - val navBackStackEntry = navController.getBackStackEntry(R.id.objectSetScreen) - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME - && navBackStackEntry.savedStateHandle.contains(ARG_TEMPLATE_ID)) { - val resultTemplateId = navBackStackEntry.savedStateHandle.get(ARG_TEMPLATE_ID) - val resultTypeId = navBackStackEntry.savedStateHandle.get(ARG_TARGET_TYPE_ID) - val resultTypeKey = navBackStackEntry.savedStateHandle.get(ARG_TARGET_TYPE_KEY) - if (!resultTemplateId.isNullOrBlank() && !resultTypeId.isNullOrBlank() && !resultTypeKey.isNullOrBlank()) { - navBackStackEntry.savedStateHandle.remove(ARG_TEMPLATE_ID) - navBackStackEntry.savedStateHandle.remove(ARG_TARGET_TYPE_ID) - navBackStackEntry.savedStateHandle.remove(ARG_TARGET_TYPE_KEY) - vm.proceedWithSelectedTemplate( - template = resultTemplateId, - typeId = resultTypeId, - typeKey = resultTypeKey - ) + try { + val navController = findNavController() + val navBackStackEntry = navController.getBackStackEntry(R.id.objectSetScreen) + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME + && navBackStackEntry.savedStateHandle.contains(ARG_TEMPLATE_ID) + ) { + val resultTemplateId = + navBackStackEntry.savedStateHandle.get(ARG_TEMPLATE_ID) + val resultTypeId = + navBackStackEntry.savedStateHandle.get(ARG_TARGET_TYPE_ID) + val resultTypeKey = + navBackStackEntry.savedStateHandle.get(ARG_TARGET_TYPE_KEY) + if (!resultTemplateId.isNullOrBlank() && !resultTypeId.isNullOrBlank() && !resultTypeKey.isNullOrBlank()) { + navBackStackEntry.savedStateHandle.remove(ARG_TEMPLATE_ID) + navBackStackEntry.savedStateHandle.remove(ARG_TARGET_TYPE_ID) + navBackStackEntry.savedStateHandle.remove(ARG_TARGET_TYPE_KEY) + vm.proceedWithSelectedTemplate( + template = resultTemplateId, + typeId = resultTypeId, + typeKey = resultTypeKey + ) + } } } + + navBackStackEntry.lifecycle.addObserver(observer) + + viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_DESTROY) { + navBackStackEntry.lifecycle.removeObserver(observer) + } + }) + } catch ( + e: Exception + ) { + Timber.w(e) } - - navBackStackEntry.lifecycle.addObserver(observer) - - viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_DESTROY) { - navBackStackEntry.lifecycle.removeObserver(observer) - } - }) } override fun injectDependencies() { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugFragment.kt new file mode 100644 index 0000000000..3c0ef402b5 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugFragment.kt @@ -0,0 +1,93 @@ +package com.anytypeio.anytype.ui.settings + +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.LaunchedEffect +import androidx.fragment.app.viewModels +import androidx.fragment.compose.content +import com.anytypeio.anytype.core_utils.tools.ZIP_MIME_TYPE +import com.anytypeio.anytype.core_utils.tools.zipDirectory +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment +import com.anytypeio.anytype.di.common.componentManager +import com.anytypeio.anytype.presentation.settings.DebugViewModel +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import javax.inject.Inject +import kotlin.getValue + +class DebugFragment : BaseBottomSheetComposeFragment() { + + private val createFileLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument(ZIP_MIME_TYPE)) { uri -> + uri?.let { saveZipToUri(it) } + } + + @Inject + lateinit var factory: DebugViewModel.Factory + + private val vm by viewModels { factory } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = content { + DebugScreen( + onExportAllClicked = vm::onExportWorkingDirectory + ) + LaunchedEffect(Unit) { + vm.commands.collect { cmd -> + when(cmd) { + is DebugViewModel.Command.ExportWorkingDirectory -> { + proceedWithZippingAndSharingWorkDirectory( + folderName = cmd.folderName, + exportFileName = cmd.exportFileName + ) + } + } + } + } + } + + private fun proceedWithZippingAndSharingWorkDirectory( + folderName: String, + exportFileName: String + ) { + val folder = File( + requireContext().filesDir, + folderName + ) + val zipped = File( + requireContext().cacheDir, + DebugViewModel.EXPORT_WORK_DIRECTORY_TEMP_FOLDER + ) + zipDirectory( + sourceDir = folder, + zipFile = zipped + ) + createFileLauncher.launch(exportFileName) + } + + private fun saveZipToUri(uri: Uri) { + try { + requireContext().contentResolver.openOutputStream(uri)?.use { outputStream -> + val zipFile = File(requireContext().cacheDir, DebugViewModel.EXPORT_WORK_DIRECTORY_TEMP_FOLDER) + FileInputStream(zipFile).use { it.copyTo(outputStream) } + } + } catch (e: IOException) { + e.printStackTrace() + } + } + + override fun injectDependencies() { + componentManager().debugComponent.get().inject(this) + } + + override fun releaseDependencies() { + componentManager().debugComponent.release() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugScreen.kt new file mode 100644 index 0000000000..4379ad8963 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/DebugScreen.kt @@ -0,0 +1,78 @@ +package com.anytypeio.anytype.ui.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.material3.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.unit.dp +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.foundation.Dragger +import com.anytypeio.anytype.core_ui.foundation.Header + +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.views.BodyRegular + +@Composable +fun DebugScreen( + onExportAllClicked: () -> Unit +) { + Column( + modifier = Modifier.fillMaxSize() + ) { + Dragger( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 6.dp) + ) + Header( + text = stringResource(R.string.debug) + ) + + Spacer(modifier = Modifier.height(10.dp)) + + ActionItem( + title = "Export work directory", + onClick = onExportAllClicked + ) + + Divider() + + } +} + +@Composable +private fun ActionItem( + title: String, + onClick: () -> Unit +) { + Text( + text = title, + style = BodyRegular, + color = colorResource(R.color.text_primary), + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + .clickable { + onClick() + } + ) +} + +@DefaultPreviews +@Composable +fun DebugScreenPreview() { + DebugScreen( + onExportAllClicked = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt index c8cd219d89..c6aff8ba4b 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia import androidx.compose.material.MaterialTheme import androidx.compose.runtime.collectAsState @@ -28,6 +27,7 @@ import com.anytypeio.anytype.core_utils.ext.subscribe import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.tools.FeatureToggles import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment +import com.anytypeio.anytype.device.launchMediaPicker import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.other.MediaPermissionHelper import com.anytypeio.anytype.ui.profile.KeychainPhraseDialog @@ -120,7 +120,14 @@ class ProfileSettingsFragment : BaseBottomSheetComposeFragment() { } } ), - clearProfileImage = { vm.onClearProfileImage() } + clearProfileImage = { vm.onClearProfileImage() }, + onDebugClicked = { + runCatching { + findNavController().navigate(R.id.debugScreen) + } + }, + isDebugEnabled = vm.isDebugEnabled.collectAsStateWithLifecycle().value, + onHeaderTitleClicked = vm::onHeaderTitleClicked ) } } @@ -140,13 +147,12 @@ class ProfileSettingsFragment : BaseBottomSheetComposeFragment() { } private fun proceedWithIconClick() { - try { - pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) - } catch (e: Exception) { - Timber.w(e, "Error while opening photo picker") - toast("Error while opening photo picker") - permissionHelper.openFilePicker(Mimetype.MIME_IMAGE_ALL, null) - } + launchMediaPicker( + pickMedia = pickMedia, + permissionHelper = permissionHelper, + mediaType = PickVisualMedia.ImageOnly, + fallbackMimeType = Mimetype.MIME_IMAGE_ALL + ) } private fun openGallery() { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/spaces/CreateSpaceFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/spaces/CreateSpaceFragment.kt index f8f4712ea6..39fae64634 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/spaces/CreateSpaceFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/spaces/CreateSpaceFragment.kt @@ -63,9 +63,6 @@ class CreateSpaceFragment : BaseBottomSheetComposeFragment() { deeplink = null ) ) -// if (command.showMultiplayerTooltip) { -// findNavController().navigate(R.id.multiplayerFeatureDialog) -// } }.onFailure { Timber.e(it, "Error while exiting to vault or opening created space") } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt index a5161d767b..5490c29e48 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt @@ -18,7 +18,6 @@ import com.anytypeio.anytype.core_utils.ext.orNull import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.ext.visible import com.anytypeio.anytype.core_utils.ui.BaseFragment -import com.anytypeio.anytype.core_utils.ui.ViewState import com.anytypeio.anytype.databinding.FragmentSplashBinding import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.other.DefaultDeepLinkResolver @@ -31,6 +30,8 @@ import com.anytypeio.anytype.ui.editor.EditorFragment import com.anytypeio.anytype.ui.home.HomeScreenFragment import com.anytypeio.anytype.ui.onboarding.OnboardingFragment import com.anytypeio.anytype.ui.sets.ObjectSetFragment +import com.anytypeio.anytype.ui.update.MigrationFailedScreen +import com.anytypeio.anytype.ui.update.MigrationInProgressScreen import com.anytypeio.anytype.ui.vault.VaultFragment import javax.inject.Inject import kotlinx.coroutines.launch @@ -64,31 +65,37 @@ class SplashFragment : BaseFragment(R.layout.fragment_spl launch { vm.state.collect { state -> when(state) { - is ViewState.Error -> { - binding.error.text = state.error + is SplashViewModel.State.Init -> { + binding.error.gone() + binding.compose.visibility = View.GONE + } + is SplashViewModel.State.Error -> { + binding.error.text = state.msg binding.error.visible() } - else -> { - binding.error.gone() - binding.error.text = "" - } - } - } - } - - launch { - vm.loadingState.collect { isLoading -> - when (isLoading) { - true -> { - binding.loadingContainer.setContent { + is SplashViewModel.State.Loading -> { + binding.compose.setContent { PulsatingCircleScreen() } - binding.logo.visibility = View.GONE - binding.loadingContainer.visibility = View.VISIBLE + binding.compose.visible() } - false -> { - binding.logo.visibility = View.GONE - binding.loadingContainer.visibility = View.GONE + is SplashViewModel.State.Migration -> { + binding.compose.setContent { + if (state is SplashViewModel.State.Migration.InProgress) { + MigrationInProgressScreen() + } else if (state is SplashViewModel.State.Migration.Failed) { + MigrationFailedScreen( + state = state.state, + onRetryClicked = vm::onRetryMigrationClicked + ) + } + } + binding.compose.visible() + } + is SplashViewModel.State.Success -> { + binding.compose.gone() + binding.error.gone() + binding.error.text = "" } } } @@ -271,11 +278,6 @@ class SplashFragment : BaseFragment(R.layout.fragment_spl args = OnboardingFragment.args(deepLink) ) } - is SplashViewModel.Command.NavigateToMigration -> { - findNavController().navigate( - R.id.migrationNeededScreen - ) - } is SplashViewModel.Command.CheckAppStartIntent -> { val intent = activity?.intent if (intent != null && (intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND)) { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorFragment.kt deleted file mode 100644 index d9b06acb8b..0000000000 --- a/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorFragment.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.anytypeio.anytype.ui.update - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.material.MaterialTheme -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment -import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.presentation.update.MigrationErrorViewModel -import com.anytypeio.anytype.ui.base.navigation -import com.anytypeio.anytype.ui.settings.typography -import javax.inject.Inject -import kotlinx.coroutines.launch -import timber.log.Timber - -class MigrationErrorFragment : BaseComposeFragment() { - - @Inject - lateinit var factory: MigrationErrorViewModel.Factory - - private val vm by viewModels { factory } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView( - context = requireContext() - ).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - MaterialTheme(typography = typography) { - MigrationErrorScreen(vm::onAction) - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - vm.commands.collect { command -> - when(command) { - is MigrationErrorViewModel.Command.Browse -> { - browseUrl(command) - } - is MigrationErrorViewModel.Command.Exit -> { - navigation().exitFromMigrationScreen() - } - } - } - } - } - } - - private fun browseUrl(command: MigrationErrorViewModel.Command.Browse) { - try { - Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(command.url) - }.let(::startActivity) - } catch (e: Exception) { - Timber.e(e, "Error while browsing url") - } - } - - override fun injectDependencies() { - componentManager().migrationErrorComponent.get().inject(this) - } - - override fun releaseDependencies() { - componentManager().migrationErrorComponent.release() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt index 049aa7ef2f..aa3d6d4407 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt @@ -1,219 +1,159 @@ package com.anytypeio.anytype.ui.update -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +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.shape.RoundedCornerShape -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Card +import androidx.compose.foundation.layout.size +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable -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.draw.rotate +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.anytypeio.anytype.R -import com.anytypeio.anytype.core_ui.foundation.noRippleClickable -import com.anytypeio.anytype.core_ui.views.BodyCallout -import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.core_ui.foundation.AlertConfig +import com.anytypeio.anytype.core_ui.foundation.AlertIcon +import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED +import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular import com.anytypeio.anytype.core_ui.views.ButtonPrimary import com.anytypeio.anytype.core_ui.views.ButtonSize import com.anytypeio.anytype.core_ui.views.HeadlineHeading -import com.anytypeio.anytype.core_ui.views.HeadlineSubheading -import com.anytypeio.anytype.presentation.update.MigrationErrorViewModel.ViewAction -import kotlinx.coroutines.launch - +import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate @Composable -fun MigrationErrorScreen(onViewAction: (ViewAction) -> Unit) { +fun MigrationInProgressScreen() { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = colorResource(id = R.color.background_primary)), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + CircularProgressIndicator( + modifier = Modifier + .size(96.dp) + .align(Alignment.CenterHorizontally) + , + backgroundColor = colorResource(R.color.shape_secondary), + color = Color(0xFFFFB522), + strokeWidth = 8.dp + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.migration_migration_is_in_progress), + style = HeadlineHeading, + color = colorResource(R.color.text_primary), + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 44.dp) + .fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.migration_this_shouldn_t_take_long), + style = BodyCalloutRegular, + color = colorResource(R.color.text_secondary), + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 44.dp) + .fillMaxWidth() + ) + } + } +} + +@Composable +fun MigrationFailedScreen( + state: MigrationHelperDelegate.State.Failed, + onRetryClicked: () -> Unit +) { + val description = when(state) { + MigrationHelperDelegate.State.Failed.NotEnoughSpace -> { + stringResource(R.string.migration_error_please_free_up_space_and_run_the_process_again) + } + is MigrationHelperDelegate.State.Failed.UnknownError -> { + state.error.message ?: stringResource(R.string.unknown_error) + } + } Box( modifier = Modifier .fillMaxSize() .background(color = colorResource(id = R.color.background_primary)) ) { - Cards(onViewAction) - CloseButton(closeClicks = { onViewAction(ViewAction.CloseScreen) }) - BackHandler(enabled = true) { onViewAction(ViewAction.CloseScreen) } - } -} - -@Composable -fun Cards(onViewAction: (ViewAction) -> Unit) { - Column(modifier = Modifier.padding(horizontal = 20.dp)) { - Text( - text = stringResource(id = R.string.almost_there), - style = HeadlineHeading, - color = colorResource(id = R.color.text_primary), - modifier = Modifier.padding(top = 56.dp) - ) - Text( - text = stringResource(id = R.string.almost_there_subtitle), - style = BodyRegular, - color = colorResource(id = R.color.text_primary), - modifier = Modifier.padding(top = 12.dp) - ) - InfoCard( - modifier = Modifier.padding(top = 32.dp), - title = stringResource(id = R.string.i_did_not_not_complete_migration), - toggleClick = { onViewAction(ViewAction.ToggleMigrationNotReady) }, - expanded = true, - content = { - val hereText = stringResource(id = R.string.here) - val text = buildAnnotatedString { - append(stringResource(id = R.string.update_steps_first)) - append(" ") - pushStringAnnotation( - tag = ANNOTATION_TAG, - annotation = hereText - ) - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(hereText) - } - pop() - append(stringResource(R.string.update_steps_last)) - } - ClickableText( - modifier = Modifier.padding(top = 12.dp), - text = text, - style = BodyCallout.copy( - color = colorResource(id = R.color.text_primary) - ), - onClick = { offset -> - text.getStringAnnotations( - tag = ANNOTATION_TAG, - start = offset, - end = offset - ).firstOrNull().let { - if (it?.item == hereText) { - onViewAction(ViewAction.DownloadDesktop) - } - } - }, + Column( + modifier = Modifier.align(Alignment.Center) + ) { + AlertIcon( + icon = AlertConfig.Icon( + gradient = GRADIENT_TYPE_RED, + icon = R.drawable.ic_alert_error ) - }, - ) - InfoCard( - modifier = Modifier.padding(top = 20.dp), - title = stringResource(id = R.string.i_completed_migration), - expanded = false, - toggleClick = { onViewAction(ViewAction.ToggleMigrationReady) }, - content = { - Column { - Text( - modifier = Modifier.padding(top = 12.dp), - text = stringResource(id = R.string.migration_error_msg), - style = BodyCallout, - color = colorResource(id = R.color.text_primary) - ) - ButtonPrimary( - text = stringResource(id = R.string.visit_forum), - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - onClick = { onViewAction(ViewAction.VisitForum) }, - size = ButtonSize.Large - ) - } - } - ) - } -} - -@Composable -fun InfoCard( - modifier: Modifier = Modifier, - title: String, - expanded: Boolean, - toggleClick: () -> Unit, - content: @Composable AnimatedVisibilityScope.() -> Unit -) { - - val cardOpened = remember { mutableStateOf(expanded) } - - val rotationDegree = remember { - Animatable( - if (expanded) ROTATION_CLOSED else ROTATION_OPENED - ) - } - val coroutineScope = rememberCoroutineScope() - - Card( - modifier = modifier, - backgroundColor = colorResource(id = R.color.shape_transparent), - elevation = 0.dp, - shape = RoundedCornerShape(16.dp) - ) { - Box { - Image( - painter = painterResource(id = R.drawable.icon_migration_card_arrow), - contentDescription = "", - modifier = Modifier - .align(Alignment.TopEnd) - .padding(top = 22.dp, end = 12.dp) - .rotate(rotationDegree.value) - .noRippleClickable { - cardOpened.value = !cardOpened.value - coroutineScope.launch { - if (cardOpened.value) { - toggleClick() - rotationDegree.animateTo(ROTATION_CLOSED) - } else { - rotationDegree.animateTo(ROTATION_OPENED) - } - } - } ) - - Column( - Modifier - .fillMaxWidth() - .padding(20.dp) - ) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.migration_migration_failed), + style = HeadlineHeading, + color = colorResource(R.color.text_primary), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + if (description.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) Text( - text = title, - style = HeadlineSubheading, - color = colorResource(id = R.color.text_primary) + text = description, + color = colorResource(R.color.text_secondary), + style = BodyCalloutRegular, + modifier = Modifier.padding(horizontal = 20.dp).fillMaxWidth(), + textAlign = TextAlign.Center ) - AnimatedVisibility(visible = cardOpened.value) { - content() - } } } + ButtonPrimary( + modifier = Modifier + .padding(20.dp) + .align(Alignment.BottomCenter) + .fillMaxWidth(), + text = stringResource(R.string.migration_error_try_again), + size = ButtonSize.Large, + onClick = onRetryClicked + ) } } - -private const val ANNOTATION_TAG = "here_text_tag" -private const val ROTATION_OPENED = 0F -private const val ROTATION_CLOSED = 180F - +@DefaultPreviews @Composable -private fun CloseButton(closeClicks: () -> Unit) { - Box(modifier = Modifier.fillMaxSize()) { - Image(painter = painterResource(id = R.drawable.ic_navigation_close), - contentDescription = "close image", - modifier = Modifier - .align(Alignment.TopEnd) - .padding(top = 12.dp, end = 12.dp) - .noRippleClickable { closeClicks.invoke() } - ) - } +fun MigrationInProgressScreenPreview() { + MigrationInProgressScreen() +} + +@DefaultPreviews +@Composable +fun MigrationFailedScreenPreview() { + MigrationFailedScreen( + state = MigrationHelperDelegate.State.Failed.NotEnoughSpace, + onRetryClicked = {} + ) +} + +@DefaultPreviews +@Composable +fun MigrationFailedGenericScreenPreview() { + MigrationFailedScreen( + state = MigrationHelperDelegate.State.Failed.UnknownError( + Exception(stringResource(R.string.default_text_placeholder)) + ), + onRetryClicked = {} + ) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/IntroduceVaultFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/IntroduceVaultFragment.kt index 9b254f617e..bb62fcb265 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/IntroduceVaultFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/IntroduceVaultFragment.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment import com.anytypeio.anytype.ui.settings.typography +@Deprecated("Outdated. To be deleted soon.") class IntroduceVaultFragment : BaseBottomSheetComposeFragment() { override fun onCreateView( diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/IntroduceVaultScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/IntroduceVaultScreen.kt index 90b3876c5b..aa404a0205 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/IntroduceVaultScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/IntroduceVaultScreen.kt @@ -34,6 +34,7 @@ import com.anytypeio.anytype.core_ui.views.ButtonSize import com.anytypeio.anytype.core_ui.views.HeadlineHeading import kotlinx.coroutines.launch +@Deprecated("To be deleted") @Composable fun IntroduceVaultScreen( onDoneClicked: () -> Unit diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt index 6d00e0617f..bb524ab30a 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt @@ -117,13 +117,6 @@ class VaultFragment : BaseComposeFragment() { Timber.e(it, "Error while opening profile settings from vault") } } - is Command.ShowIntroduceVault -> { - runCatching { - findNavController().navigate(R.id.actionShowIntroduceVaultScreen) - }.onFailure { - Timber.e(it, "Error while opening introduce-vault-screen from vault") - } - } is Command.Deeplink.Invite -> { findNavController().navigate( R.id.requestJoinSpaceScreen, diff --git a/app/src/main/res/layout/fragment_object_menu.xml b/app/src/main/res/layout/fragment_object_menu.xml index d7bd9d1c9a..7bf4498f39 100644 --- a/app/src/main/res/layout/fragment_object_menu.xml +++ b/app/src/main/res/layout/fragment_object_menu.xml @@ -1,209 +1,184 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> + android:background="@drawable/dragger" /> + + + + - + + + + - - - - + app:title="@string/description" /> - + - + - + - + - + - + - - + + + + + + android:layout_marginTop="12dp" + android:layout_weight="0"> + android:layout_height="match_parent" + android:layout_gravity="center_vertical" /> @@ -211,7 +186,7 @@ android:id="@+id/anchor" android:layout_width="match_parent" android:layout_height="1dp" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" - /> - \ No newline at end of file + app:layout_constraintEnd_toEndOf="parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml index 1d95f2888f..fabfb3e84e 100644 --- a/app/src/main/res/layout/fragment_splash.xml +++ b/app/src/main/res/layout/fragment_splash.xml @@ -5,13 +5,6 @@ android:layout_height="match_parent" android:background="@color/background_primary"> - - + android:visibility="gone" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_object_set_header.xml b/app/src/main/res/layout/layout_object_set_header.xml index bd5f6d006f..df57ab26ac 100644 --- a/app/src/main/res/layout/layout_object_set_header.xml +++ b/app/src/main/res/layout/layout_object_set_header.xml @@ -84,7 +84,6 @@ android:ellipsize="end" android:hint="@string/description" android:inputType="textMultiLine" - android:maxLines="3" android:textColorHint="@color/text_tertiary" tools:text="Description" /> diff --git a/app/src/main/res/navigation/graph.xml b/app/src/main/res/navigation/graph.xml index e5089aa9ec..4d4c1b23dc 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"> + + + + + + @@ -272,9 +291,6 @@ - + + + - - - 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 0ff50435d0..e4dc73c82a 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() { @@ -244,6 +271,7 @@ sealed class ObjectWrapper { val id: Id by default val name: String? by default + val description: String? = getSingleValue(Relations.DESCRIPTION) val iconImage: String? get() = getSingleValue(Relations.ICON_IMAGE) val iconOption: Double? by default @@ -292,6 +320,7 @@ sealed class ObjectWrapper { return spaceLocalStatus == SpaceStatus.LOADING && spaceAccountStatus != SpaceStatus.SPACE_REMOVING && spaceAccountStatus != SpaceStatus.SPACE_DELETED + && spaceAccountStatus != SpaceStatus.SPACE_JOINING } val isActive: Boolean 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/chats/Chat.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/chats/Chat.kt index 1c66fbdb22..4e28968cb2 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/chats/Chat.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/chats/Chat.kt @@ -67,6 +67,7 @@ sealed class Chat { id: Id, text: String, attachments: List = emptyList(), + marks: List ) : Message = Message( id = id, createdAt = 0L, @@ -77,7 +78,7 @@ sealed class Chat { replyToMessageId = "", content = Content( text = text, - marks = emptyList(), + marks = marks, style = Block.Content.Text.Style.P ), order = "" diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/exceptions/AccountIsDeletedException.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/exceptions/AccountIsDeletedException.kt index 4e0743340e..a9b78e1f66 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/exceptions/AccountIsDeletedException.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/exceptions/AccountIsDeletedException.kt @@ -1,5 +1,9 @@ package com.anytypeio.anytype.core_models.exceptions class AccountIsDeletedException : Exception() -class MigrationNeededException: Exception() -class NeedToUpdateApplicationException: Exception() \ No newline at end of file +class NeedToUpdateApplicationException: Exception() +class AccountMigrationNeededException: Exception() + +sealed class MigrationFailedException : Exception() { + class NotEnoughSpace : MigrationFailedException() +} \ No newline at end of file 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-models/src/main/java/com/anytypeio/anytype/core_models/settings/VaultSettings.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/settings/VaultSettings.kt index bbe6d5a3ba..2429f897c0 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/settings/VaultSettings.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/settings/VaultSettings.kt @@ -5,14 +5,12 @@ import com.anytypeio.anytype.core_models.FALLBACK_DATE_PATTERN import com.anytypeio.anytype.core_models.Id data class VaultSettings( - val showIntroduceVault: Boolean, val orderOfSpaces: List = emptyList(), val isRelativeDates: Boolean, val dateFormat: String ) { companion object { fun default() : VaultSettings = VaultSettings( - showIntroduceVault = false, orderOfSpaces = emptyList(), isRelativeDates = DEFAULT_RELATIVE_DATES, dateFormat = FALLBACK_DATE_PATTERN 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/ComposeCommons.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/ComposeCommons.kt index 7d89d9ff62..5b222b9d78 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/ComposeCommons.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/common/ComposeCommons.kt @@ -5,10 +5,42 @@ import androidx.compose.foundation.layout.ime import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.rememberUpdatedState +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.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R @Composable fun keyboardAsState(): State { val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0 return rememberUpdatedState(isImeVisible) -} \ No newline at end of file +} + +@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 + ) + } + } +) \ No newline at end of file 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/extensions/ComposableExtensions.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ComposableExtensions.kt index 7cd5605154..61dbf82227 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ComposableExtensions.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ComposableExtensions.kt @@ -40,6 +40,12 @@ fun dark( } } +@Composable +fun dark(code: String): Color { + val colorTheme = ThemeColor.entries.find { it.code == code } ?: ThemeColor.DEFAULT + return dark(colorTheme) +} + @Composable fun light( color: ThemeColor @@ -57,6 +63,12 @@ fun light( ThemeColor.DEFAULT -> colorResource(id = R.color.palette_light_default) } +@Composable +fun light(code: String): Color { + val colorTheme = ThemeColor.entries.find { it.code == code } ?: ThemeColor.DEFAULT + return light(colorTheme) +} + @OptIn(ExperimentalFoundationApi::class) fun Modifier.bouncingClickable( enabled: Boolean = true, @@ -95,7 +107,7 @@ fun Modifier.bouncingClickable( ) } -fun SnapshotStateList.swapList(newList: List){ +fun SnapshotStateList.swapList(newList: List) { clear() addAll(newList) } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ResExtension.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ResExtension.kt index daae3134f4..cc76110be5 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ResExtension.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/extensions/ResExtension.kt @@ -278,8 +278,7 @@ fun ObjectLayoutView.getName(): Int? = when (this) { @StringRes fun RelationFormat.getPrettyName(): Int = when (this) { - RelationFormat.LONG_TEXT -> R.string.relation_format_long_text - RelationFormat.SHORT_TEXT -> R.string.relation_format_short_text + RelationFormat.LONG_TEXT, RelationFormat.SHORT_TEXT -> R.string.relation_format_long_text RelationFormat.NUMBER -> R.string.relation_format_number RelationFormat.STATUS -> R.string.relation_format_status RelationFormat.TAG -> R.string.relation_format_tag diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt index 68cfeba72b..5390f19240 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt @@ -1339,7 +1339,8 @@ class BlockAdapter( bind( item = blocks[position] as BlockView.Title.Basic, onPageIconClicked = onPageIconClicked, - onCoverClicked = onCoverClicked + onCoverClicked = onCoverClicked, + click = onClickListener ) setTextInputClickListener { if (Build.VERSION.SDK_INT == N || Build.VERSION.SDK_INT == N_MR1) { @@ -1356,7 +1357,8 @@ class BlockAdapter( bind( item = blocks[position] as BlockView.Title.Todo, onPageIconClicked = onPageIconClicked, - onCoverClicked = onCoverClicked + onCoverClicked = onCoverClicked, + click = onClickListener ) setTextInputClickListener { if (Build.VERSION.SDK_INT == N || Build.VERSION.SDK_INT == N_MR1) { @@ -1373,7 +1375,8 @@ class BlockAdapter( bind( item = blocks[position] as BlockView.Title.Profile, onProfileIconClicked = onClickListener, - onCoverClicked = onCoverClicked + onCoverClicked = onCoverClicked, + click = onClickListener ) setTextInputClickListener { if (Build.VERSION.SDK_INT == N || Build.VERSION.SDK_INT == N_MR1) { diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/CustomImageResizeTransformation.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/CustomImageResizeTransformation.kt new file mode 100644 index 0000000000..d59bcdc7c4 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/CustomImageResizeTransformation.kt @@ -0,0 +1,71 @@ +package com.anytypeio.anytype.core_ui.features.editor.holders.other + +import android.graphics.Bitmap +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import java.security.MessageDigest +import timber.log.Timber + +class CustomImageResizeTransformation( + private val maxWidth: Int, + private val maxHeight: Int +) : BitmapTransformation() { + + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int + ): Bitmap { + return try { + val imageWidth = toTransform.width + val imageHeight = toTransform.height + val targetAspectRatio = maxWidth.toFloat() / maxHeight + + when { + imageWidth > maxWidth && imageHeight > maxHeight -> { + val imageAspectRatio = imageWidth.toFloat() / imageHeight + + if (imageAspectRatio > targetAspectRatio) { + val cropWidth = (imageHeight * targetAspectRatio).toInt() + val cropStartX = (imageWidth - cropWidth) / 2 + Bitmap.createBitmap(toTransform, cropStartX, 0, cropWidth, imageHeight) + } else { + val cropHeight = (imageWidth / targetAspectRatio).toInt() + val cropStartY = (imageHeight - cropHeight) / 2 + Bitmap.createBitmap(toTransform, 0, cropStartY, imageWidth, cropHeight) + } + } + imageWidth > maxWidth && imageHeight <= maxHeight -> { + val scaleFactor = maxWidth.toFloat() / imageWidth + val newHeight = (imageHeight * scaleFactor).toInt() + Bitmap.createScaledBitmap(toTransform, maxWidth, newHeight, true) + } + imageHeight > maxHeight && imageWidth <= maxWidth -> { + val cropHeight = (imageWidth / targetAspectRatio).toInt() + val cropStartY = (imageHeight - cropHeight) / 2 + Bitmap.createBitmap(toTransform, 0, cropStartY, imageWidth, cropHeight) + } + else -> toTransform + } + } catch (e: IllegalArgumentException) { + Timber.e( + e, + "Failed to transform bitmap: Invalid dimensions or parameters provided. Width: ${toTransform.width}, Height: ${toTransform.height}, MaxWidth: $maxWidth, MaxHeight: $maxHeight" + ) + toTransform + } catch (e: OutOfMemoryError) { + Timber.e( + e, + "Failed to transform bitmap: Insufficient memory to process the image." + ) + toTransform + } + } + + override fun equals(other: Any?) = other is CustomImageResizeTransformation + override fun hashCode() = "CustomImageResizeTransformation".hashCode() + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update("CustomImageResizeTransformation".toByteArray()) + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/Title.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/Title.kt index 442aa63fb7..12ab093d4f 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/Title.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/Title.kt @@ -1,6 +1,9 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.other +import android.content.Context +import android.graphics.Bitmap import android.text.Spannable +import android.util.TypedValue import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.FrameLayout.LayoutParams @@ -38,6 +41,11 @@ import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType import com.anytypeio.anytype.presentation.editor.editor.model.BlockView import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import java.security.MessageDigest import timber.log.Timber sealed class Title(view: View) : BlockViewHolder(view), TextHolder { @@ -50,7 +58,8 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { fun bind( item: BlockView.Title, - onCoverClicked: () -> Unit + onCoverClicked: () -> Unit, + click: (ListenerType) -> Unit ) { setImage(item) applyTextColor(item) @@ -162,22 +171,34 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { } } } - open fun setImage(item: BlockView.Title) { + Timber.d("Setting image for ${item.id}, image=${item.image}") item.image?.let { url -> image.visible() - Glide - .with(image) - .load(url) - .centerCrop() - .into(image) - } ?: apply { image.setImageDrawable(null) } + loadImageWithCustomResize(image, url) + } ?: run { image.setImageDrawable(null) } } - private fun showKeyboard() { - content.postDelayed(16L) { - imm().showSoftInput(content, InputMethodManager.SHOW_IMPLICIT) - } + private fun loadImageWithCustomResize(imageView: ImageView, url: String) { + val context = imageView.context + val displayMetrics = context.resources.displayMetrics + val screenWidth = displayMetrics.widthPixels + val maxWidth = screenWidth - dpToPx(context, 40) + val maxHeight = dpToPx(context, 443) + + Glide.with(context) + .load(url) + .override(Target.SIZE_ORIGINAL) + .apply(RequestOptions().transform(CustomImageResizeTransformation(maxWidth, maxHeight))) + .into(imageView) + } + + private fun dpToPx(context: Context, dp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + context.resources.displayMetrics + ).toInt() } open fun processPayloads( @@ -275,14 +296,25 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { fun bind( item: BlockView.Title.Basic, onPageIconClicked: () -> Unit, - onCoverClicked: () -> Unit + onCoverClicked: () -> Unit, + click: (ListenerType) -> Unit ) { super.bind( item = item, - onCoverClicked = onCoverClicked + onCoverClicked = onCoverClicked, + click = click ) setEmoji(item) applySearchHighlights(item) + + image.setOnClickListener { + click( + ListenerType.Picture.TitleView( + item = item + ) + ) + } + if (item.mode == BlockView.Mode.EDIT) { icon.setOnClickListener { onPageIconClicked() } image.setOnClickListener { onPageIconClicked() } @@ -299,9 +331,11 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { topMargin = dimen(R.dimen.dp_10) } binding.imageIcon.updateLayoutParams { - topMargin = if (!item.hasCover) dimen(R.dimen.dp_51) else dimen(R.dimen.dp_102) + topMargin = + if (!item.hasCover) dimen(R.dimen.dp_51) else dimen(R.dimen.dp_102) } } + item.emoji != null -> { binding.imageIcon.gone() binding.docEmojiIconContainer.visible() @@ -309,9 +343,11 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { topMargin = dimen(R.dimen.dp_12) } binding.docEmojiIconContainer.updateLayoutParams { - topMargin = if (!item.hasCover) dimen(R.dimen.dp_60) else dimen(R.dimen.dp_120) + topMargin = + if (!item.hasCover) dimen(R.dimen.dp_60) else dimen(R.dimen.dp_120) } } + else -> { binding.imageIcon.gone() binding.docEmojiIconContainer.gone() @@ -397,9 +433,10 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { override val content: TextInputWidget = binding.title override val selectionView: View = itemView - private val gradientView : ComposeView get() = binding - .docProfileIconContainer - .findViewById(R.id.gradient) + private val gradientView: ComposeView + get() = binding + .docProfileIconContainer + .findViewById(R.id.gradient) private val iconText = binding.imageText private var hasImage = false @@ -411,11 +448,13 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { fun bind( item: BlockView.Title.Profile, onProfileIconClicked: (ListenerType) -> Unit, - onCoverClicked: () -> Unit + onCoverClicked: () -> Unit, + click: (ListenerType) -> Unit ) { super.bind( item = item, - onCoverClicked = onCoverClicked + onCoverClicked = onCoverClicked, + click = click ) setupMargins(item) applySearchHighlights(item) @@ -512,11 +551,13 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { fun bind( item: BlockView.Title.Todo, onPageIconClicked: () -> Unit, - onCoverClicked: () -> Unit + onCoverClicked: () -> Unit, + click: (ListenerType) -> Unit ) { super.bind( item = item, - onCoverClicked = onCoverClicked + onCoverClicked = onCoverClicked, + click = click ) setLocked(item.mode) checkbox.isSelected = item.isChecked @@ -576,7 +617,8 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { ) { super.bind( item = item, - onCoverClicked = {} + onCoverClicked = {}, + click = {} ) icon.setIcon(item.icon) } 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 new file mode 100644 index 0000000000..ae050de3cd --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldEmpty.kt @@ -0,0 +1,247 @@ +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 +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.width +import androidx.compose.foundation.layout.widthIn +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 +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.Relation +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.views.Relations1 + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FieldEmpty( + modifier: Modifier = Modifier, + title: String, + fieldFormat: RelationFormat, + isLocal: Boolean, + onFieldClick: () -> Unit, + onAddToCurrentTypeClick: () -> Unit, + onRemoveFromObjectClick: () -> Unit, +) { + val defaultModifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + when (fieldFormat) { + Relation.Format.LONG_TEXT, + Relation.Format.SHORT_TEXT, + Relation.Format.URL -> { + val emptyState = getEnterValueText(fieldFormat) + FieldVerticalEmpty( + modifier = defaultModifier, + title = title, + emptyState = emptyState, + isLocal = isLocal, + onFieldClick = onFieldClick, + onAddToCurrentTypeClick = onAddToCurrentTypeClick, + onRemoveFromObjectClick = onRemoveFromObjectClick + ) + } + + else -> { + val emptyState = getEnterValueText(fieldFormat) + FieldHorizontalEmpty( + modifier = defaultModifier, + title = title, + 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 + .combinedClickable( + onClick = { onFieldClick()}, + onLongClick = { + if (isLocal) isMenuExpanded.value = true + } + ) + .padding(horizontal = 16.dp, vertical = 16.dp) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = emptyState, + style = Relations1, + color = colorResource(id = R.color.text_tertiary), + 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( + modifier = Modifier.widthIn(max = halfScreenWidth), + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = emptyState, + 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() + } + ) + } +} + +@Composable +private fun getEnterValueText(format: RelationFormat): String { + return when (format) { + Relation.Format.LONG_TEXT, + Relation.Format.SHORT_TEXT -> stringResource(R.string.field_text_empty) + + Relation.Format.NUMBER -> stringResource(R.string.field_number_empty) + Relation.Format.DATE -> stringResource(R.string.field_date_empty) + Relation.Format.CHECKBOX -> "" + Relation.Format.URL -> stringResource(R.string.field_url_empty) + Relation.Format.EMAIL -> stringResource(R.string.field_email_empty) + Relation.Format.PHONE -> stringResource(R.string.field_phone_empty) + Relation.Format.OBJECT -> stringResource(R.string.field_object_empty) + else -> "" + } +} + +@DefaultPreviews +@Composable +fun PreviewField() { + LazyColumn( + modifier = Modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + item { + Spacer(modifier = Modifier.height(12.dp)) + } + item { + FieldEmpty( + title = "Description", + 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, + isLocal = true, + onFieldClick = {}, + onAddToCurrentTypeClick = {}, + onRemoveFromObjectClick = {} + ) + } + item { + Spacer(modifier = Modifier.height(12.dp)) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..3b8272e5b1 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeCheckbox.kt @@ -0,0 +1,129 @@ +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 +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +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 +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_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, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(10.dp)) + Box( + modifier = Modifier.widthIn(max = halfScreenWidth) + ) { + if (isCheck) { + Image( + painter = painterResource(id = R.drawable.ic_checkbox_checked), + contentDescription = "Checkbox", + modifier = Modifier.size(24.dp) + ) + } else { + Image( + painter = painterResource(id = R.drawable.ic_checkbox_default), + contentDescription = "Checkbox", + modifier = Modifier.size(24.dp) + ) + } + } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) + } +} + + +@DefaultPreviews +@Composable +fun FieldTypeCheckboxPreview() { + FieldTypeCheckbox( + title = "Creation date", + 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 new file mode 100644 index 0000000000..ba8fcdb7cf --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeDate.kt @@ -0,0 +1,128 @@ +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 +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +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 +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.DayOfWeekCustom +import com.anytypeio.anytype.core_models.RelativeDate +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.views.BodyCallout +import com.anytypeio.anytype.core_ui.views.Relations1 + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FieldTypeDate( + modifier: Modifier = Modifier, + title: String, + 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, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(10.dp)) + Box( + modifier = Modifier.widthIn(max = halfScreenWidth) + ) { + Text( + text = relativeDate.getPrettyName(), + style = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) + } +} + + +@DefaultPreviews +@Composable +fun FieldTypeDatePreview() { + FieldTypeDate( + title = "Creation date", + relativeDate = RelativeDate.Tomorrow( + 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 new file mode 100644 index 0000000000..ed4cc5688c --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeFile.kt @@ -0,0 +1,293 @@ +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 +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.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +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 +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.views.BodyCallout +import com.anytypeio.anytype.core_ui.views.Relations1 +import com.anytypeio.anytype.core_ui.views.Relations2 +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, + 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, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .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() + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = fieldObject.name, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Box( + modifier = Modifier.widthIn(max = halfScreenWidth) + ) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = singleItem + ) + } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) + } + } else { + Column( + modifier = defaultModifier + ) { + Text( + modifier = Modifier.wrapContentWidth(), + text = fieldObject.name, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(10.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + ) { + // The first item (if present) + if (fieldObject.files.isNotEmpty()) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + ) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = fieldObject.files.first() + ) + } + } + // The second item (if present) + if (fieldObject.files.size > 1) { + Spacer(modifier = Modifier.width(8.dp)) + Box(modifier = Modifier.widthIn(max = halfScreenWidth)) { + if (fieldObject.files.size == 2) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = fieldObject.files[1] + ) + } else { + // If there are more than two items, display the second item with a "+n" suffix. + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + ListWidgetObjectIcon( + icon = fieldObject.files[1].icon, + iconSize = 18.dp, + modifier = Modifier, + onTaskIconClicked = { + // Do nothing + } + ) + Spacer(modifier = Modifier.width(4.dp)) + // The main text with an integrated suffix that occupies the remaining space. + FileNameWithSuffix( + text = fieldObject.files[1].name, + suffix = "+${fieldObject.files.size - 2}", + textStyle = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + countStyle = Relations2.copy( + color = colorResource(id = R.color.text_secondary) + ), + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + .padding(vertical = 2.dp) + ) + } + } + } + } + } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) + } + } +} + +// Helper function to display a single item: icon (if available) + text. +@Composable +internal fun ItemView(modifier: Modifier, objView: FileView) { + Row( + modifier = modifier.padding(vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ListWidgetObjectIcon( + icon = objView.icon, + iconSize = 18.dp, + modifier = Modifier, + onTaskIconClicked = { + // Do nothing + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = objView.name, + style = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +/** + * A composable that displays a row consisting of the main text and a suffix. + * If the main text is short enough, the suffix (for example, "+n") + * will appear immediately after it; if the text is long, it will be truncated (with an ellipsis) + * to leave space for the suffix. + */ +@Composable +internal fun FileNameWithSuffix( + text: String, + suffix: String, + textStyle: TextStyle, + countStyle: TextStyle, + modifier: Modifier = Modifier +) { + val density = LocalDensity.current + + SubcomposeLayout(modifier = modifier) { constraints -> + val suffixConstraints = constraints.copy(minWidth = 0, maxWidth = Constraints.Infinity) + val suffixPlaceable = subcompose("suffix") { + Box( + modifier = Modifier.background( + color = colorResource(R.color.shape_tertiary), + shape = RoundedCornerShape(4.dp) + ) + ) { + Text( + modifier = Modifier.padding(horizontal = 4.dp), + text = suffix, + style = countStyle, + maxLines = 1, + ) + } + }.first().measure(suffixConstraints) + + // The available space for the main text is the total width minus the width of the suffix. + val availableWidthForText = (constraints.maxWidth - suffixPlaceable.width).coerceAtLeast(0) + val textConstraints = constraints.copy(minWidth = 0, maxWidth = availableWidthForText) + val textPlaceable = subcompose("text") { + Text( + text = text, + style = textStyle, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }.first().measure(textConstraints) + + // If width constraints are specified (e.g., when using weight), use the full available width. + val finalWidth = + if (constraints.hasBoundedWidth) constraints.maxWidth else (textPlaceable.width + suffixPlaceable.width) + val height = maxOf(textPlaceable.height, suffixPlaceable.height) + layout(finalWidth, height) { + // Align content to the left. + textPlaceable.placeRelative(0, 0) + val offsetYPx = with(density) { 0.5.dp.roundToPx() } + val offsetXPx = with(density) { 8.dp.roundToPx() } + suffixPlaceable.placeRelative(textPlaceable.width + offsetXPx, offsetYPx) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..19ac70f4f7 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeMultiselect.kt @@ -0,0 +1,313 @@ +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 +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +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 +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.extensions.dark +import com.anytypeio.anytype.core_ui.extensions.light +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, + 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, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(10.dp)) + TagRow( + tags = tags, + modifier = Modifier.fillMaxWidth(), + textStyle = Relations1 + ) + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) + } +} + +/** + * A composable that displays a single tag “chip” with text and a background. + * + * @param text The tag text. + * @param backgroundColor The chip’s background color. + * @param textStyle The [TextStyle] used for the tag text. + * @param isSingle If true, the chip is rendered in single-mode – meaning that if the chip does not fit + * in the available width, its text will be truncated (TextOverflow.Ellipsis). + * @param isOverflow If true, this chip is an overflow indicator (e.g. “+3”). + * @param modifier Modifier to be applied to the chip. + */ +@Composable +fun TagChip( + text: String, + tagColor: String, + textStyle: TextStyle, + isSingle: Boolean = false, + isOverflow: Boolean = false, + modifier: Modifier = Modifier +) { + // In single mode, we allow truncation. + Box( + modifier = modifier + .wrapContentWidth() + .background(light(tagColor), shape = RoundedCornerShape(6.dp)) + .padding(horizontal = 6.dp) + ) { + Text( + text = text, + style = textStyle, + color = dark(tagColor), + maxLines = 1, + overflow = if (isSingle) TextOverflow.Ellipsis else TextOverflow.Clip + ) + } +} + +/** + * A composable that lays out a row of tags in a single horizontal line. + * + * The behavior is as follows: + * 1. **Single tag case:** If there is only one tag, it is displayed in a row. If its intrinsic width + * exceeds the available width, the text is truncated (TextOverflow.Ellipsis). + * + * 2. **Multiple tags case:** The layout tries to display as many tags as possible in full (i.e. without truncation). + * - If a tag would be rendered with truncation, it is omitted and all remaining tags are replaced by + * an overflow chip (e.g. “+n”). + * - For example, if the first tag is short and fits but the second tag’s full width would exceed the available space, + * then only the first tag is displayed and an overflow chip shows the remaining count. + * + * @param tags The list of [Tag] objects to display. + * @param modifier Modifier to be applied to the overall layout. + * @param textStyle The [TextStyle] used for the tag text. + * @param spacing The spacing (in dp) between adjacent tags. + * @param overflowChipColor The background color for the overflow chip. + */ +@Composable +fun TagRow( + tags: List, + modifier: Modifier = Modifier, + textStyle: TextStyle, + spacing: Dp = 4.dp, + overflowChipColor: Color = Color.Red +) { + val density = LocalDensity.current + + SubcomposeLayout( + modifier = modifier + .fillMaxWidth() + .wrapContentSize(Alignment.TopStart) + ) { constraints -> + val availableWidth = constraints.maxWidth + val spacingPx = spacing.roundToPx() + + // If there are no tags, layout an empty box. + if (tags.isEmpty()) { + return@SubcomposeLayout layout(0, 0) {} + } + + // --- Single tag case --- + if (tags.size == 1) { + // Render the single tag in "single" mode so that it truncates if needed. + val tagPlaceable = subcompose("tag0") { + TagChip( + modifier = Modifier.padding(horizontal = 4.dp), + text = tags[0].tag, + tagColor = tags[0].color, + textStyle = textStyle, + isSingle = true + ) + }.first().measure(constraints) + return@SubcomposeLayout layout( + width = availableWidth, + height = tagPlaceable.height + ) { + tagPlaceable.placeRelative(0, 0) + } + } + + + // --- Multiple tags case --- + val measuredPlaceables = mutableListOf() + var consumedWidth = 0 + var shownTagCount = 0 + + // Iterate over tags and measure their full intrinsic width (i.e. no truncation). + for ((index, tag) in tags.withIndex()) { + // Measure the tag chip with an "unbounded" width to get its full intrinsic width. + val tagPlaceable = subcompose("tag$index") { + TagChip( + text = tags[index].tag, + tagColor = tags[index].color, + textStyle = textStyle, + isSingle = false + ) + }.first().measure(constraints.copy(maxWidth = Constraints.Infinity)) + + // Calculate additional spacing (if not the first tag). + val additionalSpacing = if (shownTagCount > 0) spacingPx else 0 + + // How many tags would remain if we add this tag? + val remainingCount = tags.size - (shownTagCount + 1) + // Pre-measure an overflow chip if needed, using a unique key. + val overflowPlaceableCandidate = if (remainingCount > 0) { + subcompose("overflow_$index") { + TagChip( + text = "+$remainingCount", + tagColor = tags[index].color, + textStyle = textStyle, + isOverflow = true + ) + }.first().measure(constraints.copy(maxWidth = Constraints.Infinity)) + } else { + null + } + + // Compute candidate width: current consumed width + spacing + tag width + + // (if needed, spacing and overflow chip width) + val candidateWidth = consumedWidth + + additionalSpacing + + tagPlaceable.width + + (if (overflowPlaceableCandidate != null) spacingPx + overflowPlaceableCandidate.width else 0) + + // If the candidate width fits into the available width, accept this tag. + if (candidateWidth <= availableWidth) { + measuredPlaceables.add(tagPlaceable) + consumedWidth += additionalSpacing + tagPlaceable.width + shownTagCount++ + } else { + // Otherwise, do not include this tag; break out of the loop. + break + } + } + + // Calculate the number of remaining tags. + val remainingCount = tags.size - shownTagCount + val overflowPlaceable = if (remainingCount > 0) { + subcompose("overflow_final") { + Box( + modifier = Modifier.background( + color = colorResource(R.color.shape_tertiary), + shape = RoundedCornerShape(4.dp) + ) + ) { + Text( + modifier = Modifier.padding(horizontal = 4.dp), + text = "+$remainingCount", + style = Relations2.copy( + color = colorResource(id = R.color.text_secondary) + ), + maxLines = 1, + ) + } + }.first().measure(constraints.copy(maxWidth = Constraints.Infinity)) + } else { + null + } + + // Final width is the sum of consumed width plus spacing and overflow chip (if present) + val totalWidth = if (overflowPlaceable != null) { + consumedWidth + spacingPx + overflowPlaceable.width + } else { + consumedWidth + } + val maxHeight = + (measuredPlaceables.map { it.height } + listOf(overflowPlaceable?.height ?: 0)) + .maxOrNull() ?: 0 + + layout(totalWidth, maxHeight) { + var xPosition = 0 + measuredPlaceables.forEach { placeable -> + placeable.placeRelative(xPosition, 0) + xPosition += placeable.width + spacingPx + } + val offsetYPx = with(density) { 1.dp.roundToPx() } + overflowPlaceable?.placeRelative(xPosition, offsetYPx) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..b829ea54c8 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeObjects.kt @@ -0,0 +1,293 @@ +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 +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.views.BodyCallout +import com.anytypeio.anytype.core_ui.views.Relations1 +import com.anytypeio.anytype.core_ui.views.Relations2 +import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon +import com.anytypeio.anytype.presentation.relations.ObjectRelationView +import com.anytypeio.anytype.presentation.sets.model.ObjectView + +/** + * The main composable for FieldObject. + * + * The first item is displayed in a Box with its width constrained to half the screen width. + * + * If a second item exists: + * - If there are exactly two items, it is displayed normally. + * - If there are more than two items, the second item is displayed with a suffix "+n" + * (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, + 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, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + if (fieldObject.objects.size == 1) { + // If there is only one item, display the title and the item in one row. + val singleItem = fieldObject.objects.first() + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = fieldObject.name, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Box( + modifier = Modifier.widthIn(max = halfScreenWidth) + ) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = singleItem + ) + } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) + } + } else { + Column( + modifier = defaultModifier + ) { + Text( + modifier = Modifier.wrapContentWidth(), + text = fieldObject.name, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(10.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + ) { + // The first item (if present) + if (fieldObject.objects.isNotEmpty()) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + ) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = fieldObject.objects.first() + ) + } + } + // The second item (if present) + if (fieldObject.objects.size > 1) { + Spacer(modifier = Modifier.width(8.dp)) + Box(modifier = Modifier.widthIn(max = halfScreenWidth)) { + if (fieldObject.objects.size == 2) { + ItemView( + modifier = Modifier.wrapContentHeight(), + objView = fieldObject.objects[1] + ) + } else { + // If there are more than two items, display the second item with a "+n" suffix. + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + ListWidgetObjectIcon( + icon = fieldObject.objects[1].icon, + iconSize = 18.dp, + modifier = Modifier, + onTaskIconClicked = { + // Do nothing + } + ) + Spacer(modifier = Modifier.width(4.dp)) + // The main text with an integrated suffix that occupies the remaining space. + TextWithSuffix( + text = fieldObject.objects[1].name, + suffix = "+${fieldObject.objects.size - 2}", + textStyle = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + countStyle = Relations2.copy( + color = colorResource(id = R.color.text_secondary) + ), + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + .padding(vertical = 2.dp) + ) + } + } + } + } + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) + } + } + } +} + +// Helper function to display a single item: icon (if available) + text. +@Composable +internal fun ItemView(modifier: Modifier, objView: ObjectView) { + Row( + modifier = modifier.padding(vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ListWidgetObjectIcon( + icon = objView.icon, + iconSize = 18.dp, + modifier = Modifier, + onTaskIconClicked = { + // Do nothing + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = objView.name, + style = BodyCallout.copy( + color = colorResource(id = R.color.text_primary) + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +/** + * A composable that displays a row consisting of the main text and a suffix. + * If the main text is short enough, the suffix (for example, "+n") + * will appear immediately after it; if the text is long, it will be truncated (with an ellipsis) + * to leave space for the suffix. + */ +@Composable +fun TextWithSuffix( + text: String, + suffix: String, + textStyle: TextStyle, + countStyle: TextStyle, + modifier: Modifier = Modifier +) { + val density = LocalDensity.current + + SubcomposeLayout(modifier = modifier) { constraints -> + val suffixConstraints = constraints.copy(minWidth = 0, maxWidth = Constraints.Infinity) + val suffixPlaceable = subcompose("suffix") { + Box( + modifier = Modifier.background( + color = colorResource(R.color.shape_tertiary), + shape = RoundedCornerShape(4.dp) + ) + ) { + Text( + modifier = Modifier.padding(horizontal = 4.dp), + text = suffix, + style = countStyle, + maxLines = 1, + ) + } + }.first().measure(suffixConstraints) + + // The available space for the main text is the total width minus the width of the suffix. + val availableWidthForText = (constraints.maxWidth - suffixPlaceable.width).coerceAtLeast(0) + val textConstraints = constraints.copy(minWidth = 0, maxWidth = availableWidthForText) + val textPlaceable = subcompose("text") { + Text( + text = text, + style = textStyle, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }.first().measure(textConstraints) + + // If width constraints are specified (e.g., when using weight), use the full available width. + val finalWidth = + if (constraints.hasBoundedWidth) constraints.maxWidth else (textPlaceable.width + suffixPlaceable.width) + val height = maxOf(textPlaceable.height, suffixPlaceable.height) + layout(finalWidth, height) { + // Align content to the left. + textPlaceable.placeRelative(0, 0) + val offsetYPx = with(density) { 0.5.dp.roundToPx() } + val offsetXPx = with(density) { 8.dp.roundToPx() } + suffixPlaceable.placeRelative(textPlaceable.width + offsetXPx, offsetYPx) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..6ac0f9d94f --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeSelect.kt @@ -0,0 +1,123 @@ +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 +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +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 +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_models.ThemeColor +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +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, + 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, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val halfScreenWidth = screenWidth / 2 - 32.dp + + Row( + modifier = defaultModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .widthIn(max = halfScreenWidth) + .wrapContentHeight() + .padding(vertical = 2.dp) + ) { + Text( + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = status.status, + style = Relations1, + color = dark(status.color), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) + } +} + +@DefaultPreviews +@Composable +fun FieldTypeSelectPreview() { + FieldTypeSelect( + title = "Status", + status = StatusView( + 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 new file mode 100644 index 0000000000..50bc4fce2d --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldTypeText.kt @@ -0,0 +1,105 @@ +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 +import androidx.compose.foundation.layout.height +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 +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.Relations1 + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FieldTypeText( + modifier: Modifier = Modifier, + title: 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, + color = colorResource(id = R.color.shape_secondary), + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 16.dp) + .padding(horizontal = 16.dp) + + Column( + modifier = defaultModifier + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + text = title, + style = Relations1, + color = colorResource(id = R.color.text_secondary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = text, + style = Relations1, + color = colorResource(id = R.color.text_primary), + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + FieldItemDropDownMenu( + showMenu = isMenuExpanded.value, + onDismissRequest = { + isMenuExpanded.value = false + }, + onAddToCurrentTypeClick = { + isMenuExpanded.value = false + onAddToCurrentTypeClick() + }, + onRemoveFromObjectClick = { + isMenuExpanded.value = false + onRemoveFromObjectClick() + } + ) + } +} + +@DefaultPreviews +@Composable +fun FieldTypeTextPreview() { + FieldTypeText( + 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.", + 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 new file mode 100644 index 0000000000..6d7d305d5c --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/FieldsListScreen.kt @@ -0,0 +1,375 @@ +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 +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, + 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), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + Dragger( + modifier = Modifier.padding(vertical = 6.dp) + ) + } + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource(id = R.string.fields_screen_title), + 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( + count = state.size, + key = { index -> state[index].identifier }, + itemContent = { index -> + val item = state[index] + when (item) { + is Model.Item -> { + val field = item.view + when (field) { + is ObjectRelationView.Checkbox -> { + FieldTypeCheckbox( + modifier = Modifier, + title = field.name, + isCheck = field.isChecked, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } + + is ObjectRelationView.Date -> { + val relativeDate = field.relativeDate + if (relativeDate != null) { + FieldTypeDate( + modifier = Modifier, + title = field.name, + relativeDate = relativeDate, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } else { + FieldEmpty( + modifier = Modifier, + title = field.name, + fieldFormat = RelationFormat.DATE, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } + } + + is ObjectRelationView.Default -> { + val textValue = field.value + if (field.key == Relations.ORIGIN) { + val code = textValue?.toInt() ?: -1 + FieldTypeText( + modifier = Modifier, + title = field.name, + text = stringResource(code.resRelationOrigin()), + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } else { + if (textValue.isNullOrEmpty() == true) { + FieldEmpty( + modifier = Modifier, + title = field.name, + fieldFormat = RelationFormat.LONG_TEXT, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } else { + FieldTypeText( + modifier = Modifier, + title = field.name, + text = textValue, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } + } + } + + is ObjectRelationView.File -> { + if (field.files.isEmpty()) { + FieldEmpty( + modifier = Modifier, + title = field.name, + fieldFormat = RelationFormat.FILE, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } else { + FieldTypeFile( + modifier = Modifier, + fieldObject = field, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } + } + + is ObjectRelationView.Object -> { + if (field.objects.isEmpty()) { + FieldEmpty( + modifier = Modifier, + title = field.name, + fieldFormat = RelationFormat.OBJECT, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } else { + FieldTypeObject( + modifier = Modifier, + fieldObject = field, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } + } + + is ObjectRelationView.Status -> { + if (field.status.isEmpty()) { + FieldEmpty( + modifier = Modifier, + title = field.name, + fieldFormat = RelationFormat.STATUS, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } else { + FieldTypeSelect( + modifier = Modifier, + title = field.name, + status = field.status.first(), + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } + } + + is ObjectRelationView.Tags -> { + if (field.tags.isEmpty()) { + FieldEmpty( + modifier = Modifier, + title = field.name, + fieldFormat = RelationFormat.TAG, + isLocal = item.isLocal, + onFieldClick = { onRelationClicked(item) }, + onAddToCurrentTypeClick = { onAddToTypeClicked(item) }, + onRemoveFromObjectClick = { onRemoveFromObjectClicked(item) } + ) + } else { + FieldTypeMultiSelect( + modifier = Modifier, + title = field.name, + 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, + is ObjectRelationView.ObjectType.Deleted, + is ObjectRelationView.Source -> { + Timber.e("Unsupported field type: $field, shouldn't be in the fields list") + } + } + } + + is Model.Section.Header -> { + Section(item) + } + is Model.Section.SideBar -> { + Section(item) + } + Model.Section.Local -> { + SectionLocal(onLocalInfoIconClicked) + } + } + } + ) + item { + Spacer(modifier = Modifier.height(64.dp)) + } + } +} + +@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 new file mode 100644 index 0000000000..1b9e79a6e6 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/fields/MultiSelectPreviews.kt @@ -0,0 +1,256 @@ +package com.anytypeio.anytype.core_ui.features.fields + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.anytypeio.anytype.core_models.ThemeColor +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.presentation.relations.value.tagstatus.RelationsListItem +import com.anytypeio.anytype.presentation.sets.model.TagView + +// -------------------- +// Test Case 1: Multiple Tags (Your First Test Case) +// -------------------- +@DefaultPreviews +@Composable +fun TagsPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "Personal", + color = ThemeColor.ORANGE.code, + ), + TagView( + id = "3", + tag = "Done", + color = ThemeColor.LIME.code, + ), + TagView( + id = "4", + tag = "In Progress", + color = ThemeColor.BLUE.code, + ), + TagView( + id = "5", + tag = "Waiting", + color = ThemeColor.YELLOW.code, + ), + TagView( + id = "6", + tag = "Blocked", + color = ThemeColor.PURPLE.code, + ), + TagView( + id = "7", + tag = "Spam", + color = ThemeColor.PINK.code, + ) + ), + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} + ) + } + } +} + +// -------------------- +// Test Case 2: Single Tag with a Very Long Name (truncated) +// -------------------- +@Preview(showBackground = true) +@Composable +fun SingleLongTagPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "This is an extremely long tag that should be truncated if it doesn't fit in the available space", + color = ThemeColor.RED.code, + ), + ), + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} + ) + } + } +} + +// -------------------- +// Test Case 3: Single Tag with a Short Name +// -------------------- +@Preview(showBackground = true) +@Composable +fun SingleShortTagPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + ), + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} + ) + } + } +} + +// -------------------- +// Test Case 4: Two Tags – First Tag Short, Second Tag Very Long (second omitted → overflow) +// -------------------- +@Preview(showBackground = true) +@Composable +fun TwoTagsFirstShortSecondLongPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "This is a very long tag that might not fit entirely", + color = ThemeColor.ORANGE.code, + ) + ), + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} + ) + } + } +} + +// -------------------- +// Test Case 5: Two Short Tags (both displayed) +// -------------------- +@Preview(showBackground = true) +@Composable +fun TwoShortTagsPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "Personal", + color = ThemeColor.ORANGE.code, + ) + ), + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} + ) + } + } +} + +// -------------------- +// Test Case 6: Three Short Tags (all displayed) +// -------------------- +@Preview(showBackground = true) +@Composable +fun ThreeShortTagsPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "Personal", + color = ThemeColor.ORANGE.code, + ), + TagView( + id = "3", + tag = "Done", + color = ThemeColor.LIME.code, + ), + ), + title = "Tag", + isLocal = true, + onRemoveFromObjectClick = {}, + onAddToCurrentTypeClick = {}, + onFieldClick = {} + ) + } + } +} + +// -------------------- +// Test Case 7: Four Tags with Overflow (only some tags displayed, remainder shown as +n) +// -------------------- +@Preview(showBackground = true) +@Composable +fun FourTagsWithOverflowPreview() { + LazyColumn { + item { + FieldTypeMultiSelect( + tags = listOf( + TagView( + id = "1", + tag = "Urgent", + color = ThemeColor.RED.code, + ), + TagView( + id = "2", + tag = "Personal", + color = ThemeColor.ORANGE.code, + ), + TagView( + id = "3", + tag = "Done", + color = ThemeColor.LIME.code, + ), + TagView( + id = "4", + tag = "In Progress", + color = ThemeColor.BLUE.code, + ) + ), + title = "Tag", + 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/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..69dda8e150 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/SearchBar.kt @@ -0,0 +1,163 @@ +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, + hint: Int = R.string.search, + 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 = 10.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 = hint), + style = BodyRegular.copy( + color = colorResource(id = R.color.glyph_active) + ) + ) + }, + 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/foundation/System.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/System.kt index 434526c409..52781ab3e1 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/System.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/System.kt @@ -20,11 +20,12 @@ import com.anytypeio.anytype.core_ui.views.Title1 @Composable fun Section( + modifier: Modifier = Modifier, title: String, color: Color = colorResource(id = R.color.text_secondary), textPaddingStart: Dp = 20.dp ) { - Box(modifier = Modifier + Box(modifier = modifier .height(52.dp) .fillMaxWidth()) { Text( diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/components/BottomNavigationMenu.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/components/BottomNavigationMenu.kt index f816d3dd7a..59b6859d26 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/components/BottomNavigationMenu.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/components/BottomNavigationMenu.kt @@ -174,18 +174,25 @@ fun BottomNavigationMenu( verticalAlignment = Alignment.CenterVertically ) { if (state is NavPanelState.Default) { - when (state.leftButtonState) { + when (val left = state.leftButtonState) { is NavPanelState.LeftButtonState.AddMembers -> { MenuItem( modifier = Modifier .width(72.dp) - .height(52.dp), + .height(52.dp) + .alpha( + if (left.isActive) + FULL_ALPHA + else + DEFAULT_DISABLED_ALPHA + ) + , contentDescription = stringResource(id = R.string.main_navigation_content_desc_members_button), res = BottomNavigationItem.ADD_MEMBERS.res, - onClick = onShareButtonClicked + onClick = onShareButtonClicked, + enabled = left.isActive ) } - is NavPanelState.LeftButtonState.Comment -> { // TODO } 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/ObjectIconWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconWidget.kt index 482afdbf79..e575ab68d7 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconWidget.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconWidget.kt @@ -392,4 +392,4 @@ class ObjectIconWidget @JvmOverloads constructor( this.width = emojiSize } } -} \ No newline at end of file +} 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/java/com/anytypeio/anytype/core_ui/widgets/dv/ViewerEditWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/dv/ViewerEditWidget.kt index 53386a982a..1ef007bfcf 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/dv/ViewerEditWidget.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/dv/ViewerEditWidget.kt @@ -1,5 +1,6 @@ package com.anytypeio.anytype.core_ui.widgets.dv +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth @@ -41,6 +43,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController @@ -50,7 +53,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction 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.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension @@ -86,7 +88,8 @@ fun ViewerEditWidget( ModalBottomSheet( modifier = Modifier .windowInsetsPadding(WindowInsets.ime) - .padding(start = 8.dp, end = 8.dp, bottom = 30.dp) + .padding(start = 8.dp, end = 8.dp, bottom = 8.dp) + .systemBarsPadding() .fillMaxWidth() .wrapContentHeight(), scrimColor = colorResource(id = R.color.modal_screen_outside_background), diff --git a/core-ui/src/main/res/drawable/ic_add_member_32.xml b/core-ui/src/main/res/drawable/ic_add_member_32.xml new file mode 100644 index 0000000000..ca352df216 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_add_member_32.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/core-ui/src/main/res/drawable/ic_disclosure_8_24.xml b/core-ui/src/main/res/drawable/ic_disclosure_8_24.xml new file mode 100644 index 0000000000..b5636b7cf4 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_disclosure_8_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_members_24.xml b/core-ui/src/main/res/drawable/ic_members_24.xml new file mode 100644 index 0000000000..50048064d2 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_members_24.xml @@ -0,0 +1,19 @@ + + + + + + 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_object_types_24.xml b/core-ui/src/main/res/drawable/ic_object_types_24.xml new file mode 100644 index 0000000000..3bbcab48fd --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_object_types_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_qr_code_32.xml b/core-ui/src/main/res/drawable/ic_qr_code_32.xml new file mode 100644 index 0000000000..901cdfb20d --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_qr_code_32.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_remote_storage_24.xml b/core-ui/src/main/res/drawable/ic_remote_storage_24.xml new file mode 100644 index 0000000000..99daee6ceb --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_remote_storage_24.xml @@ -0,0 +1,9 @@ + + + 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/item_block_title.xml b/core-ui/src/main/res/layout/item_block_title.xml index a4335ced61..8e97a2f12e 100644 --- a/core-ui/src/main/res/layout/item_block_title.xml +++ b/core-ui/src/main/res/layout/item_block_title.xml @@ -52,21 +52,24 @@ + tools:visibility="visible" /> + + + + + + + + + + + + + \ 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/core-ui/src/main/res/values/dimens.xml b/core-ui/src/main/res/values/dimens.xml index f78c571f44..8fc5d10d42 100644 --- a/core-ui/src/main/res/values/dimens.xml +++ b/core-ui/src/main/res/values/dimens.xml @@ -44,6 +44,7 @@ 102dp 120dp 203dp + 443dp 4dp 1dp diff --git a/core-ui/src/main/res/values/styles.xml b/core-ui/src/main/res/values/styles.xml index 8aa284b3a6..131c596dba 100644 --- a/core-ui/src/main/res/values/styles.xml +++ b/core-ui/src/main/res/values/styles.xml @@ -446,7 +446,6 @@