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

DROID-241 Editor | Simple tables, paste content into cells (#2471)

* DROID-241 update di

* DROID-241 send text update command on cell text change

* DROID-241 update adapter listeners

* DROID-241 paste and copy

* DROID-241 add isPartOfBlock param to paste command

* DROID-241 update di

* DROID-241 added payloads, update states

* DROID-241 todo

* DROID-241 di fix

* DROID-241 fix arguments for paste

* DROID-241 use dispatcher for paste use case

* DROID-241 update di

* DROID-241 argument update

* DROID-241 text update logic

* DROID-241 rename

Co-authored-by: konstantiniiv <ki@anytype.io>
This commit is contained in:
Konstantin Ivanov 2022-08-04 11:55:41 +02:00 committed by Evgenii Kozlov
parent a4a880fd76
commit 1ffcd74767
8 changed files with 206 additions and 89 deletions

View file

@ -1,9 +1,14 @@
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.PerDialog
import com.anytypeio.anytype.domain.block.interactor.UpdateText
import com.anytypeio.anytype.domain.clipboard.Copy
import com.anytypeio.anytype.domain.clipboard.Paste
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.objects.block.SetBlockTextValueViewModel
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.ui.editor.modals.SetBlockTextValueFragment
import dagger.Module
import dagger.Provides
@ -29,11 +34,19 @@ object SetBlockTextValueModule {
@Provides
@PerDialog
fun provideViewModelFactory(
storage: Editor.Storage,
dispatcher: Dispatcher<Payload>,
paste: Paste,
copy: Copy,
updateText: UpdateText,
storage: Editor.Storage
analytics: Analytics
): SetBlockTextValueViewModel.Factory =
SetBlockTextValueViewModel.Factory(
storage = storage,
updateText = updateText
dispatcher = dispatcher,
paste = paste,
copy = copy,
updateText = updateText,
analytics = analytics
)
}

View file

@ -16,7 +16,6 @@ import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_ui.features.editor.BlockAdapter
import com.anytypeio.anytype.core_ui.features.editor.DragAndDropAdapterDelegate
import com.anytypeio.anytype.core_ui.features.editor.marks
import com.anytypeio.anytype.core_ui.tools.ClipboardInterceptor
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
import com.anytypeio.anytype.core_utils.ext.argString
@ -28,7 +27,6 @@ import com.anytypeio.anytype.core_utils.ext.withParent
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetImeOffsetFragment
import com.anytypeio.anytype.databinding.FragmentSetBlockTextValueBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.ext.extractMarks
import com.anytypeio.anytype.presentation.objects.block.SetBlockTextValueViewModel
import com.anytypeio.anytype.ui.editor.OnFragmentInteractionListener
import java.util.*
@ -55,16 +53,7 @@ class SetBlockTextValueFragment :
onCheckboxClicked = {},
onTitleCheckboxClicked = {},
onFocusChanged = { _, _ -> },
onSplitLineEnterClicked = { id, editable, _ ->
vm.onKeyboardDoneKeyClicked(
ctx = ctx,
tableId = table,
targetId = id,
text = editable.toString(),
marks = editable.marks(),
markup = editable.extractMarks()
)
},
onSplitLineEnterClicked = { _, _, _ -> vm.onKeyboardDoneKeyClicked() },
onSplitDescription = { _, _, _ -> },
onEmptyBlockBackspaceClicked = {},
onNonEmptyBlockBackspaceClicked = { _, _ -> },
@ -74,7 +63,14 @@ class SetBlockTextValueFragment :
onTogglePlaceholderClicked = {},
onToggleClicked = {},
onTitleTextInputClicked = {},
onTextBlockTextChanged = {},
onTextBlockTextChanged = { block ->
vm.onTextBlockTextChanged(
textBlock = block,
cellId = this.block,
tableId = table,
ctx = ctx
)
},
onClickListener = vm::onClickListener,
onMentionEvent = {},
onSlashEvent = {},
@ -103,7 +99,7 @@ class SetBlockTextValueFragment :
jobs += subscribe(vm.state) { render(it) }
jobs += subscribe(vm.toasts) { toast(it) }
}
vm.onStart(tableId = table, blockId = block)
vm.onStart(tableId = table, cellId = block)
super.onStart()
}
@ -157,7 +153,22 @@ class SetBlockTextValueFragment :
return FragmentSetBlockTextValueBinding.inflate(inflater, container, false)
}
override fun onClipboardAction(action: ClipboardInterceptor.Action) {}
override fun onClipboardAction(action: ClipboardInterceptor.Action) {
when (action) {
is ClipboardInterceptor.Action.Copy -> vm.onCopy(
context = ctx,
range = action.selection,
cellId = block
)
is ClipboardInterceptor.Action.Paste -> vm.onPaste(
context = ctx,
range = action.selection,
cellId = block,
tableId = table
)
}
}
override fun onUrlPasted(url: Url) {}
override fun onDrag(v: View?, event: DragEvent?) = false

