1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 13:57:10 +09:00

DROID-1029 Collection | Enhancement | BackLink or add to Collection (#3121)

* DROID-1029 back link di

* DROID-1029 back link, fragment + viewmodel

* DROID-1029 filters

* DROID-1029 integration

* DROID-1029 back link screen + vm

* DROID-1029 object menu

* DROID-1029 open target

* DROID-1029 snackbar fixed

* DROID-1029 else

* DROID-1029 pr fix
This commit is contained in:
Konstantin Ivanov 2023-04-25 22:22:39 +02:00 committed by GitHub
parent 028049fe50
commit 1daec1007d
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 718 additions and 39 deletions

View file

@ -10,6 +10,7 @@ import com.anytypeio.anytype.di.feature.CreateAccountModule
import com.anytypeio.anytype.di.feature.CreateBookmarkModule
import com.anytypeio.anytype.di.feature.CreateDataViewViewerModule
import com.anytypeio.anytype.di.feature.CreateObjectModule
import com.anytypeio.anytype.di.feature.DaggerBacklinkOrAddToObjectComponent
import com.anytypeio.anytype.di.feature.DaggerSplashComponent
import com.anytypeio.anytype.di.feature.DebugSettingsModule
import com.anytypeio.anytype.di.feature.EditDataViewViewerModule
@ -825,6 +826,13 @@ class ComponentManager(
.build()
}
val backLinkOrAddToObjectComponent = ComponentWithParams { ctx: Id ->
DaggerBacklinkOrAddToObjectComponent.builder()
.withContext(ctx)
.withDependencies(findComponentDependencies())
.build()
}
val typeCreationComponent = Component {
DaggerTypeCreationComponent
.factory()

View file

@ -0,0 +1,80 @@
package com.anytypeio.anytype.di.feature
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
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.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.linking.BackLinkOrAddToObjectViewModelFactory
import com.anytypeio.anytype.ui.linking.BacklinkOrAddToObjectFragment
import dagger.Binds
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
@PerScreen
@Component(
dependencies = [BacklinkOrAddToObjectDependencies::class],
modules = [
BackLinkToObjectModule::class,
BackLinkToObjectModule.Declarations::class
]
)
interface BacklinkOrAddToObjectComponent {
@Component.Builder
interface Builder {
fun withDependencies(dependency: BacklinkOrAddToObjectDependencies): Builder
@BindsInstance
fun withContext(context: Id): Builder
fun build(): BacklinkOrAddToObjectComponent
}
fun inject(fragment: BacklinkOrAddToObjectFragment)
}
@Module
object BackLinkToObjectModule {
@JvmStatic
@PerScreen
@Provides
fun searchObjects(repo: BlockRepository): SearchObjects = SearchObjects(repo = repo)
@JvmStatic
@Provides
@PerScreen
fun provideGetObjectTypesUseCase(
repository: BlockRepository,
dispatchers: AppCoroutineDispatchers
): GetObjectTypes = GetObjectTypes(repository, dispatchers)
@Module
interface Declarations {
@PerScreen
@Binds
fun bindViewModelFactory(factory: BackLinkOrAddToObjectViewModelFactory): ViewModelProvider.Factory
}
}
interface BacklinkOrAddToObjectDependencies : ComponentDependencies {
fun authRepository(): AuthRepository
fun blockRepository(): BlockRepository
fun workspaceManager(): WorkspaceManager
fun urlBuilder(): UrlBuilder
fun dispatchers(): AppCoroutineDispatchers
fun analytics(): Analytics
}

View file

@ -10,6 +10,7 @@ import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.interactor.CreateBlock
import com.anytypeio.anytype.domain.block.interactor.UpdateFields
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.collections.AddObjectToCollection
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.misc.UrlBuilder
@ -110,7 +111,8 @@ object ObjectMenuModule {
dispatcher: Dispatcher<Payload>,
updateFields: UpdateFields,
featureToggles: FeatureToggles,
delegator: Delegator<Action>
delegator: Delegator<Action>,
addObjectToCollection: AddObjectToCollection
): ObjectMenuViewModel.Factory = ObjectMenuViewModel.Factory(
setObjectIsArchived = setObjectIsArchived,
duplicateObject = duplicateObject,
@ -124,7 +126,8 @@ object ObjectMenuModule {
dispatcher = dispatcher,
updateFields = updateFields,
delegator = delegator,
menuOptionsProvider = createMenuOptionsProvider(storage, featureToggles)
menuOptionsProvider = createMenuOptionsProvider(storage, featureToggles),
addObjectToCollection = addObjectToCollection
)
@JvmStatic
@ -134,6 +137,17 @@ object ObjectMenuModule {
restrictions = storage.objectRestrictions.stream(),
featureToggles = featureToggles
)
@JvmStatic
@Provides
@PerDialog
fun provideAddObjectToCollection(
repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
): AddObjectToCollection = AddObjectToCollection(
repo = repo,
dispatchers = dispatchers
)
}
@Module
@ -153,7 +167,8 @@ object ObjectSetMenuModule {
analytics: Analytics,
state: MutableStateFlow<ObjectState>,
featureToggles: FeatureToggles,
dispatcher: Dispatcher<Payload>
dispatcher: Dispatcher<Payload>,
addObjectToCollection: AddObjectToCollection
): ObjectSetMenuViewModel.Factory = ObjectSetMenuViewModel.Factory(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
@ -165,7 +180,8 @@ object ObjectSetMenuModule {
analytics = analytics,
objectState = state,
dispatcher = dispatcher,
menuOptionsProvider = createMenuOptionsProvider(state, featureToggles)
menuOptionsProvider = createMenuOptionsProvider(state, featureToggles),
addObjectToCollection = addObjectToCollection
)
@JvmStatic

View file

@ -4,6 +4,7 @@ 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.AuthSubComponent
import com.anytypeio.anytype.di.feature.BacklinkOrAddToObjectDependencies
import com.anytypeio.anytype.di.feature.CreateBookmarkSubComponent
import com.anytypeio.anytype.di.feature.CreateObjectSubComponent
import com.anytypeio.anytype.di.feature.DebugSettingsSubComponent
@ -72,7 +73,8 @@ interface MainComponent :
RelationEditDependencies,
SplashDependencies,
DeletedAccountDependencies,
MigrationErrorDependencies {
MigrationErrorDependencies,
BacklinkOrAddToObjectDependencies {
fun inject(app: AndroidApplication)
@ -170,4 +172,9 @@ private abstract class ComponentDependenciesModule private constructor() {
@ComponentDependenciesKey(MigrationErrorDependencies::class)
abstract fun migrationErrorDependencies(component: MainComponent) : ComponentDependencies
@Binds
@IntoMap
@ComponentDependenciesKey(BacklinkOrAddToObjectDependencies::class)
abstract fun provideBackLinkDependencies(component: MainComponent): ComponentDependencies
}

View file

@ -34,7 +34,7 @@ class NavigationRouter(
command.id,
command.editorSettings
)
is AppNavigation.Command.OpenObjectSet -> navigation.openObjectSet(
is AppNavigation.Command.OpenSetOrCollection -> navigation.openObjectSet(
command.target,
command.isPopUpToDashboard
)

View file

@ -9,6 +9,7 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_ui.features.objects.ObjectActionAdapter
import com.anytypeio.anytype.core_ui.layout.SpacingItemDecoration
import com.anytypeio.anytype.core_ui.reactive.click
@ -27,13 +28,15 @@ 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.moving.MoveToFragment
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
abstract class ObjectMenuBaseFragment :
BaseBottomSheetFragment<FragmentObjectMenuBinding>(),
OnMoveToAction {
OnMoveToAction,
BacklinkAction {
protected val ctx get() = arg<Id>(CTX_KEY)
private val isProfile get() = arg<Boolean>(IS_PROFILE_KEY)
@ -193,13 +196,7 @@ abstract class ObjectMenuBaseFragment :
private fun openLinkChooser() {
val fr = MoveToFragment.new(
ctx = ctx,
blocks = emptyList(),
restorePosition = null,
restoreBlock = null,
title = getString(R.string.link_to)
)
val fr = BacklinkOrAddToObjectFragment.new(ctx = ctx)
fr.showChildFragment()
}
@ -208,13 +205,17 @@ abstract class ObjectMenuBaseFragment :
dialog?.window
?.decorView
?.showActionableSnackBar(
command.currentObjectName,
command.targetObjectName,
command.icon,
R.string.snack_link_to,
binding.anchor
from = command.currentObjectName,
to = command.targetObjectName,
icon = command.icon,
middleString = R.string.snack_link_to,
anchor = binding.anchor
) {
vm.proceedWithOpeningPage(command.id)
if (command.isCollection) {
vm.proceedWithOpeningCollection(command.id)
} else {
vm.proceedWithOpeningPage(command.id)
}
}
}, 300L)
}
@ -230,6 +231,17 @@ abstract class ObjectMenuBaseFragment :
vm.onLinkedMyselfTo(myself = ctx, addTo = target, fromName)
}
override fun backLink(id: Id, name: String, layout: ObjectType.Layout?, icon: ObjectIcon) {
vm.onBackLinkOrAddToObjectAction(
ctx = ctx,
backLinkId = id,
backLinkName = name,
backLinkLayout = layout,
backLinkIcon = icon,
fromName = fromName.orEmpty()
)
}
override fun onMoveToClose(blocks: List<Id>, restorePosition: Int?, restoreBlock: Id?) {}
override fun inflateBinding(

View file

@ -0,0 +1,262 @@
package com.anytypeio.anytype.ui.linking
import android.content.DialogInterface
import android.content.res.Resources
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.FrameLayout
import androidx.core.os.bundleOf
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.anytypeio.anytype.R
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_ui.features.navigation.DefaultObjectViewAdapter
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.drawable
import com.anytypeio.anytype.core_utils.ext.hideSoftInput
import com.anytypeio.anytype.core_utils.ext.invisible
import com.anytypeio.anytype.core_utils.ext.statusBarHeight
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.core_utils.ext.withParent
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetTextInputFragment
import com.anytypeio.anytype.databinding.FragmentObjectSearchBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.linking.BackLinkOrAddToObjectViewModel
import com.anytypeio.anytype.presentation.linking.BackLinkOrAddToObjectViewModelFactory
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.search.ObjectSearchView
import com.anytypeio.anytype.ui.moving.hideProgress
import com.anytypeio.anytype.ui.moving.showProgress
import com.anytypeio.anytype.ui.search.ObjectSearchFragment
import com.google.android.material.bottomsheet.BottomSheetBehavior
import javax.inject.Inject
import kotlinx.coroutines.launch
import timber.log.Timber
class BacklinkOrAddToObjectFragment :
BaseBottomSheetTextInputFragment<FragmentObjectSearchBinding>() {
private val vm by viewModels<BackLinkOrAddToObjectViewModel> { factory }
@Inject
lateinit var factory: BackLinkOrAddToObjectViewModelFactory
private val ctx get() = arg<Id>(ARG_CTX)
private lateinit var clearSearchText: View
private lateinit var filterInputField: EditText
override val textInput: EditText get() = binding.searchView.root.findViewById(R.id.filterInputField)
private val moveToAdapter by lazy {
DefaultObjectViewAdapter(
onDefaultObjectClicked = vm::onObjectClicked
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupFullHeight()
setTransparent()
BottomSheetBehavior.from(binding.sheet).apply {
state = BottomSheetBehavior.STATE_EXPANDED
isHideable = true
skipCollapsed = true
addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
vm.onDialogCancelled()
}
}
}
)
}
vm.state.observe(viewLifecycleOwner) { observe(it) }
clearSearchText = binding.searchView.root.findViewById(R.id.clearSearchText)
filterInputField = binding.searchView.root.findViewById(R.id.filterInputField)
filterInputField.setHint(R.string.search)
filterInputField.imeOptions = EditorInfo.IME_ACTION_DONE
filterInputField.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
return@setOnEditorActionListener false
}
true
}
initialize()
}
override fun onStart() {
lifecycleScope.launch {
jobs += subscribe(vm.commands) { execute(it) }
}
super.onStart()
vm.onStart(EventsDictionary.Routes.screenSettings, ignore = ctx)
expand()
}
override fun onStop() {
super.onStop()
vm.onStop()
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
vm.onDialogCancelled()
}
private fun observe(state: ObjectSearchView) {
when (state) {
ObjectSearchView.Loading -> {
with(binding) {
recyclerView.invisible()
tvScreenStateMessage.invisible()
tvScreenStateSubMessage.invisible()
showProgress()
}
}
is ObjectSearchView.Success -> {
with(binding) {
hideProgress()
tvScreenStateMessage.invisible()
tvScreenStateSubMessage.invisible()
recyclerView.visible()
moveToAdapter.submitList(state.objects)
}
}
ObjectSearchView.EmptyPages -> {
with(binding) {
hideProgress()
recyclerView.invisible()
tvScreenStateMessage.visible()
tvScreenStateMessage.text = getString(R.string.search_empty_pages)
tvScreenStateSubMessage.invisible()
}
}
is ObjectSearchView.NoResults -> {
with(binding) {
hideProgress()
recyclerView.invisible()
tvScreenStateMessage.visible()
tvScreenStateMessage.text =
getString(R.string.search_no_results, state.searchText)
tvScreenStateSubMessage.visible()
}
}
is ObjectSearchView.Error -> {
with(binding) {
hideProgress()
recyclerView.invisible()
tvScreenStateMessage.visible()
tvScreenStateMessage.text = state.error
tvScreenStateSubMessage.invisible()
}
}
else -> Timber.d("Skipping state: $state")
}
}
private fun execute(command: BackLinkOrAddToObjectViewModel.Command) {
hideSoftInput()
when (command) {
BackLinkOrAddToObjectViewModel.Command.Exit -> {
dismiss()
}
is BackLinkOrAddToObjectViewModel.Command.CreateBacklink -> {
withParent<BacklinkAction> {
backLink(
id = command.id,
name = command.name,
layout = command.layout,
icon = command.icon
)
}
dismiss()
}
}
}
private fun initialize() {
with(binding.tvScreenTitle) {
text = getString(R.string.link_to)
visible()
}
binding.recyclerView.invisible()
binding.tvScreenStateMessage.invisible()
binding.hideProgress()
clearSearchText.setOnClickListener {
filterInputField.setText(ObjectSearchFragment.EMPTY_FILTER_TEXT)
clearSearchText.invisible()
}
filterInputField.doAfterTextChanged { newText ->
if (newText != null) {
vm.onSearchTextChanged(newText.toString())
}
if (newText.isNullOrEmpty()) {
clearSearchText.invisible()
} else {
clearSearchText.visible()
}
}
with(binding.recyclerView) {
layoutManager = LinearLayoutManager(requireContext())
adapter = moveToAdapter
addItemDecoration(
DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply {
setDrawable(drawable(R.drawable.divider_object_search))
}
)
}
}
private fun setupFullHeight() {
val lp = (binding.root.layoutParams as FrameLayout.LayoutParams)
lp.height =
Resources.getSystem().displayMetrics.heightPixels - requireActivity().statusBarHeight
binding.root.layoutParams = lp
}
private fun setTransparent() {
with(binding.root) {
background = null
(parent as? View)?.setBackgroundColor(Color.TRANSPARENT)
}
}
override fun injectDependencies() {
componentManager().backLinkOrAddToObjectComponent.get(ctx).inject(this)
}
override fun releaseDependencies() {
componentManager().backLinkOrAddToObjectComponent.release()
}
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentObjectSearchBinding = FragmentObjectSearchBinding.inflate(
inflater, container, false
)
companion object {
const val ARG_CTX = "arg.bind_link.ctx"
fun new(ctx: Id) = BacklinkOrAddToObjectFragment().apply {
arguments = bundleOf(ARG_CTX to ctx)
}
}
}
interface BacklinkAction {
fun backLink(id: Id, name: String, layout: ObjectType.Layout?, icon: ObjectIcon)
}

View file

@ -192,7 +192,7 @@ abstract class RelationValueBaseFragment<T: ViewBinding> : BaseBottomSheetFragme
private fun navigate(command: AppNavigation.Command) {
when (command) {
is AppNavigation.Command.OpenObjectSet -> {
is AppNavigation.Command.OpenSetOrCollection -> {
findNavController().navigate(
R.id.dataViewNavigation,
bundleOf(ObjectSetFragment.CONTEXT_ID_KEY to command.target)

View file

@ -6,8 +6,8 @@
android:top="48dp">
<shape android:shape="rectangle">
<solid android:color="@color/background_secondary" />
<corners android:radius="12dp" />
<solid android:color="@color/snackbar_background" />
<corners android:radius="8dp" />
</shape>
</item>
</layer-list>

View file

@ -9,7 +9,8 @@
<TextView
android:id="@+id/snackbar_text"
style="@style/TextView.UXStyle.Captions.1.Regular"
style="@style/TextView.UXStyle.Captions.1.Medium"
android:textColor="@color/text_button_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="77dp"
@ -43,7 +44,8 @@
<TextView
android:id="@+id/snackbar_action"
style="@style/TextView.UXStyle.Captions.1.Regular"
style="@style/TextView.UXStyle.Captions.1.Medium"
android:textColor="@color/text_button_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"

View file

@ -22,6 +22,7 @@ object ObjectTypeIds {
const val DASHBOARD = "ot-dashboard"
const val BOOKMARK = "ot-bookmark"
const val RELATION_OPTION = "ot-relationOption"
const val SPACE = "ot-space"
const val DEFAULT_OBJECT_TYPE_PREFIX = "ot-"
}

View file

@ -90,5 +90,6 @@
<color name="colorPrimary">#FF5722</color>
<color name="colorPrimaryDark">#000000</color>
<color name="colorAccent">#3F51B5</color>
<color name="snackbar_background">#FFFFFF</color>
</resources>

View file

@ -211,5 +211,6 @@
<color name="colorPrimary">#FF5722</color>
<color name="colorPrimaryDark">#000000</color>
<color name="colorAccent">#3F51B5</color>
<color name="snackbar_background">#000000</color>
</resources>

View file

@ -22,5 +22,6 @@ sealed class Action {
object SearchOnPage: Action()
object UndoRedo : Action()
data class OpenObject(val id: Id) : Action()
data class OpenCollection(val id: Id) : Action()
data class Duplicate(val id: Id) : Action()
}

View file

@ -386,6 +386,7 @@ class EditorViewModel(
Action.SearchOnPage -> onEnterSearchModeClicked()
Action.UndoRedo -> onUndoRedoActionClicked()
is Action.OpenObject -> proceedWithOpeningObject(action.id)
is Action.OpenCollection -> proceedWithOpeningDataViewObject(action.id)
}
}
}
@ -4143,7 +4144,7 @@ class EditorViewModel(
Timber.e(it, "Error while closing object")
navigate(
EventWrapper(
AppNavigation.Command.OpenObjectSet(
AppNavigation.Command.OpenSetOrCollection(
target,
isPopUpToDashboard
)
@ -4153,7 +4154,7 @@ class EditorViewModel(
onSuccess = {
navigate(
EventWrapper(
AppNavigation.Command.OpenObjectSet(
AppNavigation.Command.OpenSetOrCollection(
target,
isPopUpToDashboard
)

View file

@ -0,0 +1,98 @@
package com.anytypeio.anytype.presentation.linking
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.domain.base.Resultat
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.navigation.DefaultObjectView
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import com.anytypeio.anytype.presentation.search.ObjectSearchViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
class BackLinkOrAddToObjectViewModel(
urlBuilder: UrlBuilder,
searchObjects: SearchObjects,
getObjectTypes: GetObjectTypes,
private val analytics: Analytics,
private val workspaceManager: WorkspaceManager
) : ObjectSearchViewModel(
urlBuilder = urlBuilder,
getObjectTypes = getObjectTypes,
searchObjects = searchObjects,
analytics = analytics,
workspaceManager = workspaceManager
) {
private val _commands = MutableSharedFlow<Command>(replay = 0)
val commands: SharedFlow<Command> = _commands
/**
* Adding a source object as a backlink is only possible in objects
* with these Layouts and also in Collection objects.
*/
private val supported = listOf(
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO,
ObjectType.Layout.NOTE,
ObjectType.Layout.COLLECTION
)
override suspend fun getSearchObjectsParams(ignore: Id?) = SearchObjects.Params(
limit = SEARCH_LIMIT,
filters = ObjectSearchConstants.filtersBackLinkOrAddToObject(
ignore = ignore,
workspaceId = workspaceManager.getCurrentWorkspace()
),
sorts = ObjectSearchConstants.sortBackLinkOrAddToObject,
fulltext = EMPTY_QUERY,
keys = ObjectSearchConstants.defaultKeys
)
override fun onObjectClicked(view: DefaultObjectView) {
sendSearchResultEvent(view.id)
viewModelScope.launch {
_commands.emit(
Command.CreateBacklink(
id = view.id,
name = view.name,
layout = view.layout,
icon = view.icon
)
)
}
}
override fun onDialogCancelled() {
viewModelScope.launch {
_commands.emit(Command.Exit)
}
}
override suspend fun setObjects(data: List<ObjectWrapper.Basic>) {
objects.emit(
Resultat.success(data.filter {
supported.contains(it.layout)
})
)
}
sealed class Command {
object Exit : Command()
data class CreateBacklink(
val id: Id,
val name: String,
val layout: ObjectType.Layout?,
val icon: ObjectIcon
) : Command()
}
}

View file

@ -5,11 +5,14 @@ import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.tools.UrlValidator
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.collections.AddObjectToCollection
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.page.AddBackLinkToObject
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.editor.Editor
import javax.inject.Inject
class LinkToObjectViewModelFactory(
private val urlBuilder: UrlBuilder,
@ -53,4 +56,24 @@ class LinkToObjectOrWebViewModelFactory(
workspaceManager = workspaceManager
) as T
}
}
class BackLinkOrAddToObjectViewModelFactory @Inject constructor(
private val urlBuilder: UrlBuilder,
private val getObjectTypes: GetObjectTypes,
private val searchObjects: SearchObjects,
private val workspaceManager: WorkspaceManager,
private val analytics: Analytics
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return BackLinkOrAddToObjectViewModel(
urlBuilder = urlBuilder,
getObjectTypes = getObjectTypes,
searchObjects = searchObjects,
analytics = analytics,
workspaceManager = workspaceManager
) as T
}
}

View file

@ -105,7 +105,7 @@ interface AppNavigation {
data class ExitToDesktopAndOpenPage(val pageId: String) : Command()
object OpenPageSearch : Command()
data class OpenObjectSet(val target: String, val isPopUpToDashboard: Boolean = false) :
data class OpenSetOrCollection(val target: String, val isPopUpToDashboard: Boolean = false) :
Command()
data class LaunchObjectSet(val target: Id) : Command()

View file

@ -12,6 +12,7 @@ import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.block.interactor.UpdateFields
import com.anytypeio.anytype.domain.collections.AddObjectToCollection
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.misc.UrlBuilder
@ -41,7 +42,8 @@ class ObjectMenuViewModel(
private val debugTreeShareDownloader: DebugTreeShareDownloader,
private val storage: Editor.Storage,
private val analytics: Analytics,
private val updateFields: UpdateFields
private val updateFields: UpdateFields,
private val addObjectToCollection: AddObjectToCollection
) : ObjectMenuViewModelBase(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
@ -52,7 +54,8 @@ class ObjectMenuViewModel(
urlBuilder = urlBuilder,
dispatcher = dispatcher,
analytics = analytics,
menuOptionsProvider = menuOptionsProvider
menuOptionsProvider = menuOptionsProvider,
addObjectToCollection = addObjectToCollection
) {
private val objectRestrictions = storage.objectRestrictions.current()
@ -294,7 +297,8 @@ class ObjectMenuViewModel(
private val dispatcher: Dispatcher<Payload>,
private val updateFields: UpdateFields,
private val delegator: Delegator<Action>,
private val menuOptionsProvider: ObjectMenuOptionsProvider
private val menuOptionsProvider: ObjectMenuOptionsProvider,
private val addObjectToCollection: AddObjectToCollection
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ObjectMenuViewModel(
@ -310,7 +314,8 @@ class ObjectMenuViewModel(
dispatcher = dispatcher,
updateFields = updateFields,
delegator = delegator,
menuOptionsProvider = menuOptionsProvider
menuOptionsProvider = menuOptionsProvider,
addObjectToCollection = addObjectToCollection
) as T
}
}

View file

@ -1,12 +1,15 @@
package com.anytypeio.anytype.presentation.objects.menu
import android.net.Uri
import android.widget.Toast
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.domain.`object`.DuplicateObject
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.collections.AddObjectToCollection
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.misc.UrlBuilder
@ -40,7 +43,8 @@ abstract class ObjectMenuViewModelBase(
protected val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics,
private val menuOptionsProvider: ObjectMenuOptionsProvider,
private val duplicateObject: DuplicateObject
private val duplicateObject: DuplicateObject,
private val addObjectToCollection: AddObjectToCollection
) : BaseViewModel() {
protected val jobs = mutableListOf<Job>()
@ -168,6 +172,74 @@ abstract class ObjectMenuViewModelBase(
}
}
fun onBackLinkOrAddToObjectAction(
ctx: Id,
backLinkId: Id,
backLinkName: String,
backLinkLayout: ObjectType.Layout?,
backLinkIcon: ObjectIcon,
fromName: String
) {
Timber.e("onBackLinkOrAddToObjectAction, ctx:[$ctx], backLinkId:[$backLinkId], backLinkName:[$backLinkName], backLinkLayout:[$backLinkLayout], fromName:[$fromName]")
if (backLinkLayout == null) {
Timber.e("onBackLinkOrAddToObjectAction, layout is null")
viewModelScope.launch { _toasts.emit(BACK_LINK_WRONG_LAYOUT) }
return
}
when (backLinkLayout) {
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO,
ObjectType.Layout.NOTE -> {
onLinkedMyselfTo(
myself = ctx, addTo = backLinkId, fromName = fromName
)
}
ObjectType.Layout.COLLECTION -> {
proceedWithAddObjectToCollection(
ctx = ctx,
collection = backLinkId,
collectionName = backLinkName,
collectionIcon = backLinkIcon,
fromName = fromName
)
}
else -> { Timber.e("onBackLinkOrAddToObjectAction, layout:$backLinkLayout is not supported") }
}
}
private fun proceedWithAddObjectToCollection(
ctx: Id,
collection: Id,
collectionName: String,
collectionIcon: ObjectIcon,
fromName: String
) {
val params = AddObjectToCollection.Params(
ctx = collection,
after = "",
targets = listOf(ctx)
)
viewModelScope.launch {
addObjectToCollection.execute(params).fold(
onSuccess = { payload ->
dispatcher.send(payload)
sendAnalyticsObjectLinkToEvent(analytics)
commands.emit(
Command.OpenSnackbar(
id = collection,
currentObjectName = fromName,
targetObjectName = collectionName,
icon = collectionIcon,
isCollection = true
)
)
},
onFailure = { Timber.e(it, "Error while adding object to collection") }
)
}
}
fun onLinkedMyselfTo(myself: Id, addTo: Id, fromName: String?) {
Timber.d("onLinkedMyselfTo, myself:[$myself], addTo:[$addTo], fromName:[$fromName]")
jobs += viewModelScope.launch {
@ -203,6 +275,13 @@ abstract class ObjectMenuViewModelBase(
}
}
fun proceedWithOpeningCollection(id: Id) {
Timber.d("proceedWithOpeningCollection, id:[$id]")
viewModelScope.launch {
delegator.delegate(Action.OpenCollection(id))
}
}
fun proceedWithDuplication(ctx: Id) {
Timber.d("proceedWithDuplication, ctx:[$ctx]")
viewModelScope.launch {
@ -238,7 +317,8 @@ abstract class ObjectMenuViewModelBase(
val id: Id,
val currentObjectName: String?,
val targetObjectName: String?,
val icon: ObjectIcon
val icon: ObjectIcon,
val isCollection: Boolean = false
) : Command()
}
@ -253,5 +333,6 @@ abstract class ObjectMenuViewModelBase(
const val OBJECT_IS_LOCKED_MSG = "Your object is locked"
const val OBJECT_IS_UNLOCKED_MSG = "Your object is locked"
const val SOMETHING_WENT_WRONG_MSG = "Something went wrong. Please, try again later."
const val BACK_LINK_WRONG_LAYOUT = "Wrong object layout, try again"
}
}

View file

@ -7,6 +7,7 @@ import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.domain.collections.AddObjectToCollection
import com.anytypeio.anytype.domain.dashboard.interactor.AddToFavorite
import com.anytypeio.anytype.domain.dashboard.interactor.RemoveFromFavorite
import com.anytypeio.anytype.domain.misc.UrlBuilder
@ -34,6 +35,7 @@ class ObjectSetMenuViewModel(
menuOptionsProvider: ObjectMenuOptionsProvider,
private val objectState: StateFlow<ObjectState>,
private val analytics: Analytics,
private val addObjectToCollection: AddObjectToCollection
) : ObjectMenuViewModelBase(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
@ -45,6 +47,7 @@ class ObjectSetMenuViewModel(
dispatcher = dispatcher,
analytics = analytics,
menuOptionsProvider = menuOptionsProvider,
addObjectToCollection = addObjectToCollection
) {
@Suppress("UNCHECKED_CAST")
@ -60,6 +63,7 @@ class ObjectSetMenuViewModel(
private val analytics: Analytics,
private val objectState: StateFlow<ObjectState>,
private val menuOptionsProvider: ObjectMenuOptionsProvider,
private val addObjectToCollection: AddObjectToCollection
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ObjectSetMenuViewModel(
@ -73,7 +77,8 @@ class ObjectSetMenuViewModel(
analytics = analytics,
objectState = objectState,
dispatcher = dispatcher,
menuOptionsProvider = menuOptionsProvider
menuOptionsProvider = menuOptionsProvider,
addObjectToCollection = addObjectToCollection
) as T
}
}

View file

@ -10,6 +10,7 @@ import com.anytypeio.anytype.core_models.Marketplace.MARKETPLACE_ID
import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.ObjectTypeIds.AUDIO
import com.anytypeio.anytype.core_models.ObjectTypeIds.BOOKMARK
import com.anytypeio.anytype.core_models.ObjectTypeIds.DASHBOARD
import com.anytypeio.anytype.core_models.ObjectTypeIds.DATE
import com.anytypeio.anytype.core_models.ObjectTypeIds.FILE
@ -17,6 +18,8 @@ import com.anytypeio.anytype.core_models.ObjectTypeIds.IMAGE
import com.anytypeio.anytype.core_models.ObjectTypeIds.OBJECT_TYPE
import com.anytypeio.anytype.core_models.ObjectTypeIds.RELATION
import com.anytypeio.anytype.core_models.ObjectTypeIds.RELATION_OPTION
import com.anytypeio.anytype.core_models.ObjectTypeIds.SET
import com.anytypeio.anytype.core_models.ObjectTypeIds.SPACE
import com.anytypeio.anytype.core_models.ObjectTypeIds.TEMPLATE
import com.anytypeio.anytype.core_models.ObjectTypeIds.VIDEO
import com.anytypeio.anytype.core_models.Relations
@ -426,6 +429,62 @@ object ObjectSearchConstants {
)
//endregion
//region BACK LINK OR ADD TO OBJECT
fun filtersBackLinkOrAddToObject(ignore: Id?, workspaceId: String) = listOf(
DVFilter(
relation = Relations.IS_ARCHIVED,
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.IS_HIDDEN,
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.IS_DELETED,
condition = DVFilterCondition.NOT_EQUAL,
value = true
),
DVFilter(
relation = Relations.TYPE,
condition = DVFilterCondition.NOT_IN,
value = listOf(
OBJECT_TYPE,
RELATION,
TEMPLATE,
IMAGE,
FILE,
VIDEO,
AUDIO,
DASHBOARD,
DATE,
RELATION_OPTION,
SPACE,
SET,
BOOKMARK
)
),
DVFilter(
relation = Relations.ID,
condition = DVFilterCondition.NOT_EQUAL,
value = ignore
),
DVFilter(
relation = Relations.WORKSPACE_ID,
condition = DVFilterCondition.EQUAL,
value = workspaceId
)
)
val sortBackLinkOrAddToObject = listOf(
DVSort(
relationKey = Relations.LAST_MODIFIED_DATE,
type = DVSortType.DESC
)
)
//endregion
val defaultKeys = listOf(
Relations.ID,
Relations.NAME,

View file

@ -230,6 +230,7 @@ class ObjectSetViewModel(
proceedWithSettingUnsplashImage(action)
}
is Action.OpenObject -> proceedWithOpeningObject(action.id)
is Action.OpenCollection -> proceedWithOpeningObjectCollection(action.id)
is Action.Duplicate -> proceedWithNavigation(
target = action.id,
layout = ObjectType.Layout.SET
@ -1085,6 +1086,21 @@ class ObjectSetViewModel(
}
}
private suspend fun proceedWithOpeningObjectCollection(target: Id) {
isCustomizeViewPanelVisible.value = false
jobs += viewModelScope.launch {
closeBlock.execute(context).fold(
onSuccess = {
navigate(EventWrapper(AppNavigation.Command.OpenSetOrCollection(target = target)))
},
onFailure = {
Timber.e(it, "Error while closing object set: $context")
navigate(EventWrapper(AppNavigation.Command.OpenSetOrCollection(target = target)))
}
)
}
}
private suspend fun proceedWithNavigation(target: Id, layout: ObjectType.Layout?) {
when (layout) {
ObjectType.Layout.BASIC,
@ -1097,11 +1113,11 @@ class ObjectSetViewModel(
ObjectType.Layout.SET, ObjectType.Layout.COLLECTION -> {
closeBlock.execute(context).fold(
onSuccess = {
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(target)))
navigate(EventWrapper(AppNavigation.Command.OpenSetOrCollection(target)))
},
onFailure = {
Timber.e(it, "Error while closing object set: $context")
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(target)))
navigate(EventWrapper(AppNavigation.Command.OpenSetOrCollection(target)))
}
)
}

View file

@ -491,7 +491,7 @@ abstract class RelationValueBaseViewModel(
}
ObjectType.Layout.SET -> {
viewModelScope.launch {
navigation.emit(AppNavigation.Command.OpenObjectSet(id))
navigation.emit(AppNavigation.Command.OpenSetOrCollection(id))
}
}
else -> Timber.d("Unexpected layout: $layout").also {