1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-07 21:37:02 +09:00

Merge branch 'main' into droid-3330-support-push-notifications

This commit is contained in:
Evgenii Kozlov 2025-03-10 16:59:07 +01:00 committed by GitHub
commit b234798579
Signed by: github
GPG key ID: B5690EEEBB952194
329 changed files with 19341 additions and 3639 deletions

View file

@ -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

View file

@ -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
update_mw_custom: download_mw_artefacts_custom
prepare_app_manifest_for_release_apk:
./scripts/release/apk.sh

View file

@ -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 {

View file

@ -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

View file

@ -1,4 +1,4 @@
version.versionMajor=0
version.versionMinor=36
version.versionMinor=37
version.versionPatch=0
version.useDatedVersionName=false

View file

@ -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,
)

View file

@ -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,
)

View file

@ -11,6 +11,9 @@
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />-->
<!-- <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />-->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
@ -50,6 +53,19 @@
<data android:scheme="http"/>
<data android:host="invite.any.coop" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="download.anytype.io/" />
</intent-filter>
<intent-filter android:label="Link to object" android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="object.any.coop" android:pathPattern=".*" />
<data android:scheme="http" android:host="object.any.coop" android:pathPattern=".*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />

View file

@ -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(

View file

@ -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<CoverImage> {
val json = context.getJsonDataFromAsset(COVER_FILE)
return if (json != null) {
gson.fromJson(json, Array<CoverImage>::class.java).toList()
} else {
emptyList()
}
}
companion object {
const val COVER_FILE = "covers.json"
}
}

View file

@ -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<PickVisualMediaRequest>,
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<PickVisualMediaRequest>,
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)
}
}
}

View file

@ -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<T>(private val builder: () -> T) {
private var instance: T? = null

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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(

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
)
)
}
}

View file

@ -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"

View file

@ -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 {

View file

@ -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")

View file

@ -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

View file

@ -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")
}
}
}
}
}

View file

@ -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<FragmentEditorBinding>(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<FragmentEditorBinding>(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,
)
)
}

View file

@ -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)

View file

@ -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<FragmentObjectLayoutBinding>() {
private val ctx: String get() = argString(CONTEXT_ID_KEY)

View file

@ -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<T> :
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()

View file

@ -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
)
)
}

View file

@ -91,11 +91,11 @@ fun HomeScreen(
onHomeButtonClicked: () -> Unit,
onCreateNewObjectClicked: () -> Unit,
onCreateNewObjectLongClicked: () -> Unit,
onShareButtonClicked: () -> Unit,
onNavBarShareButtonClicked: () -> Unit,
onObjectCheckboxClicked: (Id, Boolean) -> Unit,
onSpaceWidgetClicked: () -> Unit,
onMove: (List<WidgetView>, 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<WidgetView>, 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
)

View file

@ -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,
)

View file

@ -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(

View file

@ -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)

View file

@ -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)

View file

@ -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 = {}
)
}

View file

@ -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<EditTypePropertiesViewModel> { 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
}
}

View file

@ -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<RelationListViewModel> { 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<OnFragmentInteractionListener> {
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
}
}

View file

@ -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<ObjectTypeViewModel> { 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
)
}
}

View file

@ -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<ObjectTypeViewModel> { 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
)
}
}

View file

@ -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<ObjectSetFragment>(
modifier = Modifier
.fillMaxSize(),
arguments = ObjectSetFragment.args(
ctx = objectId,
space = space
)
) { fragment ->
fragment.view?.findViewById<View>(R.id.objectHeader)?.visibility =
View.GONE
}
}
}

View file

@ -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<FragmentRelationListBinding>(),
RelationTextValueFragment.TextValueEditReceiver,
RelationDateValueFragment.DateValueEditReceiver {
@ -194,6 +195,10 @@ open class ObjectRelationListFragment : BaseBottomSheetFragment<FragmentRelation
Timber.e(it, "Error while opening date object from relation list")
}
}
is Command.NavigateToObjectType -> {
//do nothing
}
}
}

View file

@ -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<View>(R.id.imageIcon).apply {
if (header.title.image != null) visible() else gone()
binding.objectHeader.root.findViewById<ImageView>(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<ImageView>(R.id.emojiIcon)
.setEmojiOrNull(header.title.emoji)
if (header.title.image != null) {
binding.objectHeader.root.findViewById<ImageView>(R.id.imageIcon).apply {
Glide
.with(this)
.load(header.title.image)
.centerCrop()
.into(this)
}
} else {
binding.objectHeader.root.findViewById<ImageView>(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<String>(ARG_TEMPLATE_ID)
val resultTypeId = navBackStackEntry.savedStateHandle.get<String>(ARG_TARGET_TYPE_ID)
val resultTypeKey = navBackStackEntry.savedStateHandle.get<String>(ARG_TARGET_TYPE_KEY)
if (!resultTemplateId.isNullOrBlank() && !resultTypeId.isNullOrBlank() && !resultTypeKey.isNullOrBlank()) {
navBackStackEntry.savedStateHandle.remove<String>(ARG_TEMPLATE_ID)
navBackStackEntry.savedStateHandle.remove<String>(ARG_TARGET_TYPE_ID)
navBackStackEntry.savedStateHandle.remove<String>(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<String>(ARG_TEMPLATE_ID)
val resultTypeId =
navBackStackEntry.savedStateHandle.get<String>(ARG_TARGET_TYPE_ID)
val resultTypeKey =
navBackStackEntry.savedStateHandle.get<String>(ARG_TARGET_TYPE_KEY)
if (!resultTemplateId.isNullOrBlank() && !resultTypeId.isNullOrBlank() && !resultTypeKey.isNullOrBlank()) {
navBackStackEntry.savedStateHandle.remove<String>(ARG_TEMPLATE_ID)
navBackStackEntry.savedStateHandle.remove<String>(ARG_TARGET_TYPE_ID)
navBackStackEntry.savedStateHandle.remove<String>(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() {

View file

@ -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<DebugViewModel> { 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()
}
}

View file

@ -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 = {}
)
}

View file

@ -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() {

View file

@ -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")
}

View file

@ -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<FragmentSplashBinding>(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<FragmentSplashBinding>(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)) {

View file

@ -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<MigrationErrorViewModel> { 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()
}
}

View file

@ -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 = {}
)
}

View file

@ -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(

View file

@ -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

View file

@ -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,

View file

@ -1,209 +1,184 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools">
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/dragger"
android:layout_width="48dp"
android:layout_height="4dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="6dp"
android:background="@drawable/dragger"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:background="@drawable/dragger" />
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_constraintBottom_toTopOf="@id/rvContainer"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/lvOptions"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/optionIcon"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@drawable/default_ripple"
app:icon="@drawable/ic_object_menu_icon"
app:icon="@drawable/ic_obj_settings_icon_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dragger"
app:subtitle="@string/icon_description"
app:title="@string/icon" />
<View
android:id="@+id/iconDivider"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="@color/shape_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/optionIcon" />
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/optionCover"
android:layout_width="0dp"
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/optionCover"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
app:icon="@drawable/ic_obj_settings_cover_24"
app:subtitle="@string/cover_description"
app:title="@string/cover" />
<View
android:id="@+id/coverDivider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="@color/shape_primary" />
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuDescriptionItem
android:id="@+id/optionDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
app:icon="@drawable/ic_object_menu_cover"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iconDivider"
app:subtitle="@string/cover_description"
app:title="@string/cover" />
<View
android:id="@+id/coverDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:background="@color/shape_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/optionCover" />
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/optionLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
app:icon="@drawable/ic_object_menu_layout"
app:icon="@drawable/ic_obj_settings_description_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/coverDivider"
app:subtitle="@string/layout_description"
app:title="@string/layout" />
app:title="@string/description" />
<View
android:id="@+id/layoutDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:background="@color/shape_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/optionLayout" />
<View
android:id="@+id/descriptionDivider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="@color/shape_primary" />
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/optionRelations"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
app:icon="@drawable/ic_object_menu_relations"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layoutDivider"
app:subtitle="@string/relations_description"
app:title="@string/relations" />
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/optionRelations"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
app:icon="@drawable/ic_obj_settings_fields_24"
app:title="@string/fields" />
<View
android:id="@+id/relationsDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:background="@color/shape_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/optionRelations" />
<View
android:id="@+id/relationsDivider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="@color/shape_primary" />
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/optionHistory"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
app:icon="@drawable/ic_object_menu_history"
app:icon="@drawable/ic_obj_settings_history_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/relationsDivider"
app:subtitle="@string/history_description"
app:title="@string/history" />
<View
android:id="@+id/historyDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
android:layout_marginTop="8dp"
android:background="@color/shape_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/optionHistory" />
<View
android:id="@+id/historyDivider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="@color/shape_primary" />
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/objectDiagnostics"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
app:icon="@drawable/ic_object_menu_diagnostics"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/historyDivider"
app:subtitle="@string/object_debug"
app:title="@string/object_diagnostics" />
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/objectDiagnostics"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
app:icon="@drawable/ic_object_menu_diagnostics"
app:title="@string/object_diagnostics" />
<View
android:id="@+id/objectDiagnosticsDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
android:layout_marginTop="8dp"
android:background="@color/shape_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/objectDiagnostics" />
<View
android:id="@+id/objectDiagnosticsDivider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="@color/shape_primary" />
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/debugGoroutines"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
android:visibility="gone"
app:icon="@drawable/ic_object_menu_debug_goroutines"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/objectDiagnosticsDivider"
app:subtitle="Command Debug.StackGoroutines"
app:title="Debug Goroutines"
<com.anytypeio.anytype.core_ui.widgets.ObjectMenuItemWidget
android:id="@+id/debugGoroutines"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/default_ripple"
android:visibility="gone"
app:icon="@drawable/ic_object_menu_debug_goroutines"
app:title="Debug Goroutines"
tools:visibility="visible" />
<View
android:id="@+id/debugGoroutinesDivider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:layout_marginStart="72dp"
android:layout_marginTop="8dp"
android:visibility="gone"
android:background="@color/shape_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/debugGoroutines" />
<View
android:id="@+id/debugGoroutinesDivider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:background="@color/shape_primary"
android:visibility="gone" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<FrameLayout
android:id="@+id/rvContainer"
android:layout_width="match_parent"
android:layout_height="108dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/debugGoroutinesDivider">
android:layout_marginTop="12dp"
android:layout_weight="0">
<androidx.recyclerview.widget.RecyclerView
android:layout_gravity="center_vertical"
android:id="@+id/rvActions"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="match_parent"
android:layout_gravity="center_vertical" />
</FrameLayout>
@ -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"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
app:layout_constraintEnd_toEndOf="parent" />
</LinearLayout>

View file

@ -5,13 +5,6 @@
android:layout_height="match_parent"
android:background="@color/background_primary">
<ImageView
android:id="@+id/logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_logo" />
<TextView
android:id="@+id/version"
style="@style/TextView.UXStyle.Body"
@ -41,10 +34,9 @@
tools:visibility="visible" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/loadingContainer"
android:id="@+id/compose"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:gravity="center"/>
android:visibility="gone" />
</FrameLayout>

View file

@ -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" />

View file

@ -34,7 +34,7 @@
android:label="Object-Menu-Screen" />
<dialog
android:id="@+id/objectRelationListScreen"
android:name="com.anytypeio.anytype.ui.relations.ObjectRelationListFragment"
android:name="com.anytypeio.anytype.ui.primitives.ObjectFieldsFragment"
android:label="Object-Relation-List-Screen" />
<dialog
android:id="@+id/objectIconPickerScreen"
@ -81,6 +81,11 @@
android:id="@+id/actionExitToSpaceWidgets"
app:popUpTo="@+id/homeScreen"
app:popUpToInclusive="false" />
<dialog android:id="@+id/objectTypeFieldsScreen"
android:name="com.anytypeio.anytype.ui.primitives.ObjectTypeFieldsFragment"
android:label="ObjectTypeFieldsScreen" />
</navigation>
<include app:graph="@navigation/nav_editor_modal" />
@ -109,9 +114,10 @@
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<dialog
android:id="@+id/objectRelationListScreen"
android:name="com.anytypeio.anytype.ui.relations.ObjectRelationListFragment"
android:name="com.anytypeio.anytype.ui.primitives.ObjectFieldsFragment"
android:label="Object-Relation-List-Screen" />
<dialog
android:id="@+id/objectSetMainMenuScreen"
@ -227,13 +233,26 @@
<fragment
android:id="@+id/dateObjectScreen"
android:name="com.anytypeio.anytype.ui.date.DateObjectFragment"
android:label="Date Object"> {
android:label="Date Object">
<action
android:id="@+id/actionExitToSpaceWidgets"
app:popUpTo="@+id/homeScreen"
app:popUpToInclusive="false" />
</fragment>
<fragment
android:id="@+id/objectTypeScreen"
android:name="com.anytypeio.anytype.ui.primitives.ObjectTypeFragment"
android:label=" Object">
<action
android:id="@+id/openEmojiPicker"
app:destination="@id/typeSetIconPickerScreen" />
</fragment>
<dialog
android:id="@+id/editTypePropertiesScreen"
android:name="com.anytypeio.anytype.ui.primitives.EditTypePropertiesFragment"/>
<dialog
android:id="@+id/selectWidgetSourceScreen"
android:name="com.anytypeio.anytype.ui.widgets.SelectWidgetSourceFragment" />
@ -272,9 +291,6 @@
<action
android:id="@+id/actionCreateSpaceFromVault"
app:destination="@id/createSpaceScreen" />
<action
android:id="@+id/actionShowIntroduceVaultScreen"
app:destination="@id/introduceVaultScreen" />
</fragment>
<dialog
@ -453,6 +469,12 @@
android:name="com.anytypeio.anytype.ui.settings.DebugSettingsFragment"
android:label="DebugSettingsFragment"
tools:layout="@layout/fragment_debug_settings" />
<dialog
android:id="@+id/debugScreen"
android:name="com.anytypeio.anytype.ui.settings.DebugFragment"
android:label="DebugScreen" />
<dialog
android:id="@+id/globalSearchScreen"
android:name="com.anytypeio.anytype.ui.search.GlobalSearchFragment"
@ -634,12 +656,6 @@
android:label="TemplateSelectScreen"
tools:layout="@layout/fragment_template_select" />
<fragment
android:id="@+id/migrationNeededScreen"
android:name="com.anytypeio.anytype.ui.update.MigrationErrorFragment"
android:label="Migration-needed screen">
</fragment>
<dialog
android:id="@+id/shareSpaceScreen"
android:name="com.anytypeio.anytype.ui.multiplayer.ShareSpaceFragment"/>

View file

@ -32,7 +32,7 @@
</fragment>
<dialog
android:id="@+id/objectRelationListScreen"
android:name="com.anytypeio.anytype.ui.relations.ObjectRelationListFragment"
android:name="com.anytypeio.anytype.ui.primitives.ObjectFieldsFragment"
android:label="Object-Relation-List-Screen" />
<dialog
android:id="@+id/objectIconPickerScreen"

View file

@ -32,7 +32,7 @@
</fragment>
<dialog
android:id="@+id/objectRelationListScreen"
android:name="com.anytypeio.anytype.ui.relations.ObjectRelationListFragment"
android:name="com.anytypeio.anytype.ui.primitives.ObjectFieldsFragment"
android:label="Object-Relation-List-Screen" />
<dialog
android:id="@+id/objectIconPickerScreen"

View file

@ -1,10 +1,14 @@
package com.anytypeio.anytype.other
import android.os.Build
import androidx.compose.runtime.key
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.misc.DeepLinkResolver
import com.anytypeio.anytype.test_utils.MockDataFactory
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@ -26,6 +30,116 @@ class DefaultDeepLinkResolverTest {
assertEquals(DeepLinkResolver.Action.Import.Experience(type = "experience123", source = "source321"), result)
}
@Test
fun `resolve link to object deep link`() {
// Given
val obj = MockDataFactory.randomUuid()
val space = MockDataFactory.randomUuid()
val deeplink = "anytype://object?objectId=$obj&spaceId=$space"
// When
val result = deepLinkResolver.resolve(deeplink)
// Then
assertEquals(
DeepLinkResolver.Action.DeepLinkToObject(
space = SpaceId(space),
obj = obj
),
result
)
}
@Test
fun `resolve link to object deep link with invite`() {
// Given
val obj = MockDataFactory.randomUuid()
val space = MockDataFactory.randomUuid()
val cid = MockDataFactory.randomUuid()
val encryption = MockDataFactory.randomUuid()
val deeplink = "anytype://object?objectId=$obj&spaceId=$space&inviteID=$cid#$encryption"
// When
val result = deepLinkResolver.resolve(deeplink)
// Then
assertEquals(
DeepLinkResolver.Action.DeepLinkToObject(
space = SpaceId(space),
obj = obj,
invite = DeepLinkResolver.Action.DeepLinkToObject.Invite(
cid = cid,
key = encryption
)
),
result
)
}
@Test
fun `resolve https deep link to object`() {
// Given
val obj = MockDataFactory.randomUuid()
val space = MockDataFactory.randomUuid()
val deeplink = "https://object.any.coop/$obj?spaceId=$space"
// When
val result = deepLinkResolver.resolve(deeplink)
// Then
assertEquals(
DeepLinkResolver.Action.DeepLinkToObject(
space = SpaceId(space),
obj = obj
),
result
)
}
@Test
fun `resolve https deep link to object with invite`() {
// Given
val obj = MockDataFactory.randomUuid()
val space = MockDataFactory.randomUuid()
val cid = MockDataFactory.randomUuid()
val encryption = MockDataFactory.randomUuid()
val invite = "$cid#$encryption"
val deeplink = "https://object.any.coop/$obj?spaceId=$space&inviteID=$invite"
// When
val result = deepLinkResolver.resolve(deeplink)
// Then
assertEquals(
DeepLinkResolver.Action.DeepLinkToObject(
space = SpaceId(space),
obj = obj,
invite = DeepLinkResolver.Action.DeepLinkToObject.Invite(
cid = cid,
key = encryption
)
),
result
)
}
@Test
fun `resolve returns Invite with deeplink for invite deep links`() {
// Given

View file

@ -653,4 +653,18 @@ sealed class Command {
val dataViewId: Id,
val viewerId: Id
)
data class ObjectTypeConflictingFields(
val spaceId: String,
val objectTypeId: String
) : Command()
data class ObjectTypeSetRecommendedHeaderFields(
val objectTypeId: String,
val fields: List<Id>
) : Command()
data class ObjectTypeSetRecommendedFields(
val objectTypeId: String,
val fields: List<Id>
) : Command()
}

View file

@ -91,10 +91,10 @@ sealed class ObjectWrapper {
val restrictions: List<ObjectRestriction>
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<Double>().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<Id> get() = getValues(Relations.RECOMMENDED_RELATIONS)
val recommendedFeaturedRelations: List<Id> get() = getValues(Relations.RECOMMENDED_FEATURED_RELATIONS)
val recommendedHiddenRelations: List<Id> get() = getValues(Relations.RECOMMENDED_HIDDEN_RELATIONS)
val recommendedFileRelations: List<Id> 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<ObjectRestriction>
get() = when (val value = map[Relations.RESTRICTIONS]) {
is Double -> buildList {
ObjectRestriction.entries.firstOrNull { it.code == value.toInt() }
}
is List<*> -> value.typeOf<Double>().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<ObjectRestriction>
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<Double>().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<Id> 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

View file

@ -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"
)
}

View file

@ -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
)
}

View file

@ -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,

View file

@ -67,6 +67,7 @@ sealed class Chat {
id: Id,
text: String,
attachments: List<Attachment> = emptyList(),
marks: List<Block.Content.Text.Mark>
) : 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 = ""

View file

@ -1,5 +1,9 @@
package com.anytypeio.anytype.core_models.exceptions
class AccountIsDeletedException : Exception()
class MigrationNeededException: Exception()
class NeedToUpdateApplicationException: Exception()
class NeedToUpdateApplicationException: Exception()
class AccountMigrationNeededException: Exception()
sealed class MigrationFailedException : Exception() {
class NotEnoughSpace : MigrationFailedException()
}

View file

@ -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<Boolean>(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,
)

View file

@ -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<T> {
@ -20,4 +21,13 @@ sealed class Field<T>(open val value: Value<T>) {
data class FieldDateValue(
val timestamp: TimestampInSeconds,
val relativeDate: RelativeDate
)
data class ParsedFields(
val header: List<ObjectWrapper.Relation> = emptyList(),
val sidebar: List<ObjectWrapper.Relation> = emptyList(),
val hidden: List<ObjectWrapper.Relation> = emptyList(),
val localWithoutSystem: List<ObjectWrapper.Relation> = emptyList(),
val localSystem: List<ObjectWrapper.Relation> = emptyList(),
val file: List<ObjectWrapper.Relation> = emptyList(),
)

View file

@ -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<Id> = 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

View file

@ -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')

View file

@ -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<Boolean> {
val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
return rememberUpdatedState(isImeVisible)
}
}
@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
)
}
}
)

View file

@ -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
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

View file

@ -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
}
}

View file

@ -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 <T> SnapshotStateList<T>.swapList(newList: List<T>){
fun <T> SnapshotStateList<T>.swapList(newList: List<T>) {
clear()
addAll(newList)
}

View file

@ -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

View file

@ -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) {

View file

@ -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())
}
}

View file

@ -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<ConstraintLayout.LayoutParams> {
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<ConstraintLayout.LayoutParams> {
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)
}

View file

@ -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()
},
)
}
}

View file

@ -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))
}
}
}

View file

@ -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 = {}
)
}

View file

@ -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 = {}
)
}

View file

@ -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)
}
}
}

View file

@ -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<TagView>,
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 chips 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 tags 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<TagView>,
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<Placeable>()
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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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 = {}
)
}

View file

@ -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, youll 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 = {}
)
}

View file

@ -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<Model>,
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 = {}
)
}

View file

@ -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 = {}
)
}
}
}

View file

@ -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<RelationListViewModel.Model>,
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<TextView>(R.id.tvSectionName)
.setText(R.string.featured_relations)
}
RelationListViewModel.Model.Section.Other -> {
itemView.findViewById<TextView>(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<TextView>(R.id.tvSectionName).text = text
}
// RelationListViewModel.Model.Section.Featured -> {
// itemView.findViewById<TextView>(R.id.tvSectionName)
// .setText(R.string.featured_relations)
// }
// RelationListViewModel.Model.Section.Other -> {
// itemView.findViewById<TextView>(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<TextView>(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
}
}

View file

@ -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()
)
}

View file

@ -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 {}
}

View file

@ -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(

View file

@ -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
}

View file

@ -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
)
)
}

Some files were not shown because too many files have changed in this diff Show more