View file

@ -340,7 +340,8 @@ sealed class Command {
val range: IntRange,
val text: String,
val html: String?,
val blocks: List<Block>
val blocks: List<Block>,
val isPartOfBlock: Boolean? = null
)
/**

View file

@ -33,7 +33,8 @@ class Paste(
range = params.range,
text = clip.text,
html = clip.html,
blocks = blocks
blocks = blocks,
isPartOfBlock = params.isPartOfBlock
)
)
} else {
@ -51,7 +52,8 @@ class Paste(
val context: Id,
val focus: Id,
val range: IntRange,
val selected: List<Id>
val selected: List<Id>,
val isPartOfBlock: Boolean? = null
)
companion object {

View file

@ -625,7 +625,8 @@ class Middleware(
htmlSlot = command.html.orEmpty(),
selectedTextRange = range,
anySlot = blocks,
selectedBlockIds = command.selected
selectedBlockIds = command.selected,
isPartOfBlock = command.isPartOfBlock ?: false
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.blockPaste(request)

View file

@ -87,7 +87,8 @@ sealed class Intent {
val context: Id,
val focus: Id,
val selected: List<Id>,
val range: IntRange
val range: IntRange,
val isPartOfBlock: Boolean? = null
) : Clipboard()
class Copy(

View file

@ -494,7 +494,8 @@ class Orchestrator(
context = intent.context,
focus = intent.focus,
range = intent.range,
selected = intent.selected
selected = intent.selected,
isPartOfBlock = intent.isPartOfBlock
)
).proceed(
failure = defaultOnError,

View file

@ -3,58 +3,58 @@ package com.anytypeio.anytype.presentation.objects.block
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.Block
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.domain.block.interactor.UpdateText
import com.anytypeio.anytype.domain.clipboard.Copy
import com.anytypeio.anytype.domain.clipboard.Paste
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.editor.editor.Markup
import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.presentation.editor.editor.updateText
import com.anytypeio.anytype.presentation.editor.model.TextUpdate
import com.anytypeio.anytype.presentation.extension.sendAnalyticsCopyBlockEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsPasteBlockEvent
import com.anytypeio.anytype.presentation.mapper.mark
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import timber.log.Timber
class SetBlockTextValueViewModel(
private val updateText: UpdateText,
private val storage: Editor.Storage
private val storage: Editor.Storage,
private val dispatcher: Dispatcher<Payload>,
private val paste: Paste,
private val copy: Copy,
private val analytics: Analytics
) : BaseViewModel() {
private val doc: List<BlockView> get() = storage.views.current()
val state = MutableStateFlow<ViewState>(ViewState.Loading)
private val jobs = mutableListOf<Job>()
fun onStart(tableId: Id, blockId: Id) {
fun onStart(tableId: Id, cellId: Id) {
awaitTextBlockFromStorage(tableId = tableId, cellId = cellId)
}
private fun awaitTextBlockFromStorage(tableId: Id, cellId: Id) {
jobs += viewModelScope.launch {
storage.views.stream().mapNotNull { views ->
val table = views.firstOrNull { it.id == tableId }
if (table != null && table is BlockView.Table) {
val block = table.cells.firstOrNull { cell ->
when (cell) {
is BlockView.Table.Cell.Empty -> cell.getId() == blockId
is BlockView.Table.Cell.Text -> cell.getId() == blockId
BlockView.Table.Cell.Space -> false
}
}
if (block is BlockView.Table.Cell.Text) {
block.block.copy(
inputAction = BlockView.InputAction.Done,
isFocused = block.block.text.isEmpty()
)
} else {
null
}
} else {
null
val blockText = getCellTextBlockFromTable(views, tableId, cellId)
blockText?.copy(
inputAction = BlockView.InputAction.Done,
isFocused = blockText.text.isEmpty()
)
}.take(1)
.collectLatest {
state.value = ViewState.Success(data = listOf(it))
}
}.collectLatest { block ->
state.value = ViewState.Success(data = listOf(block))
}
}
}
@ -65,59 +65,64 @@ class SetBlockTextValueViewModel(
}
}
fun onKeyboardDoneKeyClicked(
ctx: Id,
tableId: String,
targetId: String,
text: String,
marks: List<Markup.Mark>,
markup: List<Block.Content.Text.Mark>
) {
viewModelScope.launch {
storage.views.update(doc.map { view ->
if (view.id == tableId && view is BlockView.Table) {
val updated = view.cells.map { it ->
if (it is BlockView.Table.Cell.Text && it.block.id == targetId) {
it.copy(block = it.block.copy(text = text, marks = marks))
} else {
it
}
fun onTextBlockTextChanged(textBlock: BlockView.Text, tableId: Id, cellId: Id, ctx: Id) {
Timber.d("onTextBlockTextChanged, textBlock:[$textBlock]")
val newViews = storage.views.current().map { view ->
if (view.id == tableId && view is BlockView.Table) {
val newCells = view.cells.map { cell ->
if (cell is BlockView.Table.Cell.Text && cell.block.id == textBlock.id) {
cell.copy(
block = cell.block.copy(
text = textBlock.text,
marks = textBlock.marks
)
)
} else {
cell
}
view.copy(cells = updated)
} else {
view
}
})
view.copy(cells = newCells)
} else {
view
}
}
val update = TextUpdate.Default(target = targetId, text = text, markup = markup)
val textUpdate = TextUpdate.Default(
target = cellId,
text = textBlock.text,
markup = textBlock.marks.map { it.mark() }
)
val updated = storage.document.get().map { block ->
if (block.id == update.target) {
block.updateText(update)
val newDocument = storage.document.get().map { block ->
if (block.id == textUpdate.target) {
block.updateText(textUpdate)
} else
block
}
storage.document.update(updated)
storage.document.update(newDocument)
viewModelScope.launch { storage.views.update(newViews) }
viewModelScope.launch {
updateText(
UpdateText.Params(
params = UpdateText.Params(
context = ctx,
target = targetId,
text = text,
marks = markup
target = textBlock.id,
text = textBlock.text,
marks = textBlock.marks.map { it.mark() }
)
).process(
failure = { e ->
Timber.e(e, "Error while updating block text value")
_toasts.emit("Error while updating block text value ${e.localizedMessage}")
},
success = { state.value = ViewState.Exit }
).proceed(
failure = { Timber.e(it) },
success = {}
)
}
}
fun onKeyboardDoneKeyClicked() {
state.value = ViewState.Exit
}
fun onClickListener(clicked: ListenerType) {
if (clicked is ListenerType.Mention) {
state.value = ViewState.OnMention(clicked.target)
@ -128,17 +133,99 @@ class SetBlockTextValueViewModel(
state.value = ViewState.Focus
}
fun onPaste(
context: Id,
range: IntRange,
cellId: Id,
tableId: Id
) {
Timber.d("onPaste, range:[$range]")
viewModelScope.launch {
paste(
params = Paste.Params(
context = context,
focus = cellId,
range = range,
selected = emptyList(),
isPartOfBlock = true
)
).proceed(
failure = { Timber.e(it) },
success = { response ->
dispatcher.send(response.payload)
awaitTextBlockFromStorage(tableId = tableId, cellId = cellId)
analytics.sendAnalyticsPasteBlockEvent()
}
)
}
}
fun onCopy(
context: Id,
range: IntRange?,
cellId: Id
) {
Timber.d("onCopy, range:[$range]")
val block = storage.document.get().firstOrNull { it.id == cellId }
if (block != null) {
viewModelScope.launch {
copy(
params = Copy.Params(
context = context,
range = range,
blocks = listOf(block)
)
).proceed(
failure = { Timber.e(it) },
success = { analytics.sendAnalyticsCopyBlockEvent() }
)
}
}
}
private fun getCellTextBlockFromTable(
views: List<BlockView>,
tableId: Id,
cellId: Id
): BlockView.Text.Paragraph? {
val table = views.firstOrNull { it.id == tableId }
return if (table != null && table is BlockView.Table) {
val block = table.cells.firstOrNull { cell ->
when (cell) {
is BlockView.Table.Cell.Empty -> cell.getId() == cellId
is BlockView.Table.Cell.Text -> cell.getId() == cellId
BlockView.Table.Cell.Space -> false
}
}
if (block is BlockView.Table.Cell.Text) {
block.block
} else {
null
}
} else {
null
}
}
class Factory(
private val storage: Editor.Storage,
private val dispatcher: Dispatcher<Payload>,
private val paste: Paste,
private val copy: Copy,
private val updateText: UpdateText,
private val storage: Editor.Storage
private val analytics: Analytics
) :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SetBlockTextValueViewModel(
storage = storage,
dispatcher = dispatcher,
paste = paste,
copy = copy,
updateText = updateText,
storage = storage
analytics = analytics
) as T
}
}
@ -148,6 +235,6 @@ class SetBlockTextValueViewModel(
data class OnMention(val targetId: String) : ViewState()
object Exit : ViewState()
object Loading : ViewState()
object Focus: ViewState()
object Focus : ViewState()
}
}