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:
commit
b234798579
329 changed files with 19341 additions and 3639 deletions
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
@ -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
|
||||
|
||||
|
|
7
Makefile
7
Makefile
|
@ -8,7 +8,7 @@ enable_dated_version_name:
|
|||
./gradlew -q :app:enableDatedVersionName
|
||||
|
||||
distribute_debug:
|
||||
./gradlew bundleDebug appDistributionUploadDebug
|
||||
./gradlew assembleDebug appDistributionUploadDebug
|
||||
|
||||
pr_check: compile_android_test_sources test_debug_all
|
||||
|
||||
|
@ -47,4 +47,7 @@ clean_protos:
|
|||
update_mw: download_mw_artefacts normalize_mw_imports clean_protos
|
||||
|
||||
# Update mw from custom build (download only library, you have to update your proto files manually)
|
||||
update_mw_custom: download_mw_artefacts_custom
|
||||
update_mw_custom: download_mw_artefacts_custom
|
||||
|
||||
prepare_app_manifest_for_release_apk:
|
||||
./scripts/release/apk.sh
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
version.versionMajor=0
|
||||
version.versionMinor=36
|
||||
version.versionMinor=37
|
||||
version.versionPatch=0
|
||||
version.useDatedVersionName=false
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 chip’s background color.
|
||||
* @param textStyle The [TextStyle] used for the tag text.
|
||||
* @param isSingle If true, the chip is rendered in single-mode – meaning that if the chip does not fit
|
||||
* in the available width, its text will be truncated (TextOverflow.Ellipsis).
|
||||
* @param isOverflow If true, this chip is an overflow indicator (e.g. “+3”).
|
||||
* @param modifier Modifier to be applied to the chip.
|
||||
*/
|
||||
@Composable
|
||||
fun TagChip(
|
||||
text: String,
|
||||
tagColor: String,
|
||||
textStyle: TextStyle,
|
||||
isSingle: Boolean = false,
|
||||
isOverflow: Boolean = false,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// In single mode, we allow truncation.
|
||||
Box(
|
||||
modifier = modifier
|
||||
.wrapContentWidth()
|
||||
.background(light(tagColor), shape = RoundedCornerShape(6.dp))
|
||||
.padding(horizontal = 6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = textStyle,
|
||||
color = dark(tagColor),
|
||||
maxLines = 1,
|
||||
overflow = if (isSingle) TextOverflow.Ellipsis else TextOverflow.Clip
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that lays out a row of tags in a single horizontal line.
|
||||
*
|
||||
* The behavior is as follows:
|
||||
* 1. **Single tag case:** If there is only one tag, it is displayed in a row. If its intrinsic width
|
||||
* exceeds the available width, the text is truncated (TextOverflow.Ellipsis).
|
||||
*
|
||||
* 2. **Multiple tags case:** The layout tries to display as many tags as possible in full (i.e. without truncation).
|
||||
* - If a tag would be rendered with truncation, it is omitted and all remaining tags are replaced by
|
||||
* an overflow chip (e.g. “+n”).
|
||||
* - For example, if the first tag is short and fits but the second tag’s full width would exceed the available space,
|
||||
* then only the first tag is displayed and an overflow chip shows the remaining count.
|
||||
*
|
||||
* @param tags The list of [Tag] objects to display.
|
||||
* @param modifier Modifier to be applied to the overall layout.
|
||||
* @param textStyle The [TextStyle] used for the tag text.
|
||||
* @param spacing The spacing (in dp) between adjacent tags.
|
||||
* @param overflowChipColor The background color for the overflow chip.
|
||||
*/
|
||||
@Composable
|
||||
fun TagRow(
|
||||
tags: List<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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package com.anytypeio.anytype.core_ui.features.fields
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.views.Relations1
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun FieldTypeText(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
text: String,
|
||||
isLocal: Boolean,
|
||||
onFieldClick: () -> Unit,
|
||||
onAddToCurrentTypeClick: () -> Unit,
|
||||
onRemoveFromObjectClick: () -> Unit,
|
||||
) {
|
||||
val isMenuExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
val defaultModifier = modifier
|
||||
.combinedClickable(
|
||||
onClick = onFieldClick,
|
||||
onLongClick = {
|
||||
if (isLocal) isMenuExpanded.value = true
|
||||
}
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = colorResource(id = R.color.shape_secondary),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(vertical = 16.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
|
||||
Column(
|
||||
modifier = defaultModifier
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 16.dp),
|
||||
text = title,
|
||||
style = Relations1,
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = text,
|
||||
style = Relations1,
|
||||
color = colorResource(id = R.color.text_primary),
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
FieldItemDropDownMenu(
|
||||
showMenu = isMenuExpanded.value,
|
||||
onDismissRequest = {
|
||||
isMenuExpanded.value = false
|
||||
},
|
||||
onAddToCurrentTypeClick = {
|
||||
isMenuExpanded.value = false
|
||||
onAddToCurrentTypeClick()
|
||||
},
|
||||
onRemoveFromObjectClick = {
|
||||
isMenuExpanded.value = false
|
||||
onRemoveFromObjectClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultPreviews
|
||||
@Composable
|
||||
fun FieldTypeTextPreview() {
|
||||
FieldTypeText(
|
||||
title = "Description",
|
||||
text = "Upon creating your profile, you’ll receive your very own 12 word mnemonic ‘Recovery’ phrase to protect your account. This phrase is generated on-device and represents your master key generated upon signup, similar to a Bitcoin wallet. It also prevents anyone - including Anytype - from accessing your account and decrypting your data.\n" +
|
||||
"\n" +
|
||||
"All data you create will be stored locally (on-device) first. We use zero-knowledge encryption, meaning that your data is encrypted before it leaves your device to sync with other devices or backup nodes.",
|
||||
isLocal = true,
|
||||
onRemoveFromObjectClick = {},
|
||||
onAddToCurrentTypeClick = {},
|
||||
onFieldClick = {}
|
||||
)
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue