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:
parent
a4a880fd76
commit
1ffcd74767
8 changed files with 206 additions and 89 deletions
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue