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

DROID-1460 Editor | Enhancement | Use-current-object-as-template action (#315)

This commit is contained in:
Konstantin Ivanov 2023-08-29 21:14:24 +02:00 committed by GitHub
parent 1ca5bb5632
commit 750e9f79ab
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 232 additions and 24 deletions

View file

@ -18,6 +18,7 @@ import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
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.templates.CreateTemplateFromObject
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.editor.Editor
@ -112,7 +113,8 @@ object ObjectMenuModule {
updateFields: UpdateFields,
featureToggles: FeatureToggles,
delegator: Delegator<Action>,
addObjectToCollection: AddObjectToCollection
addObjectToCollection: AddObjectToCollection,
createTemplateFromObject: CreateTemplateFromObject
): ObjectMenuViewModel.Factory = ObjectMenuViewModel.Factory(
setObjectIsArchived = setObjectIsArchived,
duplicateObject = duplicateObject,
@ -127,7 +129,8 @@ object ObjectMenuModule {
updateFields = updateFields,
delegator = delegator,
menuOptionsProvider = createMenuOptionsProvider(storage, featureToggles),
addObjectToCollection = addObjectToCollection
addObjectToCollection = addObjectToCollection,
createTemplateFromObject = createTemplateFromObject
)
@JvmStatic

View file

@ -32,6 +32,9 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DefaultItemAnimator
@ -145,6 +148,7 @@ import com.anytypeio.anytype.ui.relations.RelationAddToObjectBlockFragment
import com.anytypeio.anytype.ui.relations.RelationDateValueFragment
import com.anytypeio.anytype.ui.relations.RelationTextValueFragment
import com.anytypeio.anytype.ui.relations.RelationValueFragment
import com.anytypeio.anytype.ui.templates.EditorTemplateFragment
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import javax.inject.Inject
@ -506,6 +510,8 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
OutsideClickDetector(vm::onOutsideClicked)
)
observeSelectingTemplate()
binding.recycler.apply {
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
@ -2088,6 +2094,30 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
}
}
open fun observeSelectingTemplate() {
val navController = findNavController()
val navBackStackEntry = navController.getBackStackEntry(R.id.pageScreen)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains(EditorTemplateFragment.ARG_TEMPLATE_ID)
) {
val result =
navBackStackEntry.savedStateHandle.get<String>(EditorTemplateFragment.ARG_TEMPLATE_ID);
if (!result.isNullOrBlank()) {
navBackStackEntry.savedStateHandle.remove<String>(EditorTemplateFragment.ARG_TEMPLATE_ID)
vm.onCreateObjectWithTemplateClicked(template = result)
}
}
}
navBackStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
//------------ End of Anytype Custom Context Menu ------------
override fun inflateBinding(

View file

@ -25,6 +25,7 @@ import com.anytypeio.anytype.databinding.FragmentObjectMenuBinding
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuOptionsProvider
import com.anytypeio.anytype.presentation.objects.menu.ObjectMenuViewModelBase
import com.anytypeio.anytype.ui.editor.EditorModalFragment
import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectFragment
import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectSetFragment
import com.anytypeio.anytype.ui.editor.layout.ObjectLayoutFragment
@ -130,9 +131,18 @@ abstract class ObjectMenuBaseFragment :
ObjectMenuViewModelBase.Command.OpenLinkToChooser -> openLinkChooser()
is ObjectMenuViewModelBase.Command.OpenSnackbar -> openSnackbar(command)
is ObjectMenuViewModelBase.Command.ShareDebugTree -> shareFile(command.uri)
is ObjectMenuViewModelBase.Command.OpenTemplate -> openTemplate(command)
}
}
private fun openTemplate(command: ObjectMenuViewModelBase.Command.OpenTemplate) {
toast(getString(R.string.snackbar_template_add) + command.typeName)
findNavController().navigate(
R.id.nav_editor_modal,
bundleOf(EditorModalFragment.ARG_ID to command.template)
)
}
private fun openObjectCover() {
findNavController().navigate(
R.id.objectCoverScreen,

View file

@ -31,6 +31,8 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
@ -99,7 +101,6 @@ import com.anytypeio.anytype.ui.sets.modals.ObjectSetSettingsFragment
import com.anytypeio.anytype.ui.sets.modals.SetObjectCreateRecordFragmentBase
import com.anytypeio.anytype.ui.sets.modals.sort.ViewerSortFragment
import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.ARG_TEMPLATE_ID
import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.SELECTED_TEMPLATE_INITIAL_VALUE
import com.bumptech.glide.Glide
import javax.inject.Inject
import kotlinx.coroutines.flow.launchIn
@ -1166,13 +1167,25 @@ open class ObjectSetFragment :
)
private fun observeSelectingTemplate() {
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>(
ARG_TEMPLATE_ID,
SELECTED_TEMPLATE_INITIAL_VALUE
)?.observe(viewLifecycleOwner) { template: Id ->
Timber.d("Get result from EditorTemplateFragment: $template")
if (template.isNotEmpty()) vm.proceedWithCreatingNewDataViewObject(template)
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 result = navBackStackEntry.savedStateHandle.get<String>(ARG_TEMPLATE_ID);
if (!result.isNullOrBlank()) {
navBackStackEntry.savedStateHandle.remove<String>(ARG_TEMPLATE_ID)
vm.proceedWithCreatingNewDataViewObject(result)
}
}
}
navBackStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
companion object {

View file

@ -73,6 +73,10 @@ class EditorTemplateFragment : EditorFragment() {
}
}
override fun observeSelectingTemplate() {
// Do nothing
}
companion object {
fun newInstance(id: String): EditorTemplateFragment =
EditorTemplateFragment().apply {
@ -80,6 +84,5 @@ class EditorTemplateFragment : EditorFragment() {
}
const val ARG_TEMPLATE_ID = "template_id"
const val SELECTED_TEMPLATE_INITIAL_VALUE = ""
}
}

View file

@ -308,6 +308,7 @@
android:id="@+id/btnSelectTemplate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dp_10"
android:layout_marginStart="@dimen/dp_20"
android:layout_marginEnd="@dimen/dp_20"
android:text="Select Template"

View file

@ -76,8 +76,9 @@ class ObjectActionAdapter(
ivActionIcon.setImageResource(R.drawable.ic_object_action_link_to)
tvActionTitle.setText(R.string.link_to)
}
ObjectAction.MOVE_TO -> {
ObjectAction.USE_AS_TEMPLATE -> {
ivActionIcon.setImageResource(R.drawable.ic_object_action_template)
tvActionTitle.setText(R.string.make_template)
}
else -> {}
}

View file

@ -265,6 +265,7 @@
<string name="move_to">Move to</string>
<string name="preview">Preview</string>
<string name="link_to">Link to</string>
<string name="make_template">Make template</string>
<string name="linked_to">Linked to</string>
<string name="remove_link">Remove link</string>
<string name="mention_suggester_new_page">Create new object</string>

View file

@ -849,4 +849,8 @@ class BlockDataRepository(
override suspend fun duplicateObjectsList(ids: List<Id>): List<Id> {
return remote.duplicateObjectsList(ids)
}
override suspend fun createTemplateFromObject(ctx: Id): Id {
return remote.createTemplateFromObject(ctx)
}
}

View file

@ -364,4 +364,5 @@ interface BlockRemote {
suspend fun setInternalFlags(command: Command.SetInternalFlags): Payload
suspend fun duplicateObjectsList(ids: List<Id>): List<Id>
suspend fun createTemplateFromObject(ctx: Id): Id
}

View file

@ -414,4 +414,5 @@ interface BlockRepository {
suspend fun fileSpaceUsage(): FileLimits
suspend fun setInternalFlags(command: Command.SetInternalFlags): Payload
suspend fun duplicateObjectsList(ids: List<Id>): List<Id>
suspend fun createTemplateFromObject(ctx: Id): Id
}

View file

@ -36,7 +36,7 @@ class CreateObject(
}
val command = Command.CreateObject(
template = null,
template = params.template,
prefilled = prefilled,
internalFlags = internalFlags
)
@ -52,7 +52,8 @@ class CreateObject(
}
data class Param(
val type: String?
val type: String?,
val template: Id? = null
)
data class Result(

View file

@ -0,0 +1,21 @@
package com.anytypeio.anytype.domain.templates
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import javax.inject.Inject
class CreateTemplateFromObject @Inject constructor(
private val repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<CreateTemplateFromObject.Params, Id>(
dispatchers.io
) {
override suspend fun doWork(params: Params): Id {
return repo.createTemplateFromObject(params.obj)
}
data class Params(val obj: Id)
}

View file

@ -800,4 +800,8 @@ class BlockMiddleware(
override suspend fun duplicateObjectsList(ids: List<Id>): List<Id> {
return middleware.duplicateObjectsList(ids)
}
override suspend fun createTemplateFromObject(ctx: Id): Id {
return middleware.createTemplateFromObject(ctx)
}
}

View file

@ -2244,6 +2244,19 @@ class Middleware @Inject constructor(
return response.ids
}
@Throws(Exception::class)
fun createTemplateFromObject(
ctx: Id
): Id {
val request = Rpc.Template.CreateFromObject.Request(
contextId = ctx
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.createTemplateFromObject(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.id
}
private fun logRequest(any: Any) {
logger.logRequest(any).also {
if (BuildConfig.DEBUG && threadInfo.isOnMainThread()) {

View file

@ -149,6 +149,9 @@ interface MiddlewareService {
@Throws(Exception::class)
fun objectsListDuplicate(request: Rpc.Object.ListDuplicate.Request): Rpc.Object.ListDuplicate.Response
@Throws(Exception::class)
fun createTemplateFromObject(request: Rpc.Template.CreateFromObject.Request): Rpc.Template.CreateFromObject.Response
//endregion
//region OBJECT'S RELATIONS command

View file

@ -1599,6 +1599,19 @@ class MiddlewareServiceImplementation @Inject constructor(
}
}
override fun createTemplateFromObject(request: Rpc.Template.CreateFromObject.Request): Rpc.Template.CreateFromObject.Response {
val encoded = Service.templateCreateFromObject(
Rpc.Template.CreateFromObject.Request.ADAPTER.encode(request)
)
val response = Rpc.Template.CreateFromObject.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.Template.CreateFromObject.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
override fun spaceUsage(request: Rpc.File.SpaceUsage.Request): Rpc.File.SpaceUsage.Response {
val encoded = Service.fileSpaceUsage(
Rpc.File.SpaceUsage.Request.ADAPTER.encode(request)

View file

@ -1,3 +1,4 @@
<resources>
<string name="app_name">Middleware</string>
<string name="snackbar_template_add">The template was added to the type</string>
</resources>

View file

@ -3143,23 +3143,32 @@ class EditorViewModel(
fun onAddNewDocumentClicked() {
Timber.d("onAddNewDocumentClicked, ")
proceedWithCreatingNewObject(type = null, template = null)
}
fun onCreateObjectWithTemplateClicked(template: Id) {
Timber.d("onCreateObjectWithTemplateClicked, template:[$template]")
val objType = getObjectTypeFromDetails() ?: return
proceedWithCreatingNewObject(type = objType, template = template)
}
private fun proceedWithCreatingNewObject(type: Id?, template: Id?) {
val startTime = System.currentTimeMillis()
jobs += viewModelScope.launch {
createObject.async(CreateObject.Param(type = null))
viewModelScope.launch {
val params = CreateObject.Param(type = type, template = template)
createObject.async(params = params)
.fold(
onSuccess = { result ->
sendAnalyticsObjectCreateEvent(
analytics = analytics,
type = result.type,
storeOfObjectTypes = storeOfObjectTypes,
route = EventsDictionary.Routes.navigation,
view = EventsDictionary.View.viewNavbar,
route = EventsDictionary.Routes.objPowerTool,
startTime = startTime
)
proceedWithOpeningObject(result.objectId)
},
onFailure = { e -> Timber.e(e, "Error while creating a new page") }
onFailure = { e -> Timber.e(e, "Error while creating a new object") }
)
}
}

View file

@ -10,6 +10,7 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary.changeSortValue
import com.anytypeio.anytype.analytics.base.EventsDictionary.changeViewType
import com.anytypeio.anytype.analytics.base.EventsDictionary.clickNewOption
import com.anytypeio.anytype.analytics.base.EventsDictionary.collectionScreenShow
import com.anytypeio.anytype.analytics.base.EventsDictionary.createTemplate
import com.anytypeio.anytype.analytics.base.EventsDictionary.duplicateView
import com.anytypeio.anytype.analytics.base.EventsDictionary.objectCreate
import com.anytypeio.anytype.analytics.base.EventsDictionary.objectDuplicate
@ -1730,4 +1731,23 @@ fun CoroutineScope.sendAnalyticsSelectTemplateEvent(
}
)
)
}
fun CoroutineScope.sendAnalyticsCreateTemplateEvent(
analytics: Analytics,
objectType: String?,
startTime: Long
) {
sendEvent(
analytics = analytics,
eventName = createTemplate,
props = Props(
buildMap {
put(EventsPropertiesKey.route, "MenuObject")
put(EventsPropertiesKey.objectType, objectType)
}
),
startTime = startTime,
middleTime = System.currentTimeMillis()
)
}

View file

@ -8,6 +8,7 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.domain.base.fold
@ -19,10 +20,15 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.`object`.DuplicateObject
import com.anytypeio.anytype.domain.objects.SetObjectIsArchived
import com.anytypeio.anytype.domain.page.AddBackLinkToObject
import com.anytypeio.anytype.domain.templates.CreateTemplateFromObject
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.extension.sendAnalyticsCreateTemplateEvent
import com.anytypeio.anytype.presentation.objects.ObjectAction
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.objects.getProperName
import com.anytypeio.anytype.presentation.objects.isTemplatesAllowed
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.downloader.DebugTreeShareDownloader
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
@ -43,7 +49,8 @@ class ObjectMenuViewModel(
private val storage: Editor.Storage,
private val analytics: Analytics,
private val updateFields: UpdateFields,
private val addObjectToCollection: AddObjectToCollection
private val addObjectToCollection: AddObjectToCollection,
private val createTemplateFromObject: CreateTemplateFromObject
) : ObjectMenuViewModelBase(
setObjectIsArchived = setObjectIsArchived,
addToFavorite = addToFavorite,
@ -86,6 +93,15 @@ class ObjectMenuViewModel(
if (!isProfile && !objectRestrictions.contains(ObjectRestriction.DUPLICATE)) {
add(ObjectAction.DUPLICATE)
}
val objTypeId = storage.details.current().details[ctx]?.type?.firstOrNull()
storage.details.current().details[objTypeId]?.let { objType ->
val objTypeWrapper = ObjectWrapper.Type(objType.map)
val isTemplateAllowed = objTypeWrapper.isTemplatesAllowed()
if (isTemplateAllowed && !isTemplate) {
add(ObjectAction.USE_AS_TEMPLATE)
}
}
if (!isTemplate) {
add(ObjectAction.LINK_TO)
}
@ -229,15 +245,47 @@ class ObjectMenuViewModel(
}
isDismissed.value = true
}
ObjectAction.USE_AS_TEMPLATE -> {
proceedWithCreatingTemplateFromObject(ctx)
}
ObjectAction.MOVE_TO,
ObjectAction.MOVE_TO_BIN,
ObjectAction.USE_AS_TEMPLATE,
ObjectAction.DELETE_FILES -> {
throw IllegalStateException("$action is unsupported")
}
}
}
private fun proceedWithCreatingTemplateFromObject(ctx: Id) {
val startTime = System.currentTimeMillis()
viewModelScope.launch {
val params = CreateTemplateFromObject.Params(obj = ctx)
createTemplateFromObject.async(params).fold(
onSuccess = { template ->
val objTypeId = storage.details.current().details[ctx]?.type?.firstOrNull()
sendAnalyticsCreateTemplateEvent(analytics, objTypeId, startTime)
commands.emit(buildOpenTemplateCommand(ctx, template))
isDismissed.value = true
},
onFailure = {
Timber.e(it, "Error while creating template from object")
_toasts.emit(SOMETHING_WENT_WRONG_MSG)
}
)
}
}
private fun buildOpenTemplateCommand(ctx: Id, template: Id): Command.OpenTemplate {
val type = storage.details.current().details[ctx]?.type?.firstOrNull()
val objType =
ObjectWrapper.Basic(storage.details.current().details[type]?.map ?: emptyMap())
return Command.OpenTemplate(
template = template,
icon = ObjectIcon.from(objType, objType.layout, urlBuilder),
typeName = objType.getProperName()
)
}
private fun proceedWithUpdatingLockStatus(
ctx: Id,
isLocked: Boolean
@ -307,7 +355,8 @@ class ObjectMenuViewModel(
private val updateFields: UpdateFields,
private val delegator: Delegator<Action>,
private val menuOptionsProvider: ObjectMenuOptionsProvider,
private val addObjectToCollection: AddObjectToCollection
private val addObjectToCollection: AddObjectToCollection,
private val createTemplateFromObject: CreateTemplateFromObject
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ObjectMenuViewModel(
@ -324,7 +373,8 @@ class ObjectMenuViewModel(
updateFields = updateFields,
delegator = delegator,
menuOptionsProvider = menuOptionsProvider,
addObjectToCollection = addObjectToCollection
addObjectToCollection = addObjectToCollection,
createTemplateFromObject = createTemplateFromObject
) as T
}
}

View file

@ -41,7 +41,7 @@ abstract class ObjectMenuViewModelBase(
private val removeFromFavorite: RemoveFromFavorite,
private val addBackLinkToObject: AddBackLinkToObject,
protected val delegator: Delegator<Action>,
private val urlBuilder: UrlBuilder,
protected val urlBuilder: UrlBuilder,
protected val dispatcher: Dispatcher<Payload>,
private val analytics: Analytics,
private val menuOptionsProvider: ObjectMenuOptionsProvider,
@ -340,6 +340,11 @@ abstract class ObjectMenuViewModelBase(
val icon: ObjectIcon,
val isCollection: Boolean = false
) : Command()
data class OpenTemplate(
val template: Id,
val typeName: String,
val icon: ObjectIcon
) : Command()
}
companion object {