6081 lines
224 KiB
Kotlin
6081 lines
224 KiB
Kotlin
package com.anytypeio.anytype.presentation.editor
|
|
|
|
import android.net.Uri
|
|
import androidx.lifecycle.LiveData
|
|
import androidx.lifecycle.MutableLiveData
|
|
import androidx.lifecycle.viewModelScope
|
|
import com.anytypeio.anytype.analytics.base.Analytics
|
|
import com.anytypeio.anytype.analytics.base.EventsDictionary
|
|
import com.anytypeio.anytype.analytics.base.EventsDictionary.searchScreenShow
|
|
import com.anytypeio.anytype.analytics.base.EventsPropertiesKey
|
|
import com.anytypeio.anytype.analytics.base.sendEvent
|
|
import com.anytypeio.anytype.analytics.props.Props
|
|
import com.anytypeio.anytype.core_models.Block
|
|
import com.anytypeio.anytype.core_models.Block.Content
|
|
import com.anytypeio.anytype.core_models.Block.Prototype
|
|
import com.anytypeio.anytype.core_models.Document
|
|
import com.anytypeio.anytype.core_models.Event
|
|
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.core_models.Payload
|
|
import com.anytypeio.anytype.core_models.Position
|
|
import com.anytypeio.anytype.core_models.Relation
|
|
import com.anytypeio.anytype.core_models.SmartBlockType
|
|
import com.anytypeio.anytype.core_models.SyncStatus
|
|
import com.anytypeio.anytype.core_models.Url
|
|
import com.anytypeio.anytype.core_models.ext.addMention
|
|
import com.anytypeio.anytype.core_models.ext.asMap
|
|
import com.anytypeio.anytype.core_models.ext.content
|
|
import com.anytypeio.anytype.core_models.ext.descendants
|
|
import com.anytypeio.anytype.core_models.ext.isAllTextAndNoneCodeBlocks
|
|
import com.anytypeio.anytype.core_models.ext.isAllTextBlocks
|
|
import com.anytypeio.anytype.core_models.ext.parents
|
|
import com.anytypeio.anytype.core_models.ext.process
|
|
import com.anytypeio.anytype.core_models.ext.sortByType
|
|
import com.anytypeio.anytype.core_models.ext.supportNesting
|
|
import com.anytypeio.anytype.core_models.ext.textStyle
|
|
import com.anytypeio.anytype.core_models.ext.title
|
|
import com.anytypeio.anytype.core_models.ext.updateTextContent
|
|
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
|
|
import com.anytypeio.anytype.core_utils.common.EventWrapper
|
|
import com.anytypeio.anytype.core_utils.ext.Mimetype
|
|
import com.anytypeio.anytype.core_utils.ext.isEndLineClick
|
|
import com.anytypeio.anytype.core_utils.ext.replace
|
|
import com.anytypeio.anytype.core_utils.ext.switchToLatestFrom
|
|
import com.anytypeio.anytype.core_utils.ext.withLatestFrom
|
|
import com.anytypeio.anytype.core_utils.ui.ViewStateViewModel
|
|
import com.anytypeio.anytype.domain.`object`.ObjectTypesProvider
|
|
import com.anytypeio.anytype.domain.`object`.UpdateDetail
|
|
import com.anytypeio.anytype.domain.base.Result
|
|
import com.anytypeio.anytype.domain.block.interactor.RemoveLinkMark
|
|
import com.anytypeio.anytype.domain.block.interactor.UpdateLinkMarks
|
|
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet
|
|
import com.anytypeio.anytype.domain.clipboard.Paste.Companion.DEFAULT_RANGE
|
|
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
|
|
import com.anytypeio.anytype.domain.dataview.interactor.GetCompatibleObjectTypes
|
|
import com.anytypeio.anytype.domain.search.SearchObjects
|
|
import com.anytypeio.anytype.domain.editor.Editor
|
|
import com.anytypeio.anytype.domain.error.Error
|
|
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
|
|
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
|
|
import com.anytypeio.anytype.domain.icon.SetImageIcon
|
|
import com.anytypeio.anytype.domain.launch.GetDefaultEditorType
|
|
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
|
import com.anytypeio.anytype.domain.page.CloseBlock
|
|
import com.anytypeio.anytype.domain.page.CreateDocument
|
|
import com.anytypeio.anytype.domain.page.CreateNewDocument
|
|
import com.anytypeio.anytype.domain.page.CreateNewObject
|
|
import com.anytypeio.anytype.domain.page.CreateObject
|
|
import com.anytypeio.anytype.domain.page.OpenPage
|
|
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
|
|
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
|
|
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
|
|
import com.anytypeio.anytype.presentation.BuildConfig
|
|
import com.anytypeio.anytype.presentation.common.Action
|
|
import com.anytypeio.anytype.presentation.common.Delegator
|
|
import com.anytypeio.anytype.presentation.common.StateReducer
|
|
import com.anytypeio.anytype.presentation.common.SupportCommand
|
|
import com.anytypeio.anytype.presentation.editor.ControlPanelMachine.Interactor
|
|
import com.anytypeio.anytype.presentation.editor.Editor.Restore
|
|
import com.anytypeio.anytype.presentation.editor.editor.Command
|
|
import com.anytypeio.anytype.presentation.editor.editor.DetailModificationManager
|
|
import com.anytypeio.anytype.presentation.editor.editor.Intent
|
|
import com.anytypeio.anytype.presentation.editor.editor.Intent.Media
|
|
import com.anytypeio.anytype.presentation.editor.editor.KeyPressedEvent
|
|
import com.anytypeio.anytype.presentation.editor.editor.Markup
|
|
import com.anytypeio.anytype.presentation.editor.editor.Orchestrator
|
|
import com.anytypeio.anytype.presentation.editor.editor.Proxy
|
|
import com.anytypeio.anytype.presentation.editor.editor.SideEffect
|
|
import com.anytypeio.anytype.core_models.ThemeColor
|
|
import com.anytypeio.anytype.domain.`object`.ConvertObjectToSet
|
|
import com.anytypeio.anytype.presentation.editor.editor.ViewState
|
|
import com.anytypeio.anytype.presentation.editor.editor.actions.ActionItemType
|
|
import com.anytypeio.anytype.presentation.editor.editor.control.ControlPanelState
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.clearSearchHighlights
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.cutPartOfText
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.enterSAM
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.fillTableOfContents
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.highlight
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.isStyleClearable
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.nextSearchTarget
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.previousSearchTarget
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.singleStylingMode
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.toEditMode
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.toReadMode
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.update
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.updateCursorAndEditMode
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.updateSelection
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.applyBordersToSelectedCells
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.removeBordersFromCells
|
|
import com.anytypeio.anytype.presentation.editor.editor.ext.updateTableOfContentsViews
|
|
import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType
|
|
import com.anytypeio.anytype.presentation.editor.editor.markup
|
|
import com.anytypeio.anytype.presentation.editor.editor.mention.MentionConst.MENTION_PREFIX
|
|
import com.anytypeio.anytype.presentation.editor.editor.mention.MentionConst.MENTION_TITLE_EMPTY
|
|
import com.anytypeio.anytype.presentation.editor.editor.mention.MentionEvent
|
|
import com.anytypeio.anytype.presentation.editor.editor.mention.getMentionName
|
|
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
|
|
import com.anytypeio.anytype.presentation.editor.editor.model.Focusable
|
|
import com.anytypeio.anytype.presentation.editor.editor.model.UiBlock
|
|
import com.anytypeio.anytype.presentation.editor.editor.sam.ScrollAndMoveTargetDescriptor.Companion.END_RANGE
|
|
import com.anytypeio.anytype.presentation.editor.editor.sam.ScrollAndMoveTargetDescriptor.Companion.INNER_RANGE
|
|
import com.anytypeio.anytype.presentation.editor.editor.sam.ScrollAndMoveTargetDescriptor.Companion.START_RANGE
|
|
import com.anytypeio.anytype.presentation.editor.editor.search.SearchInDocEvent
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashExtensions
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashExtensions.SLASH_CHAR
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashExtensions.SLASH_EMPTY_SEARCH_MAX
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashExtensions.getSlashWidgetAlignmentItems
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashExtensions.getSlashWidgetStyleItems
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashItem
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashRelationView
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashWidgetState
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.convertToMarkType
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.convertToUiBlock
|
|
import com.anytypeio.anytype.presentation.editor.editor.slash.toSlashItemView
|
|
import com.anytypeio.anytype.presentation.editor.editor.styling.StylingEvent
|
|
import com.anytypeio.anytype.presentation.editor.editor.styling.getIds
|
|
import com.anytypeio.anytype.presentation.editor.editor.styling.getStyleBackgroundToolbarState
|
|
import com.anytypeio.anytype.presentation.editor.editor.styling.getStyleColorBackgroundToolbarState
|
|
import com.anytypeio.anytype.presentation.editor.editor.styling.getStyleOtherToolbarState
|
|
import com.anytypeio.anytype.presentation.editor.editor.styling.getStyleTextToolbarState
|
|
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableDelegate
|
|
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableWidgetEvent
|
|
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableWidgetState
|
|
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableWidgetViewState
|
|
import com.anytypeio.anytype.presentation.editor.editor.toCoreModel
|
|
import com.anytypeio.anytype.presentation.editor.editor.updateText
|
|
import com.anytypeio.anytype.presentation.editor.model.EditorFooter
|
|
import com.anytypeio.anytype.presentation.editor.model.TextUpdate
|
|
import com.anytypeio.anytype.presentation.editor.picker.PickerListener
|
|
import com.anytypeio.anytype.presentation.editor.render.BlockViewRenderer
|
|
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
|
|
import com.anytypeio.anytype.presentation.editor.search.search
|
|
import com.anytypeio.anytype.presentation.editor.selection.SelectionStateHolder
|
|
import com.anytypeio.anytype.presentation.editor.template.EditorTemplateDelegate
|
|
import com.anytypeio.anytype.presentation.editor.template.SelectTemplateEvent
|
|
import com.anytypeio.anytype.presentation.editor.template.SelectTemplateState
|
|
import com.anytypeio.anytype.presentation.editor.template.SelectTemplateViewState
|
|
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsBlockActionEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsBlockAlignEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsBlockBackgroundEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsBlockReorder
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsBookmarkOpen
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsGoBackEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsMentionMenuEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectShowEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectTypeChangeEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOpenAsObject
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsRelationValueEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSearchQueryEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSearchResultEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSearchWordsEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSelectionMenuEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSetDescriptionEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSetTitleEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSlashMenuEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsStyleMenuEvent
|
|
import com.anytypeio.anytype.presentation.extension.sendAnalyticsUpdateTextMarkupEvent
|
|
import com.anytypeio.anytype.presentation.mapper.mark
|
|
import com.anytypeio.anytype.presentation.mapper.style
|
|
import com.anytypeio.anytype.presentation.mapper.toObjectTypeView
|
|
import com.anytypeio.anytype.presentation.navigation.AppNavigation
|
|
import com.anytypeio.anytype.presentation.navigation.DefaultObjectView
|
|
import com.anytypeio.anytype.presentation.navigation.SupportNavigation
|
|
import com.anytypeio.anytype.presentation.objects.ObjectTypeView
|
|
import com.anytypeio.anytype.presentation.objects.SupportedLayouts
|
|
import com.anytypeio.anytype.presentation.objects.toView
|
|
import com.anytypeio.anytype.presentation.relations.DocumentRelationView
|
|
import com.anytypeio.anytype.presentation.relations.views
|
|
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
|
|
import com.anytypeio.anytype.presentation.search.ObjectSearchViewModel
|
|
import com.anytypeio.anytype.presentation.util.CopyFileStatus
|
|
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
|
|
import com.anytypeio.anytype.presentation.util.Dispatcher
|
|
import com.anytypeio.anytype.presentation.util.OnCopyFileToCacheAction
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.SharedFlow
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
import kotlinx.coroutines.flow.catch
|
|
import kotlinx.coroutines.flow.debounce
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
import kotlinx.coroutines.flow.filter
|
|
import kotlinx.coroutines.flow.filterNotNull
|
|
import kotlinx.coroutines.flow.launchIn
|
|
import kotlinx.coroutines.flow.map
|
|
import kotlinx.coroutines.flow.onEach
|
|
import kotlinx.coroutines.launch
|
|
import timber.log.Timber
|
|
import java.util.*
|
|
import java.util.regex.Pattern
|
|
import com.anytypeio.anytype.presentation.editor.Editor.Mode as EditorMode
|
|
|
|
class EditorViewModel(
|
|
private val openPage: OpenPage,
|
|
private val closePage: CloseBlock,
|
|
private val createDocument: CreateDocument,
|
|
private val createObject: CreateObject,
|
|
private val createNewDocument: CreateNewDocument,
|
|
private val interceptEvents: InterceptEvents,
|
|
private val interceptThreadStatus: InterceptThreadStatus,
|
|
private val updateLinkMarks: UpdateLinkMarks,
|
|
private val removeLinkMark: RemoveLinkMark,
|
|
private val reducer: StateReducer<List<Block>, Event>,
|
|
private val urlBuilder: UrlBuilder,
|
|
private val renderer: DefaultBlockViewRenderer,
|
|
private val orchestrator: Orchestrator,
|
|
private val analytics: Analytics,
|
|
private val dispatcher: Dispatcher<Payload>,
|
|
private val delegator: Delegator<Action>,
|
|
private val detailModificationManager: DetailModificationManager,
|
|
private val updateDetail: UpdateDetail,
|
|
private val getCompatibleObjectTypes: GetCompatibleObjectTypes,
|
|
private val objectTypesProvider: ObjectTypesProvider,
|
|
private val searchObjects: SearchObjects,
|
|
private val getDefaultEditorType: GetDefaultEditorType,
|
|
private val findObjectSetForType: FindObjectSetForType,
|
|
private val createObjectSet: CreateObjectSet,
|
|
private val copyFileToCache: CopyFileToCacheDirectory,
|
|
private val downloadUnsplashImage: DownloadUnsplashImage,
|
|
private val setDocCoverImage: SetDocCoverImage,
|
|
private val setDocImageIcon: SetDocumentImageIcon,
|
|
private val templateDelegate: EditorTemplateDelegate,
|
|
private val simpleTableDelegate: SimpleTableDelegate,
|
|
private val createNewObject: CreateNewObject,
|
|
private val objectToSet: ConvertObjectToSet
|
|
) : ViewStateViewModel<ViewState>(),
|
|
PickerListener,
|
|
SupportNavigation<EventWrapper<AppNavigation.Command>>,
|
|
SupportCommand<Command>,
|
|
BlockViewRenderer by renderer,
|
|
ToggleStateHolder by renderer,
|
|
SelectionStateHolder by orchestrator.memory.selections,
|
|
EditorTemplateDelegate by templateDelegate,
|
|
SimpleTableDelegate by simpleTableDelegate,
|
|
StateReducer<List<Block>, Event> by reducer {
|
|
|
|
val actions = MutableStateFlow(ActionItemType.defaultSorting)
|
|
|
|
val isSyncStatusVisible = MutableStateFlow(true)
|
|
val syncStatus = MutableStateFlow<SyncStatus?>(null)
|
|
|
|
val isUndoEnabled = MutableStateFlow(false)
|
|
val isRedoEnabled = MutableStateFlow(false)
|
|
val isUndoRedoToolbarIsVisible = MutableStateFlow(false)
|
|
|
|
val selectTemplateViewState = templateDelegateState.map { state ->
|
|
when (state) {
|
|
is SelectTemplateState.Available -> {
|
|
SelectTemplateViewState.Active(
|
|
count = state.templates.size
|
|
)
|
|
}
|
|
else -> SelectTemplateViewState.Idle
|
|
}
|
|
}
|
|
|
|
val simpleTablesViewState = simpleTableDelegateState.map { state ->
|
|
when (state) {
|
|
is SimpleTableWidgetState.UpdateItems -> {
|
|
SimpleTableWidgetViewState.Active(
|
|
state = state
|
|
)
|
|
}
|
|
SimpleTableWidgetState.Idle -> SimpleTableWidgetViewState.Idle
|
|
}
|
|
}
|
|
|
|
val searchResultScrollPosition = MutableStateFlow(NO_SEARCH_RESULT_POSITION)
|
|
|
|
private val session = MutableStateFlow(Session.IDLE)
|
|
|
|
val views: List<BlockView> get() = orchestrator.stores.views.current()
|
|
|
|
val pending: Queue<Restore> = LinkedList()
|
|
val restore: Queue<Restore> = LinkedList()
|
|
|
|
private val jobs = mutableListOf<Job>()
|
|
|
|
var mode: EditorMode = EditorMode.Edit
|
|
|
|
val footers = MutableStateFlow<EditorFooter>(EditorFooter.None)
|
|
|
|
private val controlPanelInteractor = Interactor(viewModelScope)
|
|
val controlPanelViewState = MutableLiveData<ControlPanelState>()
|
|
|
|
/**
|
|
* Sends renderized document to UI
|
|
*/
|
|
private val renderCommand = Proxy.Subject<Unit>()
|
|
|
|
/**
|
|
* Renderizes document, create views from it, dispatches them to [renderCommand]
|
|
*/
|
|
private val renderizePipeline = Proxy.Subject<Document>()
|
|
|
|
private val markupActionPipeline = Proxy.Subject<MarkupAction>()
|
|
|
|
/**
|
|
* Currently opened document id.
|
|
*/
|
|
var context: String = EMPTY_CONTEXT
|
|
|
|
/**
|
|
* Current document
|
|
*/
|
|
val blocks: Document get() = orchestrator.stores.document.get()
|
|
|
|
private val _focus: MutableLiveData<Id> = MutableLiveData()
|
|
val focus: LiveData<Id> = _focus
|
|
|
|
private val _toasts = MutableSharedFlow<String>()
|
|
val toasts: SharedFlow<String> = _toasts
|
|
|
|
val snacks = MutableSharedFlow<Snack>(replay = 0)
|
|
|
|
/**
|
|
* Open gallery and search media files for block with that id
|
|
*/
|
|
var currentMediaUploadDescription: Media.Upload.Description? = null
|
|
private set
|
|
|
|
private var analyticsContext: String? = null
|
|
|
|
override val navigation = MutableLiveData<EventWrapper<AppNavigation.Command>>()
|
|
override val commands = MutableLiveData<EventWrapper<Command>>()
|
|
|
|
init {
|
|
startHandlingTextChanges()
|
|
startProcessingFocusChanges()
|
|
startProcessingControlPanelViewState()
|
|
startProcessingInternalDetailModifications()
|
|
startObservingPayload()
|
|
startObservingErrors()
|
|
processRendering()
|
|
processMarkupChanges()
|
|
viewModelScope.launch { orchestrator.start() }
|
|
|
|
viewModelScope.launch {
|
|
delegator.receive().collect { action ->
|
|
when (action) {
|
|
is Action.SetUnsplashImage -> {
|
|
proceedWithSettingUnsplashImage(action)
|
|
}
|
|
is Action.Duplicate -> proceedWithOpeningPage(action.id)
|
|
Action.SearchOnPage -> onEnterSearchModeClicked()
|
|
Action.UndoRedo -> onUndoRedoActionClicked()
|
|
}
|
|
}
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
templateDelegateState.collect { state ->
|
|
Timber.v("Template delegate state: $state")
|
|
when (state) {
|
|
is SelectTemplateState.Accepted -> {
|
|
commands.postValue(EventWrapper(Command.CloseKeyboard))
|
|
navigate(
|
|
EventWrapper(
|
|
AppNavigation.Command.OpenTemplates(
|
|
type = state.type,
|
|
templates = state.templates,
|
|
ctx = context
|
|
)
|
|
)
|
|
)
|
|
}
|
|
is SelectTemplateState.Available -> {}
|
|
SelectTemplateState.Idle -> {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onPickedDocImageFromDevice(ctx: Id, path: String) {
|
|
viewModelScope.launch {
|
|
setDocImageIcon(
|
|
SetImageIcon.Params(
|
|
target = ctx,
|
|
path = path
|
|
)
|
|
).process(
|
|
failure = {
|
|
sendToast("Can't update object icon image")
|
|
Timber.e(it, "Error while setting image icon")
|
|
},
|
|
success = { (payload, _) ->
|
|
dispatcher.send(payload)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private suspend fun proceedWithSettingUnsplashImage(
|
|
action: Action.SetUnsplashImage
|
|
) {
|
|
downloadUnsplashImage(
|
|
DownloadUnsplashImage.Params(
|
|
picture = action.img
|
|
)
|
|
).process(
|
|
failure = {
|
|
Timber.e(it, "Error while download unsplash image")
|
|
},
|
|
success = { hash ->
|
|
setDocCoverImage(
|
|
SetDocCoverImage.Params.FromHash(
|
|
context = context,
|
|
hash = hash
|
|
)
|
|
).process(
|
|
failure = {
|
|
Timber.e(it, "Error while setting unsplash image")
|
|
},
|
|
success = { payload -> dispatcher.send(payload) }
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
private fun startProcessingInternalDetailModifications() {
|
|
detailModificationManager.modifications.onEach { refresh() }.launchIn(viewModelScope)
|
|
}
|
|
|
|
private fun startProcessingFocusChanges() {
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.stream().collect { focus ->
|
|
if (focus.isEmpty) {
|
|
orchestrator.stores.textSelection.update(Editor.TextSelection.empty())
|
|
} else {
|
|
if (!focus.isPending) {
|
|
try {
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.OnFocusChanged(
|
|
id = focus.id,
|
|
style = if (focus.id == context)
|
|
Content.Text.Style.TITLE
|
|
else
|
|
blocks.first { it.id == focus.id }.textStyle()
|
|
)
|
|
)
|
|
} catch (e: NoSuchElementException) {
|
|
Timber.e(e, "Could not found focused block. Doc size: ${blocks.size}")
|
|
}
|
|
}
|
|
}
|
|
_focus.postValue(focus.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun startObservingPayload() {
|
|
viewModelScope.launch {
|
|
orchestrator
|
|
.proxies
|
|
.payloads
|
|
.stream()
|
|
.filter { it.events.isNotEmpty() }
|
|
.map { payload -> processEvents(payload.events) }
|
|
.collect { flags ->
|
|
if (flags.contains(Flags.FLAG_REFRESH))
|
|
refresh()
|
|
else {
|
|
Timber.d("----------Refresh skipped----------")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun startObservingErrors() {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.errors
|
|
.stream()
|
|
.collect { sendToast(it.message ?: "Unknown error") }
|
|
}
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.toasts
|
|
.stream()
|
|
.collect { sendToast(it) }
|
|
}
|
|
}
|
|
|
|
private suspend fun processEvents(events: List<Event>): List<Flag> {
|
|
if (BuildConfig.DEBUG) {
|
|
Timber.d("Blocks before handling events: $blocks")
|
|
Timber.d("Events: $events")
|
|
}
|
|
events.forEach { event ->
|
|
when (event) {
|
|
is Event.Command.ShowObject -> {
|
|
orchestrator.stores.details.update(event.details)
|
|
orchestrator.stores.relations.update(event.relations)
|
|
orchestrator.stores.objectTypes.update(event.objectTypes)
|
|
orchestrator.stores.objectRestrictions.update(event.objectRestrictions)
|
|
val objectType = event.details.details[context]?.type?.firstOrNull()
|
|
proceedWithShowingObjectTypesWidget(objectType, event.blocks)
|
|
}
|
|
is Event.Command.Details -> {
|
|
orchestrator.stores.details.apply { update(current().process(event)) }
|
|
}
|
|
is Event.Command.ObjectRelations -> {
|
|
orchestrator.stores.relations.apply { update(current().process(event)) }
|
|
}
|
|
else -> {
|
|
// do nothing
|
|
}
|
|
}
|
|
orchestrator.stores.document.update(reduce(blocks, event))
|
|
}
|
|
if (BuildConfig.DEBUG) {
|
|
Timber.d("Blocks after handling events: $blocks")
|
|
}
|
|
return events.flags(context)
|
|
}
|
|
|
|
private fun startProcessingControlPanelViewState() {
|
|
viewModelScope.launch {
|
|
controlPanelInteractor
|
|
.state()
|
|
.distinctUntilChanged()
|
|
.collect { controlPanelViewState.postValue(it) }
|
|
}
|
|
}
|
|
|
|
private fun processMarkupChanges() {
|
|
markupActionPipeline
|
|
.stream()
|
|
.withLatestFrom(
|
|
orchestrator.stores.textSelection
|
|
.stream()
|
|
.distinctUntilChanged()
|
|
)
|
|
{ a, b -> Pair(a, b) }
|
|
.onEach { (action, textSelection) ->
|
|
val range = textSelection.selection
|
|
if (textSelection.isNotEmpty && range != null && range.first != range.last) {
|
|
applyMarkup(
|
|
selection = Pair(textSelection.id, range),
|
|
action = action
|
|
)
|
|
}
|
|
}
|
|
.launchIn(viewModelScope)
|
|
}
|
|
|
|
private fun applyLinkMarkup(
|
|
blockId: String, link: String, range: IntRange
|
|
) {
|
|
val targetBlock = blocks.first { it.id == blockId }
|
|
val targetContent = targetBlock.content as Content.Text
|
|
val linkMark = Content.Text.Mark(
|
|
type = Content.Text.Mark.Type.LINK,
|
|
range = IntRange(start = range.first, endInclusive = range.last.inc()),
|
|
param = link
|
|
)
|
|
val marks = targetContent.marks
|
|
|
|
updateLinkMarks(
|
|
scope = viewModelScope,
|
|
params = UpdateLinkMarks.Params(
|
|
marks = marks,
|
|
newMark = linkMark
|
|
),
|
|
onResult = { result ->
|
|
result.either(
|
|
fnL = { throwable ->
|
|
Timber.e("Error update marks:${throwable.message}")
|
|
},
|
|
fnR = { marks ->
|
|
val sortedMarks = marks.sortByType()
|
|
val newContent = targetContent.copy(marks = sortedMarks)
|
|
val newBlock = targetBlock.copy(content = newContent)
|
|
rerenderingBlocks(newBlock)
|
|
proceedWithUpdatingText(
|
|
intent = Intent.Text.UpdateText(
|
|
context = context,
|
|
text = newBlock.content.asText().text,
|
|
target = targetBlock.id,
|
|
marks = sortedMarks
|
|
)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
private suspend fun applyMarkup(
|
|
selection: Pair<String, IntRange>,
|
|
action: MarkupAction
|
|
) {
|
|
val target = blocks.first { block -> block.id == selection.first }
|
|
|
|
val new = target.markup(
|
|
type = action.type,
|
|
param = action.param,
|
|
range = selection.second
|
|
)
|
|
|
|
val update = blocks.map { block ->
|
|
if (block.id != target.id)
|
|
block
|
|
else
|
|
new
|
|
}
|
|
orchestrator.stores.document.update(update)
|
|
|
|
refresh()
|
|
|
|
proceedWithUpdatingText(
|
|
intent = Intent.Text.UpdateText(
|
|
context = context,
|
|
target = new.id,
|
|
text = new.content<Content.Text>().text,
|
|
marks = new.content<Content.Text>().marks
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun rerenderingBlocks(block: Block) =
|
|
viewModelScope.launch {
|
|
val update = blocks.map {
|
|
if (it.id != block.id)
|
|
it
|
|
else
|
|
block
|
|
}
|
|
orchestrator.stores.document.update(update)
|
|
refresh()
|
|
}
|
|
|
|
private fun processRendering() {
|
|
|
|
// stream to UI
|
|
|
|
renderCommand
|
|
.stream()
|
|
.switchToLatestFrom(orchestrator.stores.views.stream())
|
|
.onEach { dispatchToUI(it) }
|
|
.launchIn(viewModelScope)
|
|
|
|
// renderize, in order to send to UI
|
|
|
|
renderizePipeline
|
|
.stream()
|
|
.filter { it.isNotEmpty() }
|
|
.onEach { document -> refreshStyleToolbar(document) }
|
|
.withLatestFrom(
|
|
orchestrator.stores.focus.stream(),
|
|
orchestrator.stores.details.stream()
|
|
) { models, focus, details ->
|
|
val root = models.first { it.id == context }
|
|
if (mode == EditorMode.Locked) {
|
|
if (root.fields.isLocked != true) {
|
|
mode = EditorMode.Edit
|
|
sendToast("Your object is unlocked")
|
|
}
|
|
} else {
|
|
if (root.fields.isLocked == true) {
|
|
mode = EditorMode.Locked
|
|
sendToast("Your object is locked")
|
|
}
|
|
}
|
|
footers.value = getFooterState(root, details)
|
|
val flags = mutableListOf<BlockViewRenderer.RenderFlag>()
|
|
val doc = models.asMap().render(
|
|
mode = mode,
|
|
root = root,
|
|
focus = focus,
|
|
anchor = context,
|
|
indent = INITIAL_INDENT,
|
|
details = details,
|
|
relations = orchestrator.stores.relations.current(),
|
|
restrictions = orchestrator.stores.objectRestrictions.current(),
|
|
selection = currentSelection(),
|
|
objectTypes = orchestrator.stores.objectTypes.current()
|
|
) { onRenderFlagFound -> flags.add(onRenderFlagFound) }
|
|
if (flags.isNotEmpty()) {
|
|
doc.fillTableOfContents()
|
|
} else {
|
|
doc
|
|
}
|
|
}
|
|
.catch { error ->
|
|
Timber.e(error, "Get error in renderizePipeline")
|
|
emit(emptyList())
|
|
}
|
|
.onEach { views ->
|
|
orchestrator.stores.views.update(views)
|
|
renderCommand.send(Unit)
|
|
}
|
|
.launchIn(viewModelScope)
|
|
}
|
|
|
|
private fun refreshStyleToolbar(document: Document) {
|
|
controlPanelViewState.value?.let { state ->
|
|
if (state.styleTextToolbar.isVisible) {
|
|
val ids = mode.getIds()
|
|
if (ids.isNullOrEmpty()) return
|
|
onSendRefreshStyleTextToolbarEvent(ids)
|
|
}
|
|
if (state.styleBackgroundToolbar.isVisible) {
|
|
val ids = mode.getIds()
|
|
if (ids.isNullOrEmpty()) return
|
|
onSendRefreshStyleBackgroundToolbarEvent(ids)
|
|
}
|
|
if (state.markupMainToolbar.isVisible) {
|
|
controlPanelInteractor.onEvent(
|
|
event = ControlPanelMachine.Event.OnRefresh.Markup(
|
|
target = document.find { block -> block.id == orchestrator.stores.focus.current().id },
|
|
selection = orchestrator.stores.textSelection.current().selection
|
|
)
|
|
)
|
|
}
|
|
if (state.styleColorBackgroundToolbar.isVisible) {
|
|
val ids = mode.getIds()
|
|
if (ids.isNullOrEmpty()) return
|
|
onSendUpdateStyleColorBackgroundToolbarEvent(
|
|
ids,
|
|
state.styleColorBackgroundToolbar.navigatedFromStylingTextToolbar
|
|
)
|
|
}
|
|
if (state.styleExtraToolbar.isVisible) {
|
|
val ids = mode.getIds()
|
|
if (ids.isNullOrEmpty()) return
|
|
onSendUpdateStyleOtherToolbarEvent(ids)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun onSendRefreshStyleTextToolbarEvent(ids: List<Id>) {
|
|
val selected = blocks.filter { ids.contains(it.id) }
|
|
val isAllSelectedText = selected.isAllTextBlocks()
|
|
if (isAllSelectedText) {
|
|
val state = selected.map { it.content.asText() }.getStyleTextToolbarState()
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnUpdateTextToolbar(state)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun onSendRefreshStyleBackgroundToolbarEvent(ids: List<Id>) {
|
|
val selected = blocks.filter { ids.contains(it.id) }
|
|
val state = selected.getStyleBackgroundToolbarState()
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnUpdateBackgroundToolbar(state)
|
|
)
|
|
}
|
|
|
|
private fun onSendUpdateStyleColorBackgroundToolbarEvent(
|
|
ids: List<Id>,
|
|
navigateFromStylingTextToolbar: Boolean,
|
|
) {
|
|
val selected = blocks.filter { ids.contains(it.id) }
|
|
val state = selected.getStyleColorBackgroundToolbarState()
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnUpdateColorBackgroundToolbar(
|
|
state,
|
|
navigateFromStylingTextToolbar
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun onSendUpdateStyleOtherToolbarEvent(ids: List<Id>) {
|
|
val selected = blocks.filter { ids.contains(it.id) }
|
|
val state = selected.map { it.content.asText() }.getStyleOtherToolbarState()
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnUpdateOtherToolbar(state)
|
|
)
|
|
}
|
|
|
|
private fun dispatchToUI(views: List<BlockView>) {
|
|
stateData.postValue(
|
|
ViewState.Success(
|
|
blocks = views
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun startHandlingTextChanges() {
|
|
orchestrator
|
|
.proxies
|
|
.changes
|
|
.stream()
|
|
.filterNotNull()
|
|
.onEach { update -> orchestrator.textInteractor.consume(update, context) }
|
|
.launchIn(viewModelScope)
|
|
|
|
orchestrator
|
|
.proxies
|
|
.saves
|
|
.stream()
|
|
.filterNotNull()
|
|
.onEach { update ->
|
|
val updated = blocks.map { block ->
|
|
if (block.id == update.target) {
|
|
block.updateText(update)
|
|
} else
|
|
block
|
|
}
|
|
orchestrator.stores.document.update(updated)
|
|
}
|
|
.map { update ->
|
|
Intent.Text.UpdateText(
|
|
context = context,
|
|
target = update.target,
|
|
text = update.text,
|
|
marks = update.markup.filter { it.range.first != it.range.last }
|
|
)
|
|
}
|
|
.onEach { params ->
|
|
proceedWithUpdatingText(params)
|
|
}
|
|
.launchIn(viewModelScope)
|
|
}
|
|
|
|
private fun proceedWithUpdatingText(intent: Intent.Text.UpdateText) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(intent)
|
|
}
|
|
}
|
|
|
|
fun onStart(id: Id) {
|
|
Timber.d("onStart, id:[$id]")
|
|
|
|
context = id
|
|
|
|
stateData.postValue(ViewState.Loading)
|
|
|
|
jobs += viewModelScope.launch {
|
|
interceptEvents
|
|
.build(InterceptEvents.Params(context))
|
|
.map { events -> processEvents(events) }
|
|
.collect { flags ->
|
|
if (flags.contains(Flags.FLAG_REFRESH))
|
|
refresh()
|
|
else
|
|
Timber.d("----------Refresh skipped----------")
|
|
}
|
|
}
|
|
|
|
jobs += viewModelScope.launch {
|
|
interceptThreadStatus
|
|
.build(InterceptThreadStatus.Params(context))
|
|
.collect { syncStatus.value = it }
|
|
}
|
|
|
|
jobs += viewModelScope.launch {
|
|
dispatcher
|
|
.flow()
|
|
.filter { it.context == context }
|
|
.collect { orchestrator.proxies.payloads.send(it) }
|
|
}
|
|
val startTime = System.currentTimeMillis()
|
|
viewModelScope.launch {
|
|
openPage(OpenPage.Params(id)).proceed(
|
|
success = { result ->
|
|
when (result) {
|
|
is Result.Success -> {
|
|
val middleTime = System.currentTimeMillis()
|
|
session.value = Session.OPEN
|
|
onStartFocusing(result.data)
|
|
orchestrator.proxies.payloads.send(result.data)
|
|
// Temporarily hiding sync status for file objects.
|
|
// TODO Remove when sync status for files is ready.
|
|
result.data.events.forEach { event ->
|
|
if (event is Event.Command.ShowObject) {
|
|
if (event.type == SmartBlockType.FILE) {
|
|
isSyncStatusVisible.value = false
|
|
}
|
|
val block = event.blocks.firstOrNull { it.id == context }
|
|
analyticsContext = block?.fields?.analyticsContext
|
|
sendAnalyticsObjectShowEvent(
|
|
analytics = analytics,
|
|
startTime = startTime,
|
|
middleTime = middleTime,
|
|
type = event.details.details[context]?.type?.firstOrNull(),
|
|
layoutCode = event.details.details[context]?.layout,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
}
|
|
}
|
|
is Result.Failure -> {
|
|
session.value = Session.ERROR
|
|
when (result.error) {
|
|
Error.BackwardCompatibility -> dispatch(Command.AlertDialog)
|
|
Error.NotFoundObject -> {
|
|
stateData.postValue(ViewState.NotExist)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
failure = {
|
|
session.value = Session.ERROR
|
|
Timber.e(it, "Error while opening page with id: $id")
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
//TODO need refactoring, logic must depend on Object Layouts
|
|
private fun onStartFocusing(payload: Payload) {
|
|
val event = payload.events.find { it is Event.Command.ShowObject }
|
|
if (event is Event.Command.ShowObject) {
|
|
val root = event.blocks.find { it.id == context }
|
|
when {
|
|
root == null -> Timber.e("Could not find the root block on initial focusing")
|
|
root.fields.isLocked == true -> {
|
|
mode = EditorMode.Locked
|
|
}
|
|
root.children.size == 1 -> {
|
|
val first = event.blocks.first { it.id == root.children.first() }
|
|
val content = first.content
|
|
if (content is Content.Layout && content.type == Content.Layout.Type.HEADER) {
|
|
try {
|
|
val title = event.blocks.title()
|
|
if (title != null && title.content<Content.Text>().text.isEmpty()) {
|
|
val focus = Editor.Focus(id = title.id, cursor = Editor.Cursor.End)
|
|
viewModelScope.launch { orchestrator.stores.focus.update(focus) }
|
|
} else {
|
|
Timber.d("Skipping initial focusing. Title is not empty or is null")
|
|
}
|
|
} catch (e: Throwable) {
|
|
Timber.e(e, "Error while initial focusing")
|
|
}
|
|
}
|
|
}
|
|
root.children.size == 2 -> {
|
|
val layout = event.details.details[root.id]?.layout
|
|
if (layout == ObjectType.Layout.NOTE.code.toDouble()) {
|
|
val block = event.blocks.firstOrNull { it.content is Content.Text }
|
|
if (block != null && block.content<Content.Text>().text.isEmpty()) {
|
|
val focus = Editor.Focus(id = block.id, cursor = Editor.Cursor.End)
|
|
viewModelScope.launch { orchestrator.stores.focus.update(focus) }
|
|
}
|
|
}
|
|
}
|
|
else -> Timber.d("Skipping initial focusing, document is not empty.")
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onAddWebLinkToBlock(blockId: Id, link: Id) {
|
|
Timber.d("onAddWebUrlLinkToBlock, blockId:[$blockId] link:[$link]")
|
|
onUpdateBlockListMarkup(ids = listOf(blockId), type = Markup.Type.LINK, param = link)
|
|
}
|
|
|
|
fun onAddObjectLinkToBlock(blockId: Id, objectId: Id) {
|
|
Timber.d("onAddObjectIdLinkToBlock, blockId:[$blockId] objectId:[$objectId]")
|
|
onUpdateBlockListMarkup(ids = listOf(blockId), type = Markup.Type.OBJECT, param = objectId)
|
|
}
|
|
|
|
fun onSystemBackPressed(editorHasChildrenScreens: Boolean) {
|
|
Timber.d("onSystemBackPressed, editorHasChildrenScreens:[$editorHasChildrenScreens]")
|
|
if (editorHasChildrenScreens) {
|
|
dispatch(Command.PopBackStack)
|
|
} else {
|
|
val state = controlPanelViewState.value
|
|
checkNotNull(state) { "Control panel state is null" }
|
|
when {
|
|
state.styleTextToolbar.isVisible -> {
|
|
onCloseBlockStyleToolbarClicked()
|
|
}
|
|
state.styleColorBackgroundToolbar.isVisible -> {
|
|
onCloseBlockStyleColorToolbarClicked()
|
|
}
|
|
state.styleExtraToolbar.isVisible -> {
|
|
onCloseBlockStyleExtraToolbarClicked()
|
|
}
|
|
state.multiSelect.isVisible -> {
|
|
onExitMultiSelectModeClicked()
|
|
}
|
|
state.styleBackgroundToolbar.isVisible -> {
|
|
onCloseBlockStyleBackgroundToolbarClicked()
|
|
}
|
|
else -> {
|
|
proceedWithExitingBack()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onDismissBlockActionMenu(editorHasChildrenScreens: Boolean) {
|
|
Timber.d("onDismissBlockActionMenu, editorHasChildrenScreens:[$editorHasChildrenScreens]")
|
|
onExitActionMode()
|
|
onSystemBackPressed(editorHasChildrenScreens)
|
|
}
|
|
|
|
fun onBackButtonPressed() {
|
|
Timber.d("onBackButtonPressed, ")
|
|
viewModelScope.sendAnalyticsGoBackEvent(analytics, analyticsContext)
|
|
proceedWithExitingBack()
|
|
}
|
|
|
|
fun onHomeButtonClicked() {
|
|
Timber.d("onHomeButtonClicked, ")
|
|
if (stateData.value == ViewState.NotExist) {
|
|
navigateToDesktop()
|
|
return
|
|
}
|
|
proceedWithExitingToDashboard()
|
|
}
|
|
|
|
fun proceedWithExitingBack() {
|
|
exitBack()
|
|
}
|
|
|
|
private fun exitBack() {
|
|
when (session.value) {
|
|
Session.ERROR -> navigate(EventWrapper(AppNavigation.Command.Exit))
|
|
Session.IDLE -> navigate(EventWrapper(AppNavigation.Command.Exit))
|
|
Session.OPEN -> {
|
|
viewModelScope.launch {
|
|
closePage(
|
|
CloseBlock.Params(context)
|
|
).proceed(
|
|
success = { navigation.postValue(EventWrapper(AppNavigation.Command.Exit)) },
|
|
failure = { Timber.e(it, "Error while closing document: $context") }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun proceedWithExitingToDashboard() {
|
|
exitDashboard()
|
|
}
|
|
|
|
private fun exitDashboard() {
|
|
viewModelScope.launch {
|
|
closePage(CloseBlock.Params(context)).proceed(
|
|
success = { navigateToDesktop() },
|
|
failure = { Timber.e(it, "Error while closing this page: $context") }
|
|
)
|
|
}
|
|
}
|
|
|
|
fun navigateToDesktop() {
|
|
Timber.d("navigateToDesktop, ")
|
|
navigation.postValue(EventWrapper(AppNavigation.Command.ExitToDesktop))
|
|
}
|
|
|
|
@Deprecated("replace by onTextBlockTextChanged")
|
|
fun onTextChanged(
|
|
id: String,
|
|
text: String,
|
|
marks: List<Content.Text.Mark>
|
|
) {
|
|
Timber.d("onTextChanged, id:[$id], text:[$text], marks:[$marks]")
|
|
val update = TextUpdate.Default(target = id, text = text, markup = marks)
|
|
viewModelScope.launch { orchestrator.proxies.changes.send(update) }
|
|
}
|
|
|
|
fun onTitleBlockTextChanged(id: Id, text: String) {
|
|
Timber.d("onTitleBlockTextChanged, id:[$id], text:[$text]")
|
|
val new = views.map {
|
|
if (it.id == id && it is BlockView.Title) {
|
|
it.text = text
|
|
it
|
|
} else {
|
|
it
|
|
}
|
|
}
|
|
val update = TextUpdate.Default(
|
|
target = id,
|
|
text = text,
|
|
markup = emptyList()
|
|
)
|
|
viewModelScope.launch { orchestrator.stores.views.update(new) }
|
|
viewModelScope.launch { orchestrator.proxies.changes.send(update) }
|
|
if (isObjectTypesWidgetVisible) {
|
|
dispatchObjectCreateEvent()
|
|
proceedWithHidingObjectTypeWidget()
|
|
}
|
|
}
|
|
|
|
fun onDescriptionBlockTextChanged(view: BlockView.Description) {
|
|
|
|
Timber.d("onDescriptionBlockTextChanged, view:[$view]")
|
|
|
|
val new = views.map { if (it.id == view.id) view else it }
|
|
val update = TextUpdate.Default(
|
|
target = view.id,
|
|
text = view.text,
|
|
markup = emptyList()
|
|
)
|
|
viewModelScope.launch { orchestrator.stores.views.update(new) }
|
|
viewModelScope.launch { orchestrator.proxies.changes.send(update) }
|
|
if (isObjectTypesWidgetVisible) {
|
|
dispatchObjectCreateEvent()
|
|
proceedWithHidingObjectTypeWidget()
|
|
}
|
|
}
|
|
|
|
fun onTextBlockTextChanged(view: BlockView.Text) {
|
|
Timber.d("onTextBlockTextChanged, view:[$view]")
|
|
|
|
val update = TextUpdate.Pattern(
|
|
target = view.id,
|
|
text = view.text,
|
|
markup = view.marks.map { it.mark() }
|
|
)
|
|
|
|
val store = orchestrator.stores.views
|
|
val old = store.current()
|
|
val new = old.map { if (it.id == view.id) view else it }
|
|
|
|
viewModelScope.launch {
|
|
if (view is BlockView.Text.Header && new.any { it is BlockView.TableOfContents }) {
|
|
store.update(new.updateTableOfContentsViews(view))
|
|
renderCommand.send(Unit)
|
|
} else {
|
|
store.update(new)
|
|
}
|
|
}
|
|
|
|
viewModelScope.launch { orchestrator.proxies.changes.send(update) }
|
|
if (isObjectTypesWidgetVisible) {
|
|
dispatchObjectCreateEvent()
|
|
proceedWithHidingObjectTypeWidget()
|
|
}
|
|
}
|
|
|
|
fun onSelectionChanged(id: String, selection: IntRange) {
|
|
if (mode != EditorMode.Edit) return
|
|
Timber.d("onSelectionChanged, id:[$id] selection:[$selection]")
|
|
viewModelScope.launch {
|
|
orchestrator.stores.textSelection.update(Editor.TextSelection(id, selection))
|
|
}
|
|
blocks.find { it.id == id }?.let { target ->
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.OnSelectionChanged(
|
|
target = target,
|
|
selection = selection
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onBlockFocusChanged(id: String, hasFocus: Boolean) {
|
|
Timber.d("onBlockFocusChanged, id:[$id] hasFocus:[$hasFocus]")
|
|
if (hasFocus) {
|
|
isUndoRedoToolbarIsVisible.value = false
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(
|
|
Editor.Focus.id(id = id, isPending = false)
|
|
)
|
|
}
|
|
// controlPanelInteractor.onEvent(
|
|
// ControlPanelMachine.Event.OnFocusChanged(
|
|
// id = id,
|
|
// style = if (id == context)
|
|
// Content.Text.Style.TITLE
|
|
// else
|
|
// blocks.first { it.id == id }.textStyle()
|
|
// )
|
|
// )
|
|
}
|
|
}
|
|
|
|
private fun proceedWithMergingBlocks(
|
|
target: String,
|
|
previous: String
|
|
) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.Merge(
|
|
context = context,
|
|
previous = previous,
|
|
pair = Pair(previous, target),
|
|
previousLength = blocks.find { it.id == previous }?.let { block ->
|
|
if (block.content is Content.Text) {
|
|
block.content.asText().text.length
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onEnterKeyClicked(
|
|
target: String,
|
|
text: String,
|
|
marks: List<Content.Text.Mark>,
|
|
range: IntRange
|
|
) {
|
|
Timber.d("onEnterKeyClicked, target:[$target] text:[$text] marks:[$marks] range:[$range]")
|
|
val focus = orchestrator.stores.focus.current()
|
|
if (!focus.isEmpty && focus.id == target) {
|
|
proceedWithEnterEvent(focus.id, range, text, marks)
|
|
} else {
|
|
Timber.e("No blocks in focus, emit SplitLineEnter event")
|
|
}
|
|
}
|
|
|
|
fun onSplitObjectDescription(
|
|
target: Id,
|
|
text: String,
|
|
range: IntRange
|
|
) {
|
|
proceedWithSplitEvent(
|
|
target = target,
|
|
text = text,
|
|
range = range,
|
|
marks = emptyList()
|
|
)
|
|
}
|
|
|
|
private fun proceedWithEnterEvent(
|
|
target: Id,
|
|
range: IntRange,
|
|
text: String,
|
|
marks: List<Content.Text.Mark>
|
|
) {
|
|
if (text.isEndLineClick(range)) {
|
|
onEndLineEnterClicked(target, text, marks)
|
|
} else {
|
|
proceedWithSplitEvent(target, range, text, marks)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithSplitEvent(
|
|
target: Id,
|
|
range: IntRange,
|
|
text: String,
|
|
marks: List<Content.Text.Mark>
|
|
) {
|
|
|
|
val block = blocks.first { it.id == target }
|
|
val content = block.content<Content.Text>()
|
|
|
|
val update = blocks.updateTextContent(target, text, marks)
|
|
orchestrator.stores.document.update(update)
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.saves.send(null)
|
|
orchestrator.proxies.changes.send(null)
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateText(
|
|
context = context,
|
|
target = target,
|
|
marks = marks,
|
|
text = text
|
|
)
|
|
)
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.Split(
|
|
context = context,
|
|
block = block,
|
|
range = range,
|
|
isToggled = if (content.isToggle()) renderer.isToggled(target) else null,
|
|
style = content.style
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onEndLineEnterClicked(
|
|
id: String,
|
|
text: String,
|
|
marks: List<Content.Text.Mark>
|
|
) {
|
|
Timber.d("onEndLineEnterClicked, id:[$id] text:[$text] marks:[$marks]")
|
|
|
|
val target = blocks.first { it.id == id }
|
|
|
|
val content = target.content<Content.Text>().copy(
|
|
text = text,
|
|
marks = marks
|
|
)
|
|
|
|
val update = blocks.replace(
|
|
replacement = { old -> old.copy(content = content) }
|
|
) { block -> block.id == id }
|
|
|
|
orchestrator.stores.document.update(update)
|
|
|
|
if (content.isList() || content.isToggle()) {
|
|
handleEndlineEnterPressedEventForListItem(content, id)
|
|
} else {
|
|
proceedWithCreatingNewTextBlock(
|
|
id = id,
|
|
style = Content.Text.Style.P
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onDocumentMenuClicked() {
|
|
Timber.d("onDocumentMenuClicked, ")
|
|
proceedWithOpeningObjectMenu()
|
|
}
|
|
|
|
private fun proceedWithOpeningObjectMenu() {
|
|
blocks.find { it.id == context }?.let { root ->
|
|
val content = root.content
|
|
check(content is Content.Smart)
|
|
when (content.type) {
|
|
SmartBlockType.PROFILE_PAGE -> {
|
|
val details = orchestrator.stores.details.current().details
|
|
dispatch(
|
|
command = Command.OpenProfileMenu(
|
|
isFavorite = details[context]?.isFavorite ?: false,
|
|
isLocked = mode == EditorMode.Locked
|
|
)
|
|
)
|
|
}
|
|
SmartBlockType.PAGE -> {
|
|
val details = orchestrator.stores.details.current().details
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnDocumentMenuClicked)
|
|
dispatch(
|
|
command = Command.OpenDocumentMenu(
|
|
isArchived = details[context]?.isArchived ?: false,
|
|
isFavorite = details[context]?.isFavorite ?: false,
|
|
isLocked = mode == EditorMode.Locked
|
|
)
|
|
)
|
|
}
|
|
SmartBlockType.FILE -> {
|
|
val details = orchestrator.stores.details.current().details
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnDocumentMenuClicked)
|
|
dispatch(
|
|
command = Command.OpenDocumentMenu(
|
|
isArchived = details[context]?.isArchived ?: false,
|
|
isFavorite = details[context]?.isFavorite ?: false,
|
|
isLocked = mode == EditorMode.Locked
|
|
)
|
|
)
|
|
}
|
|
else -> {
|
|
Timber.e("Trying to open menu for unexpected smart content: ${content.type}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onEmptyBlockBackspaceClicked(id: String) {
|
|
Timber.d("onEmptyBlockBackspaceClicked, id:[$id]")
|
|
val position = views.indexOfFirst { it.id == id }
|
|
if (position > 0) {
|
|
val current = views[position]
|
|
if (current is BlockView.Text && current.isStyleClearable()) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateStyle(
|
|
context = context,
|
|
targets = listOf(id),
|
|
style = Content.Text.Style.P
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
val previous = views[position.dec()]
|
|
if (previous !is BlockView.Text
|
|
&& previous !is BlockView.Title
|
|
&& previous !is BlockView.Description
|
|
&& previous !is BlockView.FeaturedRelation
|
|
) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Unlink(
|
|
context = context,
|
|
targets = listOf(previous.id),
|
|
previous = null,
|
|
next = null,
|
|
cursor = null
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
proceedWithUnlinking(target = id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onNonEmptyBlockBackspaceClicked(
|
|
id: String,
|
|
text: String,
|
|
marks: List<Content.Text.Mark>
|
|
) {
|
|
Timber.d("onNonEmptyBlockBackspaceClicked, id:[$id] text:[$text] marks:[$marks]")
|
|
|
|
val update = blocks.map { block ->
|
|
if (block.id == id) {
|
|
block.copy(
|
|
content = block.content<Content.Text>().copy(
|
|
text = text,
|
|
marks = marks
|
|
)
|
|
)
|
|
} else {
|
|
block
|
|
}
|
|
}
|
|
|
|
orchestrator.stores.document.update(update)
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.saves.send(null)
|
|
orchestrator.proxies.changes.send(null)
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateText(
|
|
context = context,
|
|
target = id,
|
|
marks = marks,
|
|
text = text
|
|
)
|
|
)
|
|
}
|
|
|
|
val index = views.indexOfFirst { it.id == id }
|
|
|
|
if (index > 0) {
|
|
val previousBlockId = index.dec()
|
|
when (val previous = views[previousBlockId]) {
|
|
is BlockView.Text -> {
|
|
proceedWithMergingBlocks(
|
|
previous = previous.id,
|
|
target = id
|
|
)
|
|
}
|
|
is BlockView.FeaturedRelation -> {
|
|
val upperThanPreviousBlock = views.getOrNull(previousBlockId.dec())
|
|
if (upperThanPreviousBlock is Focusable) {
|
|
proceedWithMergingBlocks(
|
|
previous = upperThanPreviousBlock.id,
|
|
target = id
|
|
)
|
|
}
|
|
}
|
|
is BlockView.Description,
|
|
is BlockView.Title -> {
|
|
proceedWithMergingBlocks(
|
|
previous = previous.id,
|
|
target = id
|
|
)
|
|
}
|
|
else -> {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Unlink(
|
|
context = context,
|
|
targets = listOf(previous.id),
|
|
previous = null,
|
|
next = null,
|
|
cursor = null
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Timber.d("Skipping merge on non-empty-block-backspace-pressed event")
|
|
}
|
|
}
|
|
|
|
private fun handleEndlineEnterPressedEventForListItem(
|
|
content: Content.Text,
|
|
id: String
|
|
) {
|
|
if (content.text.isNotEmpty()) {
|
|
proceedWithSplitEvent(
|
|
target = id,
|
|
range = content.text.length..content.text.length,
|
|
marks = content.marks,
|
|
text = content.text
|
|
)
|
|
} else {
|
|
proceedWithUpdateTextStyle(
|
|
style = Content.Text.Style.P,
|
|
targets = listOf(id)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithCreatingNewTextBlock(
|
|
id: String,
|
|
style: Content.Text.Style,
|
|
position: Position = Position.BOTTOM
|
|
) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Create(
|
|
context = context,
|
|
target = id,
|
|
position = position,
|
|
prototype = Prototype.Text(style = style)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithEnteringActionMode(target: Id, scrollTarget: Boolean = true) {
|
|
val views = orchestrator.stores.views.current()
|
|
val view = views.find { it.id == target }
|
|
|
|
val restrictions = orchestrator.stores.objectRestrictions.current()
|
|
if (restrictions.isNotEmpty()) {
|
|
when (view) {
|
|
is BlockView.Code, is BlockView.Text,
|
|
is BlockView.Media, is BlockView.MediaPlaceholder,
|
|
is BlockView.Upload -> {
|
|
if (restrictions.contains(ObjectRestriction.BLOCKS)) {
|
|
sendToast(NOT_ALLOWED_FOR_OBJECT)
|
|
return
|
|
}
|
|
}
|
|
is BlockView.Relation, is BlockView.FeaturedRelation -> {
|
|
if (restrictions.contains(ObjectRestriction.RELATIONS)) {
|
|
sendToast(NOT_ALLOWED_FOR_OBJECT)
|
|
return
|
|
}
|
|
}
|
|
is BlockView.Title -> {
|
|
if (restrictions.contains(ObjectRestriction.DETAILS)) {
|
|
sendToast(NOT_ALLOWED_FOR_OBJECT)
|
|
return
|
|
}
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
|
|
toggleSelection(target)
|
|
|
|
if (view !is BlockView.Table && view !is BlockView.TableOfContents) {
|
|
val descendants = blocks.asMap().descendants(parent = target)
|
|
if (isSelected(target)) {
|
|
descendants.forEach { child -> select(child) }
|
|
} else {
|
|
descendants.forEach { child -> unselect(child) }
|
|
}
|
|
}
|
|
|
|
mode = EditorMode.Select
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(Editor.Focus.empty())
|
|
orchestrator.stores.views.update(
|
|
views.enterSAM(targets = currentSelection())
|
|
)
|
|
renderCommand.send(Unit)
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.MultiSelect.OnEnter(
|
|
currentSelection().size
|
|
)
|
|
)
|
|
if (isSelected(target) && scrollTarget) {
|
|
dispatch(Command.ScrollToActionMenu(target = target))
|
|
}
|
|
}
|
|
|
|
proceedWithUpdatingActionsForCurrentSelection()
|
|
}
|
|
|
|
private fun proceedWithUpdatingActionsForCurrentSelection() {
|
|
val isMultiMode = currentSelection().size > 1
|
|
|
|
val targetActions = mutableListOf<ActionItemType>().apply {
|
|
addAll(ActionItemType.defaultSorting)
|
|
}
|
|
val excludedActions = mutableSetOf<ActionItemType>()
|
|
|
|
if (isMultiMode) {
|
|
excludedActions.add(ActionItemType.AddBelow)
|
|
excludedActions.add(ActionItemType.Divider)
|
|
excludedActions.add(ActionItemType.DividerExtended)
|
|
excludedActions.add(ActionItemType.OpenObject)
|
|
}
|
|
|
|
var needSortByDownloads = false
|
|
|
|
blocks.forEach { block ->
|
|
if (currentSelection().contains(block.id)) {
|
|
when (val content = block.content) {
|
|
is Content.Bookmark -> {
|
|
excludedActions.add(ActionItemType.Download)
|
|
if (!isMultiMode) {
|
|
if (content.targetObjectId != null) {
|
|
val details = orchestrator.stores.details.current().details
|
|
val obj = ObjectWrapper.Basic(
|
|
details[content.targetObjectId]?.map ?: emptyMap()
|
|
)
|
|
val isReady = content.state == Content.Bookmark.State.DONE
|
|
val isActive = obj.isArchived != true && obj.isDeleted != true
|
|
val idx = targetActions.indexOf(ActionItemType.OpenObject)
|
|
if (idx == NO_POSITION && isReady && isActive) {
|
|
targetActions.add(
|
|
OPEN_OBJECT_POSITION,
|
|
ActionItemType.OpenObject
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
is Content.Divider -> {
|
|
excludedActions.add(ActionItemType.Download)
|
|
}
|
|
is Content.File -> {
|
|
needSortByDownloads = true
|
|
if (content.state == Content.File.State.DONE) {
|
|
targetActions.addIfNotExists(ActionItemType.Download)
|
|
} else {
|
|
excludedActions.add(ActionItemType.Download)
|
|
}
|
|
}
|
|
is Content.Link -> {
|
|
targetActions.clear()
|
|
if (!isMultiMode) {
|
|
targetActions.addAll(ActionItemType.objectSorting)
|
|
} else {
|
|
targetActions.addAll(ActionItemType.objectSortingMultiline)
|
|
}
|
|
if (!BuildConfig.ENABLE_LINK_APPERANCE_MENU) {
|
|
excludedActions.add(ActionItemType.Preview)
|
|
}
|
|
}
|
|
is Content.Page -> {
|
|
excludedActions.add(ActionItemType.Download)
|
|
}
|
|
is Content.RelationBlock -> {
|
|
excludedActions.add(ActionItemType.Download)
|
|
}
|
|
is Content.Latex -> {
|
|
excludedActions.add(ActionItemType.Download)
|
|
}
|
|
is Content.Text -> {
|
|
excludedActions.add(ActionItemType.Download)
|
|
}
|
|
is Content.Table -> {
|
|
excludedActions.add(ActionItemType.Paste)
|
|
excludedActions.add(ActionItemType.Copy)
|
|
excludedActions.add(ActionItemType.Style)
|
|
}
|
|
is Content.TableOfContents -> {
|
|
excludedActions.add(ActionItemType.Paste)
|
|
excludedActions.add(ActionItemType.Copy)
|
|
excludedActions.add(ActionItemType.Style)
|
|
}
|
|
else -> {
|
|
// do nothing
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
targetActions.removeAll(excludedActions)
|
|
|
|
actions.value = if (needSortByDownloads) {
|
|
targetActions.sortedBy { it !is ActionItemType.Download }
|
|
} else {
|
|
targetActions
|
|
}
|
|
}
|
|
|
|
private fun MutableList<ActionItemType>.addIfNotExists(
|
|
item: ActionItemType,
|
|
position: Int = NO_POSITION
|
|
) {
|
|
if (contains(item)) {
|
|
return
|
|
}
|
|
|
|
if (position == NO_POSITION) {
|
|
add(item)
|
|
} else {
|
|
add(position, item)
|
|
}
|
|
}
|
|
|
|
fun onStylingToolbarEvent(event: StylingEvent) {
|
|
Timber.d("onStylingToolbarEvent, event:[$event]")
|
|
val ids: List<Id>? = mode.getIds()
|
|
if (ids.isNullOrEmpty()) return
|
|
when (event) {
|
|
is StylingEvent.Coloring.Text -> {
|
|
onToolbarTextColorAction(ids, event.color.code)
|
|
}
|
|
is StylingEvent.Coloring.Background -> {
|
|
onBlockBackgroundColorAction(ids, event.color.code)
|
|
}
|
|
is StylingEvent.Markup.Bold -> {
|
|
onUpdateBlockListMarkup(ids, Markup.Type.BOLD)
|
|
}
|
|
is StylingEvent.Markup.Italic -> {
|
|
onUpdateBlockListMarkup(ids, Markup.Type.ITALIC)
|
|
}
|
|
is StylingEvent.Markup.StrikeThrough -> {
|
|
onUpdateBlockListMarkup(ids, Markup.Type.STRIKETHROUGH)
|
|
}
|
|
is StylingEvent.Markup.Code -> {
|
|
onUpdateBlockListMarkup(ids, Markup.Type.KEYBOARD)
|
|
}
|
|
is StylingEvent.Markup.Underline -> {
|
|
onUpdateBlockListMarkup(ids, Markup.Type.UNDERLINE)
|
|
}
|
|
is StylingEvent.Markup.Link -> {
|
|
if (ids.size == 1) {
|
|
onBlockStyleLinkClicked(ids[0])
|
|
} else {
|
|
sendToast(ERROR_UNSUPPORTED_BEHAVIOR)
|
|
}
|
|
}
|
|
is StylingEvent.Alignment.Left -> {
|
|
proceedWithAlignmentUpdate(ids, Block.Align.AlignLeft)
|
|
}
|
|
is StylingEvent.Alignment.Center -> {
|
|
proceedWithAlignmentUpdate(ids, Block.Align.AlignCenter)
|
|
}
|
|
is StylingEvent.Alignment.Right -> {
|
|
proceedWithAlignmentUpdate(ids, Block.Align.AlignRight)
|
|
}
|
|
else -> Timber.d("Ignoring styling toolbar event: $event")
|
|
}
|
|
}
|
|
|
|
fun onStyleToolbarMarkupAction(type: Markup.Type, param: String? = null) {
|
|
Timber.d("onStyleToolbarMarkupAction, type:[$type] param:[$param]")
|
|
viewModelScope.launch {
|
|
markupActionPipeline.send(
|
|
MarkupAction(
|
|
type = type,
|
|
param = param
|
|
)
|
|
)
|
|
}
|
|
viewModelScope.sendAnalyticsUpdateTextMarkupEvent(
|
|
analytics = analytics,
|
|
type = type,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
|
|
private fun proceedWithAlignmentUpdate(targets: List<Id>, alignment: Block.Align) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.Align(
|
|
context = context,
|
|
targets = targets,
|
|
alignment = alignment
|
|
)
|
|
)
|
|
sendAnalyticsBlockAlignEvent(
|
|
analytics = analytics,
|
|
context = analyticsContext,
|
|
count = targets.size,
|
|
align = alignment
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onToolbarTextColorAction(targets: List<Id>, color: String?) {
|
|
Timber.d("onToolbarTextColorAction, ids:[$targets] color:[$color]")
|
|
check(color != null)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnBlockTextColorSelected)
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateColor(
|
|
context = context,
|
|
targets = targets,
|
|
color = color
|
|
)
|
|
)
|
|
}
|
|
viewModelScope.sendAnalyticsUpdateTextMarkupEvent(
|
|
analytics = analytics,
|
|
type = Content.Text.Mark.Type.TEXT_COLOR,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
|
|
private fun onBlockBackgroundColorAction(ids: List<Id>, color: String) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateBackgroundColor(
|
|
context = context,
|
|
targets = ids,
|
|
color = color
|
|
)
|
|
)
|
|
}
|
|
viewModelScope.sendAnalyticsBlockBackgroundEvent(
|
|
analytics = analytics,
|
|
count = ids.size,
|
|
color = color,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
|
|
private fun onBlockStyleLinkClicked(id: String) {
|
|
val target = blocks.first { it.id == id }
|
|
val range = IntRange(
|
|
start = 0,
|
|
endInclusive = target.content<Content.Text>().text.length
|
|
)
|
|
dispatch(
|
|
Command.OpenLinkToObjectOrWebScreen(
|
|
ctx = context,
|
|
target = target.id,
|
|
range = range,
|
|
isWholeBlockMarkup = true
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun onUpdateBlockListMarkup(ids: List<Id>, type: Markup.Type, param: String? = null) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateMark(
|
|
context = context,
|
|
targets = ids,
|
|
mark = Content.Text.Mark(
|
|
range = IntRange(0, Int.MAX_VALUE),
|
|
type = type.toCoreModel(),
|
|
param = param
|
|
)
|
|
)
|
|
)
|
|
sendAnalyticsUpdateTextMarkupEvent(
|
|
analytics = analytics,
|
|
type = type,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onSetRelationKeyClicked(blockId: Id, key: Id) {
|
|
Timber.d("onSetRelationKeyClicked, blockId:[$blockId] key:[$key]")
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.SetRelationKey(
|
|
context = context,
|
|
blockId = blockId,
|
|
key = key
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithUnlinking(target: String) {
|
|
|
|
val position = views.indexOfFirst { it.id == target }
|
|
|
|
var previous: Id? = null
|
|
var cursor: Int? = null
|
|
|
|
if (position <= 0) return
|
|
|
|
for (i in position.dec() downTo 0) {
|
|
when (val view = views[i]) {
|
|
is BlockView.Text -> {
|
|
previous = view.id
|
|
cursor = view.text.length
|
|
break
|
|
}
|
|
is BlockView.Code -> {
|
|
previous = view.id
|
|
cursor = view.text.length
|
|
break
|
|
}
|
|
is BlockView.Title -> {
|
|
previous = view.id
|
|
cursor = view.text?.length ?: 0
|
|
break
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Unlink(
|
|
context = context,
|
|
targets = listOf(target),
|
|
previous = previous,
|
|
next = null,
|
|
cursor = cursor
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun duplicateBlock(
|
|
blocks: List<Id>,
|
|
target: Id
|
|
) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Duplicate(
|
|
context = context,
|
|
target = target,
|
|
blocks = blocks
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onActionUndoClicked() {
|
|
Timber.d("onActionUndoClicked, ")
|
|
jobs += viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.Undo(
|
|
context = context,
|
|
onUndoExhausted = { sendSnack(Snack.UndoRedo("Nothing to undo.")) }
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onActionRedoClicked() {
|
|
Timber.d("onActionRedoClicked, ")
|
|
jobs += viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.Redo(
|
|
context = context,
|
|
onRedoExhausted = { sendSnack(Snack.UndoRedo("Nothing to redo.")) }
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onEnterSearchModeClicked() {
|
|
Timber.d("onEnterSearchModeClicked, ")
|
|
mode = EditorMode.Search
|
|
viewModelScope.launch { orchestrator.stores.views.update(views.toReadMode()) }
|
|
viewModelScope.launch { renderCommand.send(Unit) }
|
|
viewModelScope.launch { controlPanelInteractor.onEvent(ControlPanelMachine.Event.SearchToolbar.OnEnterSearchMode) }
|
|
}
|
|
|
|
fun onSetTextBlockValue() {
|
|
viewModelScope.launch { refresh() }
|
|
}
|
|
|
|
fun onDocRelationsClicked() {
|
|
Timber.d("onDocRelationsClicked, ")
|
|
dispatch(
|
|
Command.OpenObjectRelationScreen.RelationList(
|
|
ctx = context,
|
|
target = null,
|
|
isLocked = mode == EditorMode.Locked
|
|
)
|
|
)
|
|
}
|
|
|
|
fun onSearchToolbarEvent(event: SearchInDocEvent) {
|
|
Timber.d("onSearchToolbarEvent, event:[$event]")
|
|
if (mode !is EditorMode.Search) return
|
|
when (event) {
|
|
is SearchInDocEvent.Query -> {
|
|
val query = event.query.trim()
|
|
val update = if (query.isEmpty()) {
|
|
views.clearSearchHighlights()
|
|
} else {
|
|
val flags = Pattern.MULTILINE or Pattern.CASE_INSENSITIVE
|
|
val escaped = Pattern.quote(query)
|
|
val pattern = Pattern.compile(escaped, flags)
|
|
views.highlight { pairs ->
|
|
pairs.map { (key, txt) ->
|
|
BlockView.Searchable.Field(
|
|
key = key,
|
|
highlights = txt.search(pattern)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
viewModelScope.launch { orchestrator.stores.views.update(update) }
|
|
viewModelScope.launch { renderCommand.send(Unit) }
|
|
viewModelScope.sendAnalyticsSearchWordsEvent(
|
|
analytics = analytics,
|
|
length = query.length,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
is SearchInDocEvent.Next -> {
|
|
val update = views.nextSearchTarget()
|
|
viewModelScope.launch { orchestrator.stores.views.update(update) }
|
|
viewModelScope.launch { renderCommand.send(Unit) }
|
|
val target = update.find { view ->
|
|
view is BlockView.Searchable && view.searchFields.any { it.isTargeted }
|
|
}
|
|
val pos = update.indexOf(target)
|
|
searchResultScrollPosition.value = pos
|
|
}
|
|
is SearchInDocEvent.Previous -> {
|
|
val update = views.previousSearchTarget()
|
|
viewModelScope.launch { orchestrator.stores.views.update(update) }
|
|
viewModelScope.launch { renderCommand.send(Unit) }
|
|
val target = update.find { view ->
|
|
view is BlockView.Searchable && view.searchFields.any { it.isTargeted }
|
|
}
|
|
val pos = update.indexOf(target)
|
|
searchResultScrollPosition.value = pos
|
|
}
|
|
is SearchInDocEvent.Cancel -> {
|
|
mode = EditorMode.Edit
|
|
val update = views.clearSearchHighlights().toEditMode()
|
|
viewModelScope.launch { orchestrator.stores.views.update(update) }
|
|
viewModelScope.launch { renderCommand.send(Unit) }
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.SearchToolbar.OnExitSearchMode)
|
|
dispatch(Command.ClearSearchInput)
|
|
}
|
|
is SearchInDocEvent.Search -> {
|
|
val update = views.nextSearchTarget()
|
|
viewModelScope.launch { orchestrator.stores.views.update(update) }
|
|
viewModelScope.launch { renderCommand.send(Unit) }
|
|
val target = update.find { view ->
|
|
view is BlockView.Searchable && view.searchFields.any { it.isTargeted }
|
|
}
|
|
val pos = update.indexOf(target)
|
|
searchResultScrollPosition.value = pos
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
|
|
fun onAddTextBlockClicked(style: Content.Text.Style) {
|
|
|
|
Timber.d("onAddTextBlockClicked, style:[$style]")
|
|
|
|
val target = blocks.first { it.id == orchestrator.stores.focus.current().id }
|
|
|
|
val content = target.content
|
|
|
|
if (content is Content.Text && content.text.isEmpty()) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Replace(
|
|
context = context,
|
|
target = target.id,
|
|
prototype = Prototype.Text(style = style)
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
proceedWithCreatingNewTextBlock(
|
|
id = target.id,
|
|
style = style,
|
|
position = Position.BOTTOM
|
|
)
|
|
}
|
|
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected)
|
|
}
|
|
|
|
private fun onAddLocalVideoClicked(blockId: String) {
|
|
currentMediaUploadDescription = Media.Upload.Description(blockId, Mimetype.MIME_VIDEO_ALL)
|
|
dispatch(Command.OpenGallery(mimeType = Mimetype.MIME_VIDEO_ALL))
|
|
}
|
|
|
|
private fun onAddLocalPictureClicked(blockId: String) {
|
|
currentMediaUploadDescription = Media.Upload.Description(blockId, Mimetype.MIME_IMAGE_ALL)
|
|
dispatch(Command.OpenGallery(mimeType = Mimetype.MIME_IMAGE_ALL))
|
|
}
|
|
|
|
fun onTogglePlaceholderClicked(target: Id) {
|
|
Timber.d("onTogglePlaceholderClicked, target:[$target]")
|
|
if (mode == EditorMode.Edit) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Create(
|
|
context = context,
|
|
target = target,
|
|
prototype = Prototype.Text(
|
|
style = Content.Text.Style.P
|
|
),
|
|
position = Position.INNER
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onToggleClicked(target: Id) {
|
|
Timber.d("onToggleClicked, target:[$target]")
|
|
if (mode is EditorMode.Edit || mode is EditorMode.Locked) {
|
|
onToggleChanged(target)
|
|
viewModelScope.launch { refresh() }
|
|
}
|
|
}
|
|
|
|
private fun onAddLocalFileClicked(blockId: String) {
|
|
currentMediaUploadDescription = Media.Upload.Description(blockId, Mimetype.MIME_FILE_ALL)
|
|
dispatch(Command.OpenGallery(mimeType = Mimetype.MIME_FILE_ALL))
|
|
}
|
|
|
|
fun onAddFileBlockClicked(type: Content.File.Type) {
|
|
Timber.d("onAddFileBlockClicked, type:[$type]")
|
|
val focused = blocks.find { it.id == orchestrator.stores.focus.current().id }
|
|
if (focused != null) {
|
|
val content = focused.content
|
|
if (content is Content.Text && content.text.isEmpty()) {
|
|
proceedWithReplacingByEmptyFileBlock(
|
|
id = focused.id,
|
|
type = type
|
|
)
|
|
} else {
|
|
proceedWithCreatingEmptyFileBlock(
|
|
id = focused.id,
|
|
type = type,
|
|
position = Position.BOTTOM
|
|
)
|
|
}
|
|
} else {
|
|
Timber.e("Missing focus while onAddFileBlockClicked")
|
|
}
|
|
}
|
|
|
|
private fun proceedWithCreatingEmptyFileBlock(
|
|
id: String,
|
|
type: Content.File.Type,
|
|
state: Content.File.State = Content.File.State.EMPTY,
|
|
position: Position = Position.BOTTOM
|
|
) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Create(
|
|
context = context,
|
|
target = id,
|
|
position = position,
|
|
prototype = Prototype.File(type = type, state = state)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithReplacingByEmptyFileBlock(
|
|
id: String,
|
|
type: Content.File.Type,
|
|
state: Content.File.State = Content.File.State.EMPTY
|
|
) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Replace(
|
|
context = context,
|
|
target = id,
|
|
prototype = Prototype.File(type = type, state = state)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onCheckboxClicked(view: BlockView.Text.Checkbox) {
|
|
|
|
Timber.d("onCheckboxClicked, view:[$view]")
|
|
|
|
val update = blocks.map { block ->
|
|
if (block.id == view.id) {
|
|
block.copy(
|
|
content = block.content<Content.Text>().copy(
|
|
isChecked = view.isChecked
|
|
)
|
|
)
|
|
} else {
|
|
block
|
|
}
|
|
}
|
|
|
|
orchestrator.stores.document.update(update)
|
|
|
|
val store = orchestrator.stores.views
|
|
|
|
viewModelScope.launch {
|
|
store.update(
|
|
views.map { v ->
|
|
if (v.id == view.id)
|
|
view.copy()
|
|
else
|
|
v
|
|
}
|
|
)
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateCheckbox(
|
|
context = context,
|
|
target = view.id,
|
|
isChecked = view.isChecked
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onTitleCheckboxClicked(view: BlockView.Title.Todo) {
|
|
|
|
Timber.d("onTitleCheckboxClicked, view:[$view]")
|
|
|
|
val update = blocks.map { block ->
|
|
if (block.id == view.id) {
|
|
block.copy(
|
|
content = block.content<Content.Text>().copy(
|
|
isChecked = view.isChecked
|
|
)
|
|
)
|
|
} else {
|
|
block
|
|
}
|
|
}
|
|
|
|
orchestrator.stores.document.update(update)
|
|
|
|
val store = orchestrator.stores.views
|
|
|
|
viewModelScope.launch {
|
|
store.update(
|
|
views.map { v ->
|
|
if (v.id == view.id)
|
|
view.copy()
|
|
else
|
|
v
|
|
}
|
|
)
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateCheckbox(
|
|
context = context,
|
|
target = view.id,
|
|
isChecked = view.isChecked
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onBlockToolbarStyleClicked() {
|
|
Timber.d("onBlockToolbarStyleClicked, ")
|
|
val focus = orchestrator.stores.focus.current()
|
|
val targetId = focus.id
|
|
if (targetId.isNotEmpty()) {
|
|
when (val targetView = views.singleOrNull { it.id == targetId }) {
|
|
is BlockView.Description -> sendToast(CANNOT_OPEN_STYLE_PANEL_FOR_DESCRIPTION)
|
|
is BlockView.Code -> {
|
|
val selection = orchestrator.stores.textSelection.current().selection
|
|
if (selection != null && selection.first != selection.last) {
|
|
sendToast(CANNOT_OPEN_STYLE_PANEL_FOR_CODE_BLOCK_ERROR)
|
|
} else {
|
|
proceedWithStyleToolbarEvent(targetView)
|
|
}
|
|
}
|
|
is BlockView -> proceedWithStyleToolbarEvent(targetView)
|
|
else -> {
|
|
Timber.w("Failed to handle toolbar style click. Can't find targetView by id $targetId")
|
|
}
|
|
}
|
|
} else {
|
|
Timber.w("Failed to handle toolbar style click. Unknown focus for style toolbar: $focus")
|
|
}
|
|
viewModelScope.sendAnalyticsStyleMenuEvent(analytics)
|
|
}
|
|
|
|
private fun proceedWithStyleToolbarEvent(target: BlockView) {
|
|
val targetId = target.id
|
|
val targetBlock = blocks.find { it.id == targetId }
|
|
if (targetBlock != null) {
|
|
when (val content = targetBlock.content) {
|
|
is Content.Text -> {
|
|
mode = EditorMode.Styling.Single(
|
|
target = targetId,
|
|
cursor = orchestrator.stores.textSelection.current().selection?.first
|
|
)
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(Editor.Focus.empty())
|
|
orchestrator.stores.views.update(views.singleStylingMode(targetId))
|
|
renderCommand.send(Unit)
|
|
}
|
|
when {
|
|
target is BlockView.Title -> onSendUpdateStyleColorBackgroundToolbarEvent(
|
|
ids = listOf(targetId),
|
|
navigateFromStylingTextToolbar = false
|
|
)
|
|
content.style == Content.Text.Style.CODE_SNIPPET -> {
|
|
val state = targetBlock.getStyleBackgroundToolbarState()
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnUpdateBackgroundToolbar(
|
|
state
|
|
)
|
|
)
|
|
}
|
|
else -> {
|
|
val styleState = content.getStyleTextToolbarState()
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnUpdateTextToolbar(
|
|
styleState
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
else -> {
|
|
Timber.w("Failed to open style menu. Block content must be Text but was ${content.javaClass}")
|
|
sendToast("Failed to open style menu. Block content mustbe Text")
|
|
}
|
|
}
|
|
} else {
|
|
Timber.w("Failed to open style menu. Can't find target block: $target")
|
|
sendToast("Failed to open style menu. Can't find target block")
|
|
}
|
|
}
|
|
|
|
private fun proceedWithMultiStyleToolbarEvent() {
|
|
val selected = blocks.filter { currentSelection().contains(it.id) }
|
|
val isAllTextAndNoneCodeBlocks = selected.isAllTextAndNoneCodeBlocks()
|
|
mode = EditorMode.Styling.Multi(currentSelection())
|
|
if (isAllTextAndNoneCodeBlocks) {
|
|
val styleState = selected.map { it.content.asText() }.getStyleTextToolbarState()
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnUpdateTextToolbar(styleState)
|
|
)
|
|
} else {
|
|
val styleState = selected.getStyleBackgroundToolbarState()
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnUpdateBackgroundToolbar(styleState)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onCloseBlockStyleToolbarClicked() {
|
|
Timber.d("onCloseBlockStyleToolbarClicked, ")
|
|
if (mode is EditorMode.Styling.Single) {
|
|
val target = (mode as EditorMode.Styling.Single).target
|
|
val cursor = (mode as EditorMode.Styling.Single).cursor
|
|
mode = EditorMode.Edit
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(
|
|
Editor.Focus(
|
|
id = target,
|
|
cursor = cursor?.let { c -> Editor.Cursor.Range(c..c) }
|
|
)
|
|
)
|
|
orchestrator.stores.textSelection.update(
|
|
Editor.TextSelection(target, cursor?.let { it..it })
|
|
)
|
|
val focused = !orchestrator.stores.focus.current().isEmpty
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnClose(
|
|
focused
|
|
)
|
|
)
|
|
orchestrator.stores.views.update(
|
|
views.updateCursorAndEditMode(
|
|
target = target,
|
|
cursor = cursor
|
|
)
|
|
)
|
|
renderCommand.send(Unit)
|
|
}
|
|
} else if (mode is EditorMode.Styling.Multi) {
|
|
exitMultiStylingMode()
|
|
}
|
|
}
|
|
|
|
private fun exitMultiStylingMode() {
|
|
mode = EditorMode.Select
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.StylingToolbar.OnCloseMulti)
|
|
}
|
|
|
|
/**
|
|
* Closing style-toolbar and its dependent toolbars (color, extra). Back to edit mode.
|
|
*/
|
|
private fun onExitBlockStyleToolbarClicked() {
|
|
if (mode is EditorMode.Styling.Single) {
|
|
val target = (mode as EditorMode.Styling.Single).target
|
|
val cursor = (mode as EditorMode.Styling.Single).cursor
|
|
mode = EditorMode.Edit
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(
|
|
Editor.Focus(
|
|
id = target,
|
|
cursor = cursor?.let { c -> Editor.Cursor.Range(c..c) }
|
|
)
|
|
)
|
|
orchestrator.stores.textSelection.update(
|
|
Editor.TextSelection(target, cursor?.let { it..it })
|
|
)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.StylingToolbar.OnExit)
|
|
orchestrator.stores.views.update(
|
|
views.updateCursorAndEditMode(
|
|
target = target,
|
|
cursor = cursor
|
|
)
|
|
)
|
|
renderCommand.send(Unit)
|
|
}
|
|
} else if (mode is EditorMode.Styling.Multi) {
|
|
exitMultiStylingMode()
|
|
}
|
|
}
|
|
|
|
fun onCloseBlockStyleExtraToolbarClicked() {
|
|
Timber.d("onCloseBlockStyleExtraToolbarClicked, ")
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnExtraClosed
|
|
)
|
|
}
|
|
|
|
fun onCloseBlockStyleColorToolbarClicked() {
|
|
Timber.d("onCloseBlockStyleColorToolbarClicked, ")
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.StylingToolbar.OnColorBackgroundClosed
|
|
)
|
|
}
|
|
|
|
fun onCloseBlockStyleBackgroundToolbarClicked() {
|
|
Timber.d("onCloseBlockStyleColorToolbarClicked, ")
|
|
onCloseBlockStyleToolbarClicked()
|
|
}
|
|
|
|
fun onBlockToolbarBlockActionsClicked() {
|
|
Timber.d("onBlockToolbarBlockActionsClicked, ")
|
|
val target = orchestrator.stores.focus.current().id
|
|
val view = views.find { it.id == target } ?: return
|
|
when (view) {
|
|
is BlockView.Title -> {
|
|
sendToast(CANNOT_OPEN_ACTION_MENU_FOR_TITLE_ERROR)
|
|
}
|
|
is BlockView.Description -> {
|
|
sendToast(CANNOT_OPEN_ACTION_MENU_FOR_DESCRIPTION)
|
|
}
|
|
else -> {
|
|
proceedWithEnteringActionMode(target = target, scrollTarget = false)
|
|
}
|
|
}
|
|
viewModelScope.sendAnalyticsSelectionMenuEvent(analytics)
|
|
}
|
|
|
|
fun onEnterScrollAndMoveClicked() {
|
|
Timber.d("onEnterScrollAndMoveClicked, ")
|
|
mode = EditorMode.SAM
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.SAM.OnEnter)
|
|
}
|
|
|
|
fun onExitScrollAndMoveClicked() {
|
|
Timber.d("onExitScrollAndMoveClicked, ")
|
|
if (controlPanelViewState.value?.multiSelect?.isQuickScrollAndMoveMode == true) {
|
|
clearSelections()
|
|
mode = EditorMode.Edit
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.SAM.OnExit)
|
|
viewModelScope.launch { refresh() }
|
|
} else {
|
|
mode = EditorMode.Select
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.SAM.OnExit)
|
|
}
|
|
}
|
|
|
|
fun onApplyScrollAndMoveClicked() {
|
|
Timber.d("onApplyScrollAndMoveClicked, ")
|
|
}
|
|
|
|
private fun onExitActionMode() {
|
|
mode = EditorMode.Edit
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.ReadMode.OnExit)
|
|
viewModelScope.launch { refresh() }
|
|
}
|
|
|
|
// ----------------- Turn Into -----------------------------------------
|
|
|
|
private fun onTurnIntoBlockClicked(target: String, uiBlock: UiBlock) {
|
|
Timber.d("onTurnIntoBlockClicked, taget:[$target] uiBlock:[$uiBlock]")
|
|
proceedUpdateBlockStyle(
|
|
targets = listOf(target),
|
|
uiBlock = uiBlock,
|
|
errorAction = { sendToast("Cannot convert block to $uiBlock") }
|
|
)
|
|
dispatch(Command.PopBackStack)
|
|
}
|
|
|
|
fun onUpdateTextBlockStyle(uiBlock: UiBlock) {
|
|
Timber.d("onUpdateSingleTextBlockStyle, uiBlock:[$uiBlock]")
|
|
val ids = mode.getIds()
|
|
if (ids.isNullOrEmpty()) return
|
|
proceedUpdateBlockStyle(
|
|
targets = ids,
|
|
uiBlock = uiBlock,
|
|
errorAction = { sendToast("Cannot convert block to $uiBlock") }
|
|
)
|
|
}
|
|
|
|
fun onBlockStyleToolbarOtherClicked() {
|
|
Timber.d("onBlockStyleToolbarOtherClicked, ")
|
|
val ids = mode.getIds()
|
|
if (ids.isNullOrEmpty()) return
|
|
onSendUpdateStyleOtherToolbarEvent(ids)
|
|
}
|
|
|
|
fun onBlockStyleToolbarColorClicked() {
|
|
Timber.d("onBlockStyleToolbarColorClicked, ")
|
|
val ids = mode.getIds()
|
|
if (ids.isNullOrEmpty()) return
|
|
onSendUpdateStyleColorBackgroundToolbarEvent(
|
|
ids = ids,
|
|
navigateFromStylingTextToolbar = true
|
|
)
|
|
}
|
|
|
|
private fun proceedUpdateBlockStyle(
|
|
targets: List<String>,
|
|
uiBlock: UiBlock,
|
|
action: (() -> Unit)? = null,
|
|
errorAction: (() -> Unit)? = null
|
|
) {
|
|
when (uiBlock) {
|
|
UiBlock.TEXT, UiBlock.HEADER_ONE,
|
|
UiBlock.HEADER_TWO, UiBlock.HEADER_THREE,
|
|
UiBlock.HIGHLIGHTED, UiBlock.CHECKBOX,
|
|
UiBlock.BULLETED, UiBlock.NUMBERED,
|
|
UiBlock.TOGGLE, UiBlock.CODE,
|
|
UiBlock.CALLOUT -> {
|
|
action?.invoke()
|
|
proceedWithTurnIntoStyle(targets, uiBlock.style())
|
|
}
|
|
UiBlock.PAGE -> {
|
|
action?.invoke()
|
|
proceedWithTurningIntoDocument(targets)
|
|
}
|
|
UiBlock.LINE_DIVIDER -> {
|
|
action?.invoke()
|
|
proceedUpdateDividerStyle(targets, Content.Divider.Style.LINE)
|
|
}
|
|
UiBlock.THREE_DOTS -> {
|
|
action?.invoke()
|
|
proceedUpdateDividerStyle(targets, Content.Divider.Style.DOTS)
|
|
}
|
|
UiBlock.LINK_TO_OBJECT,
|
|
UiBlock.FILE,
|
|
UiBlock.IMAGE,
|
|
UiBlock.VIDEO,
|
|
UiBlock.BOOKMARK,
|
|
UiBlock.RELATION -> errorAction?.invoke()
|
|
}
|
|
}
|
|
|
|
private fun proceedWithTurnIntoStyle(
|
|
targets: List<String>,
|
|
style: Content.Text.Style
|
|
) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.TurnInto(
|
|
context = context,
|
|
targets = targets,
|
|
style = style,
|
|
analyticsContext = analyticsContext
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithUpdateTextStyle(
|
|
targets: List<String>,
|
|
style: Content.Text.Style
|
|
) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateStyle(
|
|
context = context,
|
|
targets = targets,
|
|
style = style
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedUpdateDividerStyle(targets: List<String>, style: Content.Divider.Style) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Divider.UpdateStyle(
|
|
context = context,
|
|
targets = targets,
|
|
style = style
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithTurningIntoDocument(targets: List<String>) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.TurnIntoDocument(
|
|
context = context,
|
|
targets = targets
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun addDividerBlock(style: Content.Divider.Style) {
|
|
|
|
val focused = blocks.first { it.id == orchestrator.stores.focus.current().id }
|
|
val content = focused.content
|
|
val prototype = when (style) {
|
|
Content.Divider.Style.LINE -> Prototype.DividerLine
|
|
Content.Divider.Style.DOTS -> Prototype.DividerDots
|
|
}
|
|
|
|
if (content is Content.Text && content.text.isEmpty()) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Replace(
|
|
context = context,
|
|
target = focused.id,
|
|
prototype = prototype
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
|
|
val position: Position
|
|
|
|
var target: Id = focused.id
|
|
|
|
if (focused.id == context) {
|
|
if (focused.children.isEmpty()) {
|
|
position = Position.INNER
|
|
} else {
|
|
position = Position.TOP
|
|
target = focused.children.first()
|
|
}
|
|
} else {
|
|
position = Position.BOTTOM
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Create(
|
|
context = context,
|
|
target = target,
|
|
position = position,
|
|
prototype = prototype
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private fun addTableOfContentsBlock() {
|
|
|
|
val focused = blocks.first { it.id == orchestrator.stores.focus.current().id }
|
|
val content = focused.content
|
|
val prototype = Prototype.TableOfContents
|
|
|
|
if (content is Content.Text && content.text.isEmpty()) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Replace(
|
|
context = context,
|
|
target = focused.id,
|
|
prototype = prototype
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
|
|
val position: Position
|
|
|
|
var target: Id = focused.id
|
|
|
|
if (focused.id == context) {
|
|
if (focused.children.isEmpty()) {
|
|
position = Position.INNER
|
|
} else {
|
|
position = Position.TOP
|
|
target = focused.children.first()
|
|
}
|
|
} else {
|
|
position = Position.BOTTOM
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Create(
|
|
context = context,
|
|
target = target,
|
|
position = position,
|
|
prototype = prototype
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun addSimpleTableBlock(item: SlashItem.Other.Table) {
|
|
|
|
val focused = blocks.first { it.id == orchestrator.stores.focus.current().id }
|
|
val content = focused.content
|
|
|
|
if (content is Content.Text && content.text.isEmpty()) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Table.CreateTable(
|
|
ctx = context,
|
|
target = focused.id,
|
|
position = Position.REPLACE,
|
|
rows = item.rowCount,
|
|
columns = item.columnCount
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
|
|
val position: Position
|
|
|
|
var target: Id = focused.id
|
|
|
|
if (focused.id == context) {
|
|
if (focused.children.isEmpty()) {
|
|
position = Position.INNER
|
|
} else {
|
|
position = Position.TOP
|
|
target = focused.children.first()
|
|
}
|
|
} else {
|
|
position = Position.BOTTOM
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Table.CreateTable(
|
|
ctx = context,
|
|
target = target,
|
|
position = position
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun onTableRowEmptyCellClicked(cellId: Id, rowId: Id, tableId: Id) {
|
|
fillTableBlockRow(
|
|
cellId = cellId,
|
|
targetIds = listOf(rowId),
|
|
tableId = tableId
|
|
)
|
|
}
|
|
|
|
private fun fillTableBlockRow(cellId: Id, targetIds: List<Id>, tableId: Id) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Table.FillTableRow(
|
|
ctx = context,
|
|
targetIds = targetIds
|
|
)
|
|
)
|
|
}
|
|
dispatch(
|
|
Command.OpenSetBlockTextValueScreen(
|
|
ctx = context,
|
|
block = cellId,
|
|
table = tableId
|
|
)
|
|
)
|
|
}
|
|
|
|
fun onAddDividerBlockClicked(style: Content.Divider.Style) {
|
|
Timber.d("onAddDividerBlockClicked, style:[$style]")
|
|
addDividerBlock(style)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected)
|
|
}
|
|
|
|
fun onOutsideClicked() {
|
|
Timber.d("onOutsideClicked, ")
|
|
if (mode is EditorMode.Styling) {
|
|
onExitBlockStyleToolbarClicked()
|
|
return
|
|
}
|
|
|
|
val restrictions = orchestrator.stores.objectRestrictions.current()
|
|
if (restrictions.contains(ObjectRestriction.BLOCKS)) {
|
|
Timber.d("Object contains restriction BLOCKS, can't create blocks")
|
|
return
|
|
}
|
|
|
|
val root = blocks.find { it.id == context } ?: return
|
|
|
|
if (root.children.isEmpty()) {
|
|
addNewBlockAtTheEnd()
|
|
} else {
|
|
val last = blocks.first { it.id == root.children.last() }
|
|
when (val content = last.content) {
|
|
is Content.Text -> {
|
|
when {
|
|
content.style == Content.Text.Style.TITLE -> addNewBlockAtTheEnd()
|
|
content.text.isNotEmpty() -> addNewBlockAtTheEnd()
|
|
content.text.isEmpty() -> {
|
|
val stores = orchestrator.stores
|
|
if (stores.focus.current().isEmpty) {
|
|
val focus = Editor.Focus(id = last.id, cursor = null)
|
|
viewModelScope.launch { orchestrator.stores.focus.update(focus) }
|
|
viewModelScope.launch { refresh() }
|
|
} else {
|
|
Timber.d("Outside click is ignored because focus is not empty")
|
|
}
|
|
}
|
|
else -> Timber.d("Outside-click has been ignored.")
|
|
}
|
|
}
|
|
is Content.Link -> {
|
|
addNewBlockAtTheEnd()
|
|
}
|
|
is Content.Bookmark -> {
|
|
addNewBlockAtTheEnd()
|
|
}
|
|
is Content.File -> {
|
|
addNewBlockAtTheEnd()
|
|
}
|
|
is Content.Divider -> {
|
|
addNewBlockAtTheEnd()
|
|
}
|
|
is Content.Layout -> {
|
|
addNewBlockAtTheEnd()
|
|
}
|
|
is Content.RelationBlock -> {
|
|
addNewBlockAtTheEnd()
|
|
}
|
|
is Content.Table -> {
|
|
addNewBlockAtTheEnd()
|
|
}
|
|
is Content.TableOfContents -> {
|
|
addNewBlockAtTheEnd()
|
|
}
|
|
else -> {
|
|
Timber.d("Outside-click has been ignored.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Todo this method need refactoring
|
|
fun onHideKeyboardClicked() {
|
|
Timber.d("onHideKeyboardClicked, ")
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnClearFocusClicked)
|
|
viewModelScope.launch { orchestrator.stores.focus.update(Editor.Focus.empty()) }
|
|
views.onEach { if (it is Focusable) it.isFocused = false }
|
|
viewModelScope.launch { renderCommand.send(Unit) }
|
|
}
|
|
|
|
private fun proceedWithClearingFocus() {
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(Editor.Focus.empty())
|
|
refresh()
|
|
}
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnClearFocusClicked)
|
|
}
|
|
|
|
private suspend fun refresh() {
|
|
if (BuildConfig.DEBUG) {
|
|
Timber.d("----------Blocks dispatched to render pipeline----------")
|
|
}
|
|
renderizePipeline.send(blocks)
|
|
}
|
|
|
|
private fun onPageClicked(block: Id) {
|
|
val block = blocks.firstOrNull { it.id == block }
|
|
when (val content = block?.content) {
|
|
is Content.Link -> {
|
|
proceedWithOpeningObjectByLayout(target = content.target)
|
|
}
|
|
is Content.Bookmark -> {
|
|
val target = content.targetObjectId
|
|
val details = orchestrator.stores.details.current().details[target]
|
|
if (target != null) {
|
|
val obj = ObjectWrapper.Bookmark(details?.map ?: mapOf())
|
|
if (obj.isArchived != true && obj.isDeleted != true) {
|
|
proceedWithOpeningObjectByLayout(target = target)
|
|
} else {
|
|
val source = obj.source
|
|
if (!source.isNullOrBlank()) {
|
|
commands.postValue(
|
|
EventWrapper(
|
|
Command.Browse(source)
|
|
)
|
|
)
|
|
} else {
|
|
sendToast("Source is missing for this object")
|
|
}
|
|
}
|
|
} else {
|
|
sendToast("Couldn't find the target of the link")
|
|
}
|
|
}
|
|
else -> {
|
|
sendToast("Couldn't find the target of the link")
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun proceedWithOpeningObjectByLayout(target: String) {
|
|
proceedWithClearingFocus()
|
|
val details = orchestrator.stores.details.current()
|
|
val wrapper = ObjectWrapper.Basic(map = details.details[target]?.map ?: emptyMap())
|
|
when (wrapper.layout) {
|
|
ObjectType.Layout.BASIC,
|
|
ObjectType.Layout.PROFILE,
|
|
ObjectType.Layout.NOTE,
|
|
ObjectType.Layout.TODO,
|
|
ObjectType.Layout.FILE,
|
|
ObjectType.Layout.BOOKMARK -> {
|
|
proceedWithOpeningPage(target = target)
|
|
}
|
|
ObjectType.Layout.SET -> {
|
|
proceedWithOpeningSet(target = target)
|
|
}
|
|
else -> {
|
|
sendToast("Cannot open object with layout: ${wrapper.layout}")
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onAddNewObjectClicked(type: String, layout: ObjectType.Layout) {
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected)
|
|
|
|
val position: Position
|
|
|
|
val focused = blocks.first { it.id == orchestrator.stores.focus.current().id }
|
|
|
|
var target = focused.id
|
|
|
|
if (focused.id == context) {
|
|
if (focused.children.isEmpty())
|
|
position = Position.INNER
|
|
else {
|
|
position = Position.TOP
|
|
target = focused.children.first()
|
|
}
|
|
} else {
|
|
position = Position.BOTTOM
|
|
}
|
|
|
|
val params = CreateObject.Params(
|
|
context = context,
|
|
position = position,
|
|
target = target,
|
|
type = type,
|
|
layout = layout
|
|
)
|
|
|
|
val startTime = System.currentTimeMillis()
|
|
|
|
viewModelScope.launch {
|
|
createObject(
|
|
params = params
|
|
).proceed(
|
|
failure = { Timber.e(it, "Error while creating new object with params: $params") },
|
|
success = { result ->
|
|
val middleTime = System.currentTimeMillis()
|
|
orchestrator.proxies.payloads.send(result.payload)
|
|
sendAnalyticsObjectCreateEvent(
|
|
analytics = analytics,
|
|
objType = type,
|
|
layout = layout.code.toDouble(),
|
|
route = EventsDictionary.Routes.objPowerTool,
|
|
startTime = startTime,
|
|
middleTime = middleTime,
|
|
context = analyticsContext
|
|
)
|
|
proceedWithOpeningPage(result.target)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
fun onAddNewDocumentClicked() {
|
|
|
|
Timber.d("onAddNewDocumentClicked, ")
|
|
|
|
viewModelScope.sendEvent(
|
|
analytics = analytics,
|
|
eventName = EventsDictionary.createObjectNavBar,
|
|
props = Props(mapOf(EventsPropertiesKey.context to analyticsContext))
|
|
)
|
|
|
|
jobs += viewModelScope.launch {
|
|
createNewObject.execute(Unit).fold(
|
|
onSuccess = { id ->
|
|
proceedWithOpeningPage(id)
|
|
},
|
|
onFailure = { e -> Timber.e(e, "Error while creating a new page") }
|
|
)
|
|
}
|
|
}
|
|
|
|
@Deprecated("Not used")
|
|
fun onAddNewPageClicked() {
|
|
Timber.d("onAddNewPageClicked, ")
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected)
|
|
|
|
val position: Position
|
|
|
|
val focused = blocks.first { it.id == orchestrator.stores.focus.current().id }
|
|
|
|
var target = focused.id
|
|
|
|
if (focused.id == context) {
|
|
if (focused.children.isEmpty())
|
|
position = Position.INNER
|
|
else {
|
|
position = Position.TOP
|
|
target = focused.children.first()
|
|
}
|
|
} else {
|
|
position = Position.BOTTOM
|
|
}
|
|
|
|
val params = CreateDocument.Params(
|
|
context = context,
|
|
position = position,
|
|
target = target
|
|
)
|
|
|
|
viewModelScope.launch {
|
|
createDocument(
|
|
params = params
|
|
).proceed(
|
|
failure = { Timber.e(it, "Error while creating new page with params: $params") },
|
|
success = { result ->
|
|
orchestrator.proxies.payloads.send(result.payload)
|
|
proceedWithOpeningPage(result.target)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onAddCoverClicked() {
|
|
Timber.d("onAddCoverClicked, ")
|
|
if (mode != EditorMode.Locked) {
|
|
dispatch(Command.OpenCoverGallery(context))
|
|
} else {
|
|
sendToast("Cannot change cover: your object is locked.")
|
|
}
|
|
}
|
|
|
|
fun onLayoutClicked() {
|
|
Timber.d("onLayoutClicked, ")
|
|
dispatch(Command.OpenObjectLayout(context))
|
|
}
|
|
|
|
fun onLayoutDialogDismissed() {
|
|
Timber.d("onLayoutDialogDismissed, ")
|
|
proceedWithOpeningObjectMenu()
|
|
}
|
|
|
|
fun onAddBookmarkBlockClicked() {
|
|
Timber.d("onAddBookmarkBlockClicked, ")
|
|
|
|
val focused = blocks.find { it.id == orchestrator.stores.focus.current().id } ?: return
|
|
|
|
val content = focused.content
|
|
|
|
if (content is Content.Text && content.text.isEmpty()) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Replace(
|
|
context = context,
|
|
target = focused.id,
|
|
prototype = Prototype.Bookmark.New
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
|
|
val position: Position
|
|
|
|
var target: Id = focused.id
|
|
|
|
if (focused.id == context) {
|
|
if (focused.children.isEmpty()) {
|
|
position = Position.INNER
|
|
} else {
|
|
position = Position.TOP
|
|
target = focused.children.first()
|
|
}
|
|
} else {
|
|
position = Position.BOTTOM
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Create(
|
|
context = context,
|
|
position = position,
|
|
target = target,
|
|
prototype = Prototype.Bookmark.New
|
|
)
|
|
)
|
|
}
|
|
}
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnAddBlockToolbarOptionSelected)
|
|
}
|
|
|
|
fun onAddBookmarkUrl(target: String, url: String) {
|
|
Timber.d("onAddBookmarkUrl, target:[$target] url:[$url]")
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Bookmark.SetupBookmark(
|
|
context = context,
|
|
target = target,
|
|
url = url
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun onBookmarkPlaceholderClicked(target: String) {
|
|
proceedWithBookmarkSetter(target)
|
|
}
|
|
|
|
private fun proceedWithBookmarkSetter(target: String) {
|
|
dispatch(
|
|
command = Command.OpenBookmarkSetter(
|
|
context = context,
|
|
target = target
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun onBookmarkClicked(view: BlockView.Media.Bookmark) =
|
|
dispatch(command = Command.Browse(view.url))
|
|
|
|
private fun onFailedBookmarkClicked(view: BlockView.Error.Bookmark) {
|
|
if (view.url.isBlank()) {
|
|
proceedWithBookmarkSetter(target = view.id)
|
|
} else {
|
|
dispatch(command = Command.Browse(view.url))
|
|
}
|
|
}
|
|
|
|
fun onTitleTextInputClicked() {
|
|
Timber.d("onTitleTextInputClicked, ")
|
|
if (mode is EditorMode.Styling) {
|
|
onExitBlockStyleToolbarClicked()
|
|
return
|
|
}
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnTextInputClicked)
|
|
}
|
|
|
|
fun onTextInputClicked(target: Id) {
|
|
Timber.d("onTextInputClicked, target:[$target]")
|
|
when (mode) {
|
|
is EditorMode.Select -> {
|
|
onBlockMultiSelectClicked(target)
|
|
}
|
|
is EditorMode.Styling -> {
|
|
onExitBlockStyleToolbarClicked()
|
|
}
|
|
else -> {
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnTextInputClicked)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun onBlockMultiSelectClicked(target: Id) {
|
|
proceedWithTogglingSelection(target)
|
|
proceedWithUpdatingActionsForCurrentSelection()
|
|
}
|
|
|
|
private fun proceedWithTogglingSelection(target: Id) {
|
|
(stateData.value as? ViewState.Success)?.let { state ->
|
|
|
|
var allow = true
|
|
|
|
val parent = blocks.find { it.children.contains(target) }
|
|
|
|
if (parent != null && parent.id != context) {
|
|
if (isSelected(parent.id)) allow = false
|
|
}
|
|
|
|
if (!allow) return
|
|
|
|
toggleSelection(target)
|
|
|
|
val descendants = blocks.asMap().descendants(parent = target)
|
|
|
|
if (isSelected(target)) {
|
|
descendants.forEach { child -> select(child) }
|
|
} else {
|
|
descendants.forEach { child -> unselect(child) }
|
|
}
|
|
|
|
if (currentSelection().isNotEmpty()) {
|
|
onMultiSelectModeBlockClicked()
|
|
val update = state.blocks.map { view ->
|
|
if (view.id == target || descendants.contains(view.id))
|
|
view.updateSelection(newSelection = isSelected(target))
|
|
else
|
|
view
|
|
}
|
|
stateData.postValue(ViewState.Success(update))
|
|
if (isSelected(target)) {
|
|
dispatch(Command.ScrollToActionMenu(target = target))
|
|
}
|
|
} else {
|
|
proceedWithExitingMultiSelectMode()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onPaste(
|
|
range: IntRange
|
|
) {
|
|
Timber.d("onPaste, range:[$range]")
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Clipboard.Paste(
|
|
context = context,
|
|
focus = orchestrator.stores.focus.current().id,
|
|
range = range,
|
|
selected = emptyList()
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onApplyScrollAndMove(
|
|
target: Id,
|
|
ratio: Float
|
|
) {
|
|
|
|
Timber.d("onApplyScrollAndMove, target:[$target] ratio:[$ratio]")
|
|
|
|
val ordering = views.mapIndexed { index, view -> view.id to index }.toMap()
|
|
|
|
val exclude = mutableSetOf<String>()
|
|
|
|
var moveTarget = target
|
|
|
|
var position = when (ratio) {
|
|
in START_RANGE -> Position.TOP
|
|
in END_RANGE -> Position.BOTTOM
|
|
in INNER_RANGE -> Position.INNER
|
|
else -> {
|
|
if (ratio > 1) Position.BOTTOM
|
|
else throw IllegalStateException("Unexpected ratio: $ratio")
|
|
}
|
|
}
|
|
|
|
|
|
val targetBlock = blocks.first { it.id == target }
|
|
|
|
val parent = blocks.find { it.children.contains(target) }?.id
|
|
|
|
val selected = currentSelection().toList()
|
|
|
|
if (selected.contains(target)) {
|
|
if (position == Position.INNER) {
|
|
sendToast(CANNOT_BE_DROPPED_INSIDE_ITSELF_ERROR)
|
|
} else if (selected.size == 1) {
|
|
sendToast(CANNOT_MOVE_BLOCK_ON_SAME_POSITION)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (selected.contains(parent)) {
|
|
sendToast(CANNOT_MOVE_PARENT_INTO_CHILD)
|
|
return
|
|
}
|
|
|
|
if (position == Position.INNER) {
|
|
|
|
if (!targetBlock.supportNesting()) {
|
|
sendToast(CANNOT_BE_PARENT_ERROR)
|
|
return
|
|
}
|
|
|
|
val targetContext = if (targetBlock.content is Content.Link) {
|
|
targetBlock.content<Content.Link>().target
|
|
} else {
|
|
context
|
|
}
|
|
|
|
blocks.filter { selected.contains(it.id) }.forEach { block ->
|
|
block.children.forEach { if (selected.contains(it)) exclude.add(it) }
|
|
}
|
|
|
|
clearSelections()
|
|
|
|
mode = EditorMode.Edit
|
|
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.SAM.OnApply)
|
|
|
|
viewModelScope.launch {
|
|
val blocks = (selected - exclude).sortedBy { id -> ordering[id] }
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.Move(
|
|
context = context,
|
|
target = moveTarget,
|
|
targetContext = targetContext,
|
|
blocks = blocks,
|
|
position = position
|
|
)
|
|
)
|
|
sendAnalyticsBlockReorder(
|
|
analytics = analytics,
|
|
count = blocks.size,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
} else {
|
|
|
|
val targetContext = context
|
|
|
|
if (target == context) {
|
|
position = Position.TOP
|
|
moveTarget = targetBlock.children.first()
|
|
}
|
|
|
|
blocks.filter { selected.contains(it.id) }.forEach { block ->
|
|
block.children.forEach { if (selected.contains(it)) exclude.add(it) }
|
|
}
|
|
|
|
clearSelections()
|
|
|
|
mode = EditorMode.Edit
|
|
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.SAM.OnApply)
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.Move(
|
|
context = context,
|
|
target = moveTarget,
|
|
targetContext = targetContext,
|
|
blocks = (selected - exclude).sortedBy { id -> ordering[id] },
|
|
position = position
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onCopy(
|
|
range: IntRange?
|
|
) {
|
|
Timber.d("onCopy, ")
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Clipboard.Copy(
|
|
context = context,
|
|
range = range,
|
|
blocks = listOf(blocks.first { it.id == focus.value })
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onBookmarkPasted(url: Url) {
|
|
Timber.d("onBookmarkPasted $url")
|
|
val focus = orchestrator.stores.focus.current()
|
|
if (!focus.isEmpty) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Bookmark.CreateBookmark(
|
|
context = context,
|
|
target = focus.id,
|
|
position = Position.TOP,
|
|
url = url
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onClickListener(clicked: ListenerType) {
|
|
Timber.d("onClickListener, clicked:[$clicked]")
|
|
if (mode is EditorMode.Styling) {
|
|
onExitBlockStyleToolbarClicked()
|
|
return
|
|
}
|
|
isUndoRedoToolbarIsVisible.value = false
|
|
when (clicked) {
|
|
is ListenerType.Bookmark.View -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> {
|
|
onBookmarkClicked(clicked.item)
|
|
viewModelScope.sendAnalyticsBookmarkOpen(analytics)
|
|
}
|
|
EditorMode.Locked -> onBookmarkClicked(clicked.item)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.item.id)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Bookmark.Placeholder -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onBookmarkPlaceholderClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Bookmark.Error -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onFailedBookmarkClicked(clicked.item)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.item.id)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.File.View -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onFileClicked(clicked.target)
|
|
EditorMode.Locked -> onFileClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.File.Placeholder -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onAddLocalFileClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.File.Error -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onAddLocalFileClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.File.Upload -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> Unit
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Picture.View -> {
|
|
when (mode) {
|
|
EditorMode.Edit, EditorMode.Locked -> {
|
|
val target = blocks.find { it.id == clicked.target }
|
|
if (target != null) {
|
|
val content = target.content
|
|
check(content is Content.File)
|
|
dispatch(
|
|
Command.OpenFullScreenImage(
|
|
target = clicked.target,
|
|
url = urlBuilder.original(content.hash)
|
|
)
|
|
)
|
|
} else {
|
|
Timber.e("Could not find target for picture")
|
|
}
|
|
}
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Picture.Placeholder -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onAddLocalPictureClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Picture.Error -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onAddLocalPictureClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Picture.Upload -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> Unit
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Video.View -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> Unit
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Video.Placeholder -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onAddLocalVideoClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Video.Error -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onAddLocalVideoClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Video.Upload -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> Unit
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.ProfileImageIcon -> {
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnDocumentIconClicked)
|
|
dispatch(Command.OpenDocumentImagePicker(Mimetype.MIME_IMAGE_ALL))
|
|
}
|
|
is ListenerType.LongClick -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> proceedWithEnteringActionMode(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(target = clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.LinkToObject -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onPageClicked(clicked.target)
|
|
EditorMode.Locked -> onPageClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.LinkToObjectArchived -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> onPageClicked(clicked.target)
|
|
EditorMode.Locked -> onPageClicked(clicked.target)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.LinkToObjectDeleted -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> Unit
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Mention -> {
|
|
when (mode) {
|
|
EditorMode.Edit, EditorMode.Locked -> {
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(Editor.Focus.empty())
|
|
}
|
|
onMentionClicked(clicked.target)
|
|
}
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.EditableBlock -> {
|
|
//Todo block view refactoring
|
|
}
|
|
ListenerType.TitleBlock -> {
|
|
//Todo block view refactoring
|
|
}
|
|
is ListenerType.DividerClick -> {
|
|
when (mode) {
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Latex -> {
|
|
when (mode) {
|
|
// EditorMode.Edit -> proceedWithEnteringActionMode(clicked.id)
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.id)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Code.SelectLanguage -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> dispatch(Command.Dialog.SelectLanguage(clicked.target))
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Relation.Placeholder -> {
|
|
when (mode) {
|
|
EditorMode.Edit -> dispatch(
|
|
Command.OpenObjectRelationScreen.RelationAdd(
|
|
ctx = context,
|
|
target = clicked.target
|
|
)
|
|
)
|
|
else -> onBlockMultiSelectClicked(clicked.target)
|
|
}
|
|
}
|
|
is ListenerType.Relation.Related -> {
|
|
val restrictions = orchestrator.stores.objectRestrictions.current()
|
|
if (restrictions.contains(ObjectRestriction.RELATIONS)) {
|
|
sendToast(NOT_ALLOWED_FOR_RELATION)
|
|
Timber.d("No interaction allowed with this relation")
|
|
return
|
|
}
|
|
when (mode) {
|
|
EditorMode.Edit, EditorMode.Locked -> {
|
|
val relationId =
|
|
(clicked.value as BlockView.Relation.Related).view.relationId
|
|
val relation =
|
|
orchestrator.stores.relations.current().first { it.key == relationId }
|
|
if (relation.isReadOnly) {
|
|
sendToast(NOT_ALLOWED_FOR_RELATION)
|
|
Timber.d("No interaction allowed with this relation")
|
|
return
|
|
}
|
|
when (relation.format) {
|
|
Relation.Format.SHORT_TEXT,
|
|
Relation.Format.LONG_TEXT,
|
|
Relation.Format.URL,
|
|
Relation.Format.PHONE,
|
|
Relation.Format.NUMBER,
|
|
Relation.Format.EMAIL -> {
|
|
dispatch(
|
|
Command.OpenObjectRelationScreen.Value.Text(
|
|
ctx = context,
|
|
target = context,
|
|
relation = relationId,
|
|
isLocked = mode == EditorMode.Locked
|
|
)
|
|
)
|
|
}
|
|
Relation.Format.CHECKBOX -> {
|
|
proceedWithTogglingBlockRelationCheckbox(clicked.value, relationId)
|
|
}
|
|
Relation.Format.DATE -> {
|
|
dispatch(
|
|
Command.OpenObjectRelationScreen.Value.Date(
|
|
ctx = context,
|
|
target = context,
|
|
relation = relationId
|
|
)
|
|
)
|
|
}
|
|
else -> {
|
|
dispatch(
|
|
Command.OpenObjectRelationScreen.Value.Default(
|
|
ctx = context,
|
|
target = context,
|
|
relation = relationId,
|
|
targetObjectTypes = relation.objectTypes,
|
|
isLocked = mode == EditorMode.Locked
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
else -> onBlockMultiSelectClicked(clicked.value.id)
|
|
}
|
|
}
|
|
is ListenerType.Relation.Featured -> {
|
|
val restrictions = orchestrator.stores.objectRestrictions.current()
|
|
if (restrictions.contains(ObjectRestriction.RELATIONS)) {
|
|
sendToast(NOT_ALLOWED_FOR_RELATION)
|
|
return
|
|
}
|
|
when (mode) {
|
|
EditorMode.Edit, EditorMode.Locked -> {
|
|
val relationId = clicked.relation.relationId
|
|
val relation =
|
|
orchestrator.stores.relations.current().first { it.key == relationId }
|
|
if (relation.isReadOnly) {
|
|
sendToast(NOT_ALLOWED_FOR_RELATION)
|
|
return
|
|
}
|
|
when (relation.format) {
|
|
Relation.Format.SHORT_TEXT,
|
|
Relation.Format.LONG_TEXT,
|
|
Relation.Format.URL,
|
|
Relation.Format.PHONE,
|
|
Relation.Format.NUMBER,
|
|
Relation.Format.EMAIL -> {
|
|
dispatch(
|
|
Command.OpenObjectRelationScreen.Value.Text(
|
|
ctx = context,
|
|
target = context,
|
|
relation = relationId,
|
|
isLocked = mode == EditorMode.Locked
|
|
)
|
|
)
|
|
}
|
|
Relation.Format.CHECKBOX -> {
|
|
val view = clicked.relation
|
|
if (view is DocumentRelationView.Checkbox) {
|
|
viewModelScope.launch {
|
|
updateDetail(
|
|
UpdateDetail.Params(
|
|
ctx = context,
|
|
key = relationId,
|
|
value = !view.isChecked
|
|
)
|
|
).process(
|
|
success = {
|
|
dispatcher.send(it)
|
|
sendAnalyticsRelationValueEvent(
|
|
analytics = analytics,
|
|
context = analyticsContext
|
|
)
|
|
},
|
|
failure = {
|
|
Timber.e(
|
|
it,
|
|
"Error while updating relation values"
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
Relation.Format.DATE -> {
|
|
dispatch(
|
|
Command.OpenObjectRelationScreen.Value.Date(
|
|
ctx = context,
|
|
target = context,
|
|
relation = relationId
|
|
)
|
|
)
|
|
}
|
|
else -> {
|
|
dispatch(
|
|
Command.OpenObjectRelationScreen.Value.Default(
|
|
ctx = context,
|
|
target = context,
|
|
relation = relationId,
|
|
targetObjectTypes = relation.objectTypes,
|
|
isLocked = mode == EditorMode.Locked
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
else -> {
|
|
// Do nothing
|
|
}
|
|
}
|
|
}
|
|
is ListenerType.Relation.ChangeObjectType -> {
|
|
if (mode != EditorMode.Locked) {
|
|
val restrictions = orchestrator.stores.objectRestrictions.current()
|
|
if (restrictions.contains(ObjectRestriction.TYPE_CHANGE)) {
|
|
sendToast(NOT_ALLOWED_FOR_OBJECT)
|
|
Timber.d("No interaction allowed with this object type")
|
|
return
|
|
}
|
|
dispatch(
|
|
Command.OpenChangeObjectTypeScreen(
|
|
ctx = context,
|
|
smartBlockType = getObjectSmartBlockType(),
|
|
excludedTypes = listOf(ObjectType.BOOKMARK_TYPE),
|
|
isDraft = isObjectTypesWidgetVisible
|
|
)
|
|
)
|
|
} else {
|
|
sendToast("Your object is locked. To change its type, simply unlock it.")
|
|
}
|
|
}
|
|
is ListenerType.Relation.ObjectTypeOpenSet -> {
|
|
viewModelScope.launch {
|
|
findObjectSetForType(FindObjectSetForType.Params(clicked.type)).process(
|
|
failure = { Timber.e(it, "Error while search for a set for this type") },
|
|
success = { response ->
|
|
when (response) {
|
|
is FindObjectSetForType.Response.NotFound -> {
|
|
snacks.emit(Snack.ObjectSetNotFound(clicked.type))
|
|
}
|
|
is FindObjectSetForType.Response.Success -> {
|
|
proceedWithOpeningSet(response.obj.id)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
is ListenerType.TableOfContentsItem -> {
|
|
when (mode) {
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
EditorMode.Edit, EditorMode.Locked -> {
|
|
val block = views.find { it.id == clicked.item }
|
|
val pos = views.indexOf(block)
|
|
if (pos != NO_SCROLL_POSITION) {
|
|
commands.value = EventWrapper(Command.ScrollToPosition(pos))
|
|
}
|
|
}
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.TableOfContents -> {
|
|
when (mode) {
|
|
EditorMode.Select -> onBlockMultiSelectClicked(clicked.target)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.Callout.Icon -> {
|
|
dispatch(Command.OpenTextBlockIconPicker(clicked.blockId))
|
|
}
|
|
is ListenerType.TableEmptyCell -> {
|
|
when (mode) {
|
|
EditorMode.Edit, EditorMode.Locked -> {
|
|
if (currentSelection().isNotEmpty()) {
|
|
Timber.e("Some other blocks are selected, amend table cell click")
|
|
return
|
|
}
|
|
proceedWithSelectingCell(
|
|
cellId = clicked.cellId,
|
|
tableId = clicked.tableId
|
|
)
|
|
onTableRowEmptyCellClicked(
|
|
cellId = clicked.cellId,
|
|
rowId = clicked.rowId,
|
|
tableId = clicked.tableId
|
|
)
|
|
}
|
|
EditorMode.Select -> onBlockMultiSelectClicked(target = clicked.tableId)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.TableTextCell -> {
|
|
when (mode) {
|
|
EditorMode.Edit, EditorMode.Locked -> {
|
|
if (currentSelection().isNotEmpty()) {
|
|
Timber.e("Some other blocks are selected, amend table cell click")
|
|
return
|
|
}
|
|
proceedWithSelectingCell(
|
|
cellId = clicked.cellId,
|
|
tableId = clicked.tableId
|
|
)
|
|
dispatch(
|
|
Command.OpenSetBlockTextValueScreen(
|
|
ctx = context,
|
|
block = clicked.cellId,
|
|
table = clicked.tableId
|
|
)
|
|
)
|
|
}
|
|
EditorMode.Select -> onBlockMultiSelectClicked(target = clicked.tableId)
|
|
else -> Unit
|
|
}
|
|
}
|
|
is ListenerType.TableEmptyCellMenu -> {}
|
|
is ListenerType.TableTextCellMenu -> {
|
|
onShowSimpleTableWidgetClicked(id = clicked.cellId)
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
|
|
private fun proceedWithTogglingBlockRelationCheckbox(
|
|
value: BlockView.Relation.Related,
|
|
relation: Id
|
|
) {
|
|
viewModelScope.launch {
|
|
val view = value.view as DocumentRelationView.Checkbox
|
|
updateDetail(
|
|
UpdateDetail.Params(
|
|
ctx = context,
|
|
key = relation,
|
|
value = !view.isChecked
|
|
)
|
|
).process(
|
|
success = {
|
|
dispatcher.send(it)
|
|
sendAnalyticsRelationValueEvent(
|
|
analytics = analytics,
|
|
context = analyticsContext
|
|
)
|
|
},
|
|
failure = { Timber.e(it, "Error while updating relation values") }
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun onProceedWithFilePath(filePath: String?) {
|
|
Timber.d("onProceedWithFilePath, filePath:[$filePath]")
|
|
if (filePath == null) {
|
|
Timber.w("Error while getting filePath")
|
|
return
|
|
}
|
|
if (filePath.endsWith(FORMAT_WEBP, true)) {
|
|
sendToast(ERROR_UNSUPPORTED_WEBP)
|
|
return
|
|
}
|
|
viewModelScope.launch {
|
|
val uploadDescription = currentMediaUploadDescription
|
|
if (uploadDescription != null) {
|
|
orchestrator.proxies.intents.send(
|
|
Media.Upload(
|
|
context = context,
|
|
description = uploadDescription,
|
|
filePath = filePath,
|
|
url = "",
|
|
)
|
|
)
|
|
} else {
|
|
Timber.w("Failed to upload file $filePath. uploadDescription==null")
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onRestoreSavedState(uploadMediaDescription: Media.Upload.Description?) {
|
|
currentMediaUploadDescription = uploadMediaDescription
|
|
}
|
|
|
|
fun onPageIconClicked() {
|
|
Timber.d("onPageIconClicked, ")
|
|
val restrictions = orchestrator.stores.objectRestrictions.current()
|
|
val isDetailsAllowed = restrictions.none { it == ObjectRestriction.DETAILS }
|
|
if (isDetailsAllowed) {
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnDocumentIconClicked)
|
|
dispatch(Command.OpenDocumentEmojiIconPicker)
|
|
} else {
|
|
sendToast(NOT_ALLOWED_FOR_OBJECT)
|
|
}
|
|
}
|
|
|
|
private fun onFileClicked(id: String) {
|
|
val file = blocks.find { it.id == id }
|
|
if (file != null && file.content is Content.File) {
|
|
val cnt = (file.content as Content.File)
|
|
dispatch(
|
|
Command.OpenFileByDefaultApp(
|
|
id = id,
|
|
mime = cnt.mime.orEmpty(),
|
|
uri = urlBuilder.file(cnt.hash)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun startSharingFile(id: String, onDownloaded: (Uri) -> Unit = {}) {
|
|
|
|
Timber.d("startDownloadingFile, id:[$id]")
|
|
|
|
sendToast("Preparing file to share...")
|
|
|
|
val block = blocks.firstOrNull { it.id == id }
|
|
val content = block?.content
|
|
|
|
if (content is Content.File && content.state == Content.File.State.DONE) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Media.ShareFile(
|
|
hash = content.hash.orEmpty(),
|
|
name = content.name.orEmpty(),
|
|
type = content.type,
|
|
onDownloaded = onDownloaded
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
Timber.e("Block is not File or with wrong state, can't proceed with share!")
|
|
}
|
|
}
|
|
|
|
fun startDownloadingFile(id: String) {
|
|
|
|
Timber.d("startDownloadingFile, id:[$id]")
|
|
|
|
sendToast("Downloading file in background...")
|
|
|
|
val block = blocks.firstOrNull { it.id == id }
|
|
val content = block?.content
|
|
|
|
if (content is Content.File && content.state == Content.File.State.DONE) {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Media.DownloadFile(
|
|
url = when (content.type) {
|
|
Content.File.Type.IMAGE -> urlBuilder.image(content.hash)
|
|
else -> urlBuilder.file(content.hash)
|
|
},
|
|
name = content.name.orEmpty(),
|
|
type = content.type
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
Timber.e("Block is not File or with wrong state, can't proceed with download")
|
|
}
|
|
}
|
|
|
|
private fun startDownloadingFiles(ids: List<String>) {
|
|
Timber.d("startDownloadingFiles, ids:[$ids]")
|
|
ids.forEach { id -> startDownloadingFile(id) }
|
|
}
|
|
|
|
fun onPageSearchClicked() {
|
|
|
|
Timber.d("onPageSearchClicked, ")
|
|
|
|
viewModelScope.sendEvent(
|
|
analytics = analytics,
|
|
eventName = searchScreenShow,
|
|
props = Props(mapOf(EventsPropertiesKey.context to analyticsContext))
|
|
)
|
|
navigation.postValue(EventWrapper(AppNavigation.Command.OpenPageSearch))
|
|
}
|
|
|
|
private fun onMultiSelectModeBlockClicked() {
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.MultiSelect.OnBlockClick(
|
|
count = currentSelection().size
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun addNewBlockAtTheEnd() {
|
|
proceedWithCreatingNewTextBlock(
|
|
id = "",
|
|
position = Position.INNER,
|
|
style = Content.Text.Style.P
|
|
)
|
|
}
|
|
|
|
private fun proceedWithOpeningPage(target: Id) {
|
|
viewModelScope.launch {
|
|
closePage(CloseBlock.Params(context)).process(
|
|
failure = {
|
|
Timber.e(it, "Error while closing object")
|
|
navigate(EventWrapper(AppNavigation.Command.OpenObject(target)))
|
|
},
|
|
success = {
|
|
navigate(EventWrapper(AppNavigation.Command.OpenObject(target)))
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithOpeningSet(target: Id, isPopUpToDashboard: Boolean = false) {
|
|
viewModelScope.launch {
|
|
closePage(CloseBlock.Params(context)).process(
|
|
failure = {
|
|
Timber.e(it, "Error while closing object")
|
|
navigate(
|
|
EventWrapper(
|
|
AppNavigation.Command.OpenObjectSet(
|
|
target,
|
|
isPopUpToDashboard
|
|
)
|
|
)
|
|
)
|
|
},
|
|
success = {
|
|
navigate(
|
|
EventWrapper(
|
|
AppNavigation.Command.OpenObjectSet(
|
|
target,
|
|
isPopUpToDashboard
|
|
)
|
|
)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun sendToast(msg: String) {
|
|
jobs += viewModelScope.launch {
|
|
_toasts.emit(msg)
|
|
}
|
|
}
|
|
|
|
private fun sendSnack(snack: Snack) {
|
|
jobs += viewModelScope.launch {
|
|
snacks.emit(snack)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true, when mention menu is closed, and we need absorb back button click
|
|
*/
|
|
fun onBackPressedCallback(): Boolean {
|
|
Timber.d("onBackPressedCallback, ")
|
|
return controlPanelViewState.value?.let { state ->
|
|
val isVisible = state.mentionToolbar.isVisible
|
|
val isSlashWidgetVisible = state.slashWidget.isVisible
|
|
if (isVisible) {
|
|
onMentionEvent(MentionEvent.MentionSuggestStop)
|
|
return true
|
|
}
|
|
if (isSlashWidgetVisible) {
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
return true
|
|
}
|
|
if (!orchestrator.stores.focus.current().isEmpty) {
|
|
onHideKeyboardClicked()
|
|
}
|
|
return false
|
|
} ?: run { false }
|
|
}
|
|
|
|
fun onSelectProgrammingLanguageClicked(target: Id, key: String) {
|
|
Timber.d("onSelectProgrammingLanguageClicked, target:[$target] key:[$key]")
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.UpdateFields(
|
|
context = context,
|
|
fields = listOf(
|
|
Pair(
|
|
target,
|
|
Block.Fields(
|
|
mapOf("lang" to key)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onRelationTextValueChanged(
|
|
ctx: Id,
|
|
value: Any?,
|
|
relationId: Id
|
|
) {
|
|
Timber.d("onRelationTextValueChanged, ctx:[$ctx] value:[$value] relationId:[$relationId]")
|
|
viewModelScope.launch {
|
|
updateDetail(
|
|
UpdateDetail.Params(
|
|
ctx = ctx,
|
|
key = relationId,
|
|
value = value
|
|
)
|
|
).process(
|
|
success = {
|
|
dispatcher.send(it)
|
|
sendAnalyticsRelationValueEvent(
|
|
analytics = analytics,
|
|
context = analyticsContext
|
|
)
|
|
},
|
|
failure = { Timber.e(it, "Error while updating relation values") }
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onObjectTypeChanged(type: Id, isObjectDraft: Boolean) {
|
|
Timber.d("onObjectTypeChanged, typeId:[$type], isObjectDraft:[$isObjectDraft]")
|
|
if (type == ObjectType.SET_URL) {
|
|
viewModelScope.launch {
|
|
val params = ConvertObjectToSet.Params(
|
|
ctx = context,
|
|
sources = emptyList()
|
|
)
|
|
objectToSet.invoke(params).proceed(
|
|
failure = { error -> Timber.e(error, "Error convert object to set") },
|
|
success = { setId ->
|
|
proceedWithOpeningSet(target = setId, isPopUpToDashboard = true)
|
|
}
|
|
)
|
|
}
|
|
} else {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.SetObjectType(
|
|
context = context,
|
|
typeId = type
|
|
)
|
|
)
|
|
sendAnalyticsObjectTypeChangeEvent(
|
|
analytics = analytics,
|
|
typeId = type,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
if (isObjectDraft) {
|
|
proceedWithTemplateSelection(type)
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
const val NO_SEARCH_RESULT_POSITION = -1
|
|
const val NO_SCROLL_POSITION = -1
|
|
const val EMPTY_TEXT = ""
|
|
const val EMPTY_CONTEXT = ""
|
|
const val EMPTY_FOCUS_ID = ""
|
|
const val TEXT_CHANGES_DEBOUNCE_DURATION = 500L
|
|
const val DELAY_REFRESH_DOCUMENT_TO_ENTER_MULTI_SELECT_MODE = 150L
|
|
const val DELAY_REFRESH_DOCUMENT_ON_EXIT_MULTI_SELECT_MODE = 300L
|
|
const val INITIAL_INDENT = 0
|
|
const val FORMAT_WEBP = "webp"
|
|
const val CANNOT_MOVE_BLOCK_ON_SAME_POSITION = "Selected block is already on the position"
|
|
const val CANNOT_BE_DROPPED_INSIDE_ITSELF_ERROR = "A block cannot be moved inside itself."
|
|
const val CANNOT_BE_PARENT_ERROR = "This block does not support nesting."
|
|
const val CANNOT_MOVE_PARENT_INTO_CHILD = "Cannot move parent into child."
|
|
|
|
const val CANNOT_OPEN_ACTION_MENU_FOR_TITLE_ERROR =
|
|
"Opening action menu for title currently not supported"
|
|
const val CANNOT_OPEN_ACTION_MENU_FOR_DESCRIPTION =
|
|
"Cannot open action menu for description"
|
|
const val CANNOT_OPEN_STYLE_PANEL_FOR_DESCRIPTION =
|
|
"Description block is text primitive and therefore no styling can be applied."
|
|
const val CANNOT_OPEN_STYLE_PANEL_FOR_CODE_BLOCK_ERROR =
|
|
"Opening style panel for code block currently not supported"
|
|
|
|
const val ERROR_UNSUPPORTED_BEHAVIOR = "Currently unsupported behavior."
|
|
const val NOT_ALLOWED_FOR_OBJECT = "Not allowed for this object"
|
|
const val NOT_ALLOWED_FOR_RELATION = "Not allowed for this relation"
|
|
const val ERROR_UNSUPPORTED_WEBP = "Currently WEBP format is unsupported"
|
|
}
|
|
|
|
data class MarkupAction(
|
|
val type: Markup.Type,
|
|
val param: String? = null
|
|
)
|
|
|
|
override fun onCleared() {
|
|
super.onCleared()
|
|
|
|
orchestrator.stores.focus.cancel()
|
|
orchestrator.stores.details.cancel()
|
|
orchestrator.stores.textSelection.cancel()
|
|
orchestrator.proxies.changes.cancel()
|
|
orchestrator.proxies.saves.cancel()
|
|
|
|
markupActionPipeline.cancel()
|
|
renderizePipeline.cancel()
|
|
|
|
controlPanelInteractor.channel.cancel()
|
|
|
|
Timber.d("onCleared, ")
|
|
}
|
|
|
|
fun onStop() {
|
|
Timber.d("onStop, ")
|
|
jobs.apply {
|
|
forEach { it.cancel() }
|
|
clear()
|
|
}
|
|
if (copyFileToCache.isActive()) {
|
|
copyFileToCache.cancel()
|
|
}
|
|
}
|
|
|
|
enum class Session { IDLE, OPEN, ERROR }
|
|
|
|
//region SLASH WIDGET
|
|
fun onStartSlashWidgetClicked() {
|
|
dispatch(Command.AddSlashWidgetTriggerToFocusedBlock)
|
|
viewModelScope.sendAnalyticsSlashMenuEvent(analytics)
|
|
}
|
|
|
|
fun onSlashItemClicked(item: SlashItem) {
|
|
Timber.v("onSlashItemClicked, item:[$item]")
|
|
val target = orchestrator.stores.focus.current()
|
|
if (!target.isEmpty) {
|
|
proceedWithSlashItem(item, target.id)
|
|
} else {
|
|
Timber.e("Slash Widget Error, target is empty")
|
|
}
|
|
}
|
|
|
|
fun onSlashTextWatcherEvent(event: SlashEvent) {
|
|
Timber.d("onSlashTextWatcherEvent, event:[$event]")
|
|
when (event) {
|
|
is SlashEvent.Start -> {
|
|
slashStartIndex = event.slashStart
|
|
filterSearchEmptyCount = 0
|
|
val panelEvent = ControlPanelMachine.Event.Slash.OnStart(
|
|
cursorCoordinate = event.cursorCoordinate,
|
|
slashFrom = event.slashStart
|
|
)
|
|
controlPanelInteractor.onEvent(panelEvent)
|
|
}
|
|
is SlashEvent.Filter -> {
|
|
slashFilter = event.filter.toString()
|
|
slashViewType = event.viewType
|
|
if (event.filter.isEmpty() || event.filter.first() != SLASH_CHAR) {
|
|
val widgetState = SlashWidgetState.UpdateItems.empty()
|
|
val panelEvent = ControlPanelMachine.Event.Slash.OnFilterChange(
|
|
widgetState = widgetState
|
|
)
|
|
controlPanelInteractor.onEvent(panelEvent)
|
|
return
|
|
}
|
|
if (event.filter.length == 1) {
|
|
val mainItems = SlashExtensions.getSlashWidgetMainItems()
|
|
val widgetState = SlashWidgetState.UpdateItems.empty()
|
|
.copy(mainItems = mainItems)
|
|
val panelEvent = ControlPanelMachine.Event.Slash.OnFilterChange(
|
|
widgetState = widgetState
|
|
)
|
|
controlPanelInteractor.onEvent(panelEvent)
|
|
return
|
|
}
|
|
getObjectTypes(excluded = listOf(ObjectType.BOOKMARK_TYPE)) { objectTypes ->
|
|
getRelations { relations ->
|
|
val widgetState = SlashExtensions.getUpdatedSlashWidgetState(
|
|
text = event.filter,
|
|
objectTypes = objectTypes.toSlashItemView(),
|
|
relations = relations,
|
|
viewType = slashViewType
|
|
)
|
|
incFilterSearchEmptyCount(widgetState)
|
|
val panelEvent = if (filterSearchEmptyCount == SLASH_EMPTY_SEARCH_MAX) {
|
|
filterSearchEmptyCount = 0
|
|
slashStartIndex = 0
|
|
slashFilter = ""
|
|
slashViewType = 0
|
|
ControlPanelMachine.Event.Slash.OnStop
|
|
} else {
|
|
ControlPanelMachine.Event.Slash.OnFilterChange(widgetState)
|
|
}
|
|
controlPanelInteractor.onEvent(panelEvent)
|
|
}
|
|
}
|
|
}
|
|
SlashEvent.Stop -> {
|
|
slashStartIndex = 0
|
|
slashFilter = ""
|
|
slashViewType = 0
|
|
filterSearchEmptyCount = 0
|
|
val panelEvent = ControlPanelMachine.Event.Slash.OnStop
|
|
controlPanelInteractor.onEvent(panelEvent)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun proceedWithAddingRelationToTarget(target: Id, relation: Id) {
|
|
Timber.d("proceedWithAddingRelationToTarget, target:[$target], relation:[$relation]")
|
|
val newBlockView = cutSlashFilterFromViews(target)
|
|
|
|
// cut text from List<Block> and send TextUpdate Intent
|
|
if (newBlockView != null) {
|
|
cutSlashFilterFromBlocksAndSendUpdate(
|
|
targetId = target,
|
|
text = newBlockView.text,
|
|
marks = newBlockView.marks.map { it.mark() }
|
|
)
|
|
onSlashRelationItemClicked(
|
|
relation = relation,
|
|
targetId = target,
|
|
isBlockEmpty = newBlockView.text.isEmpty()
|
|
)
|
|
} else {
|
|
Timber.e("cutSlashFilter error, BlockView is null on targetId:$target")
|
|
}
|
|
}
|
|
|
|
private fun proceedWithSlashItem(item: SlashItem, targetId: Id) {
|
|
when (item) {
|
|
is SlashItem.Main.Style -> {
|
|
val items =
|
|
listOf(SlashItem.Subheader.StyleWithBack) + getSlashWidgetStyleItems(
|
|
slashViewType
|
|
)
|
|
onSlashWidgetStateChanged(
|
|
SlashWidgetState.UpdateItems.empty().copy(
|
|
styleItems = items
|
|
)
|
|
)
|
|
}
|
|
is SlashItem.Main.Media -> {
|
|
val items =
|
|
listOf(SlashItem.Subheader.MediaWithBack) + SlashExtensions.getSlashWidgetMediaItems()
|
|
onSlashWidgetStateChanged(
|
|
SlashWidgetState.UpdateItems.empty().copy(
|
|
mediaItems = items
|
|
)
|
|
)
|
|
}
|
|
is SlashItem.Main.Relations -> {
|
|
getRelations { proceedWithRelations(it) }
|
|
}
|
|
is SlashItem.Main.Objects -> {
|
|
getObjectTypes(excluded = listOf(ObjectType.BOOKMARK_TYPE)) {
|
|
proceedWithObjectTypes(it)
|
|
}
|
|
}
|
|
is SlashItem.Main.Other -> {
|
|
val items =
|
|
listOf(SlashItem.Subheader.OtherWithBack) + SlashExtensions.getSlashWidgetOtherItems()
|
|
onSlashWidgetStateChanged(
|
|
SlashWidgetState.UpdateItems.empty().copy(
|
|
otherItems = items
|
|
)
|
|
)
|
|
}
|
|
is SlashItem.Main.Actions -> {
|
|
val items =
|
|
listOf(SlashItem.Subheader.ActionsWithBack) + SlashExtensions.getSlashWidgetActionItems()
|
|
onSlashWidgetStateChanged(
|
|
SlashWidgetState.UpdateItems.empty().copy(
|
|
actionsItems = items
|
|
)
|
|
)
|
|
}
|
|
is SlashItem.Main.Alignment -> {
|
|
val items =
|
|
listOf(SlashItem.Subheader.AlignmentWithBack) + getSlashWidgetAlignmentItems(
|
|
slashViewType
|
|
)
|
|
onSlashWidgetStateChanged(
|
|
SlashWidgetState.UpdateItems.empty().copy(
|
|
alignmentItems = items
|
|
)
|
|
)
|
|
}
|
|
is SlashItem.Main.Color -> {
|
|
val block = blocks.first { it.id == targetId }
|
|
val blockColor = block.content.asText().color
|
|
val color = if (blockColor != null) {
|
|
ThemeColor.valueOf(blockColor.toUpperCase())
|
|
} else ThemeColor.DEFAULT
|
|
val items =
|
|
listOf(SlashItem.Subheader.ColorWithBack) + SlashExtensions.getSlashWidgetColorItems(
|
|
color = color
|
|
)
|
|
onSlashWidgetStateChanged(
|
|
SlashWidgetState.UpdateItems.empty().copy(
|
|
colorItems = items
|
|
)
|
|
)
|
|
}
|
|
is SlashItem.Main.Background -> {
|
|
val block = blocks.first { it.id == targetId }
|
|
val blockBackground = block.backgroundColor
|
|
val background = if (blockBackground == null) {
|
|
ThemeColor.DEFAULT
|
|
} else {
|
|
ThemeColor.valueOf(blockBackground.toUpperCase())
|
|
}
|
|
val items = listOf(SlashItem.Subheader.BackgroundWithBack) +
|
|
SlashExtensions.getSlashWidgetBackgroundItems(
|
|
color = background
|
|
)
|
|
onSlashWidgetStateChanged(
|
|
SlashWidgetState.UpdateItems.empty().copy(
|
|
backgroundItems = items
|
|
)
|
|
)
|
|
}
|
|
is SlashItem.Style.Type -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onSlashStyleTypeItemClicked(item, targetId)
|
|
}
|
|
is SlashItem.Style.Markup -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
viewModelScope.launch {
|
|
val view = views.find { it.id == targetId }
|
|
if (view is BlockView.Text) {
|
|
val type = item.convertToMarkType()
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Text.UpdateMark(
|
|
context = context,
|
|
targets = listOf(targetId),
|
|
mark = Content.Text.Mark(
|
|
range = IntRange(0, view.text.length),
|
|
type = type
|
|
)
|
|
)
|
|
)
|
|
sendAnalyticsUpdateTextMarkupEvent(
|
|
analytics = analytics,
|
|
type = type,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
}
|
|
}
|
|
is SlashItem.Media -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onSlashMediaItemClicked(item = item)
|
|
}
|
|
is SlashItem.ObjectType -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onAddNewObjectClicked(
|
|
type = item.url,
|
|
layout = item.layout
|
|
)
|
|
}
|
|
is SlashItem.Relation -> {
|
|
val isBlockEmpty = cutSlashFilter(targetId = targetId)
|
|
onSlashRelationItemClicked(
|
|
relation = item.relation.view.relationId,
|
|
targetId = targetId,
|
|
isBlockEmpty = isBlockEmpty
|
|
)
|
|
}
|
|
is SlashItem.Other.Line -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onHideKeyboardClicked()
|
|
addDividerBlock(style = Content.Divider.Style.LINE)
|
|
}
|
|
is SlashItem.Other.Dots -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onHideKeyboardClicked()
|
|
addDividerBlock(style = Content.Divider.Style.DOTS)
|
|
}
|
|
is SlashItem.Other.TOC -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onHideKeyboardClicked()
|
|
addTableOfContentsBlock()
|
|
}
|
|
is SlashItem.Actions -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onSlashActionItemClicked(item, targetId)
|
|
}
|
|
is SlashItem.Alignment -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onSlashAlignmentItemClicked(item, targetId)
|
|
}
|
|
is SlashItem.Color -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onSlashItemColorClicked(item, targetId)
|
|
}
|
|
SlashItem.Back -> {
|
|
onSlashBackClicked()
|
|
}
|
|
is SlashItem.Subheader -> {
|
|
Timber.d("Click on Slash Subheader, do nothing")
|
|
}
|
|
SlashItem.RelationNew -> {
|
|
dispatch(
|
|
Command.OpenAddRelationScreen(ctx = context, target = targetId)
|
|
)
|
|
}
|
|
is SlashItem.Other.Table -> {
|
|
cutSlashFilter(targetId = targetId)
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
|
|
onHideKeyboardClicked()
|
|
addSimpleTableBlock(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun cutSlashFilter(targetId: Id): Boolean {
|
|
|
|
//saving cursor on slash start index
|
|
setPendingCursorToPosition(targetId = targetId, position = slashStartIndex)
|
|
|
|
// cut text from List<BlockView> and rerender views
|
|
val newBlockView = cutSlashFilterFromViews(targetId)
|
|
|
|
// cut text from List<Block> and send TextUpdate Intent
|
|
if (newBlockView != null) {
|
|
cutSlashFilterFromBlocksAndSendUpdate(
|
|
targetId = targetId,
|
|
text = newBlockView.text,
|
|
marks = newBlockView.marks.map { it.mark() }
|
|
)
|
|
} else {
|
|
Timber.e("cutSlashFilter error, BlockView is null on targetId:$targetId")
|
|
}
|
|
|
|
return newBlockView?.text?.isEmpty() ?: false
|
|
}
|
|
|
|
private fun cutSlashFilterFromViews(targetId: Id): BlockView.Text? {
|
|
Timber.d("cutSlashFilterFromViews, targetId:[$targetId], slashStartIndex:[$slashStartIndex], slashFilter:[$slashFilter]")
|
|
val blockView = views.firstOrNull { it.id == targetId }
|
|
if (blockView is BlockView.Text) {
|
|
val new = blockView.cutPartOfText(
|
|
from = slashStartIndex,
|
|
partLength = slashFilter.length
|
|
)
|
|
val update = views.update(new)
|
|
viewModelScope.launch {
|
|
orchestrator.stores.views.update(update)
|
|
renderCommand.send(Unit)
|
|
}
|
|
return new
|
|
}
|
|
return null
|
|
}
|
|
|
|
private fun cutSlashFilterFromBlocksAndSendUpdate(
|
|
targetId: Id,
|
|
text: String,
|
|
marks: List<Content.Text.Mark>
|
|
) {
|
|
val update = blocks.updateTextContent(
|
|
target = targetId,
|
|
text = text,
|
|
marks = marks
|
|
)
|
|
|
|
orchestrator.stores.document.update(update)
|
|
|
|
//send new text to Middleware
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.saves.send(null)
|
|
orchestrator.proxies.changes.send(null)
|
|
}
|
|
|
|
val intent = Intent.Text.UpdateText(
|
|
context = context,
|
|
target = targetId,
|
|
text = text,
|
|
marks = marks
|
|
)
|
|
|
|
proceedWithUpdatingText(intent)
|
|
}
|
|
|
|
private fun setPendingCursorToPosition(targetId: Id, position: Int) {
|
|
val cursor = Editor.Cursor.Range(
|
|
range = IntRange(position, position)
|
|
)
|
|
val focus = Editor.Focus(
|
|
id = targetId,
|
|
cursor = cursor
|
|
)
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(focus)
|
|
}
|
|
}
|
|
|
|
private fun getObjectTypes(
|
|
excluded: List<Id> = emptyList(),
|
|
action: (List<ObjectType>) -> Unit
|
|
) {
|
|
viewModelScope.launch {
|
|
getCompatibleObjectTypes.invoke(
|
|
GetCompatibleObjectTypes.Params(
|
|
smartBlockType = blocks.first { it.id == context }
|
|
.content<Content.Smart>().type,
|
|
excludedTypes = excluded
|
|
)
|
|
).proceed(
|
|
failure = {
|
|
Timber.e(it, "Error while getting object types")
|
|
},
|
|
success = { objectTypes ->
|
|
action.invoke(objectTypes)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun getRelations(action: (List<SlashRelationView.Item>) -> Unit) {
|
|
val relations = orchestrator.stores.relations.current()
|
|
val details = orchestrator.stores.details.current()
|
|
val detail = details.details[context]
|
|
val values = detail?.map ?: emptyMap()
|
|
val update = relations.views(
|
|
details = details,
|
|
values = values,
|
|
urlBuilder = urlBuilder
|
|
).map { SlashRelationView.Item(it) }
|
|
action.invoke(update)
|
|
}
|
|
|
|
private fun proceedWithObjectTypes(objectTypes: List<ObjectType>) {
|
|
onSlashWidgetStateChanged(
|
|
SlashWidgetState.UpdateItems.empty().copy(
|
|
objectItems = SlashExtensions.getSlashWidgetObjectTypeItems(objectTypes = objectTypes)
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun proceedWithRelations(relations: List<SlashRelationView>) {
|
|
onSlashWidgetStateChanged(
|
|
SlashWidgetState.UpdateItems.empty().copy(
|
|
relationItems = SlashExtensions.getSlashWidgetRelationItems(relations)
|
|
)
|
|
)
|
|
}
|
|
|
|
private fun onSlashItemColorClicked(item: SlashItem.Color, targetId: Id) {
|
|
|
|
val intent = when (item) {
|
|
is SlashItem.Color.Background -> {
|
|
Intent.Text.UpdateBackgroundColor(
|
|
context = context,
|
|
targets = listOf(targetId),
|
|
color = item.themeColor.code
|
|
)
|
|
}
|
|
is SlashItem.Color.Text -> {
|
|
Intent.Text.UpdateColor(
|
|
context = context,
|
|
targets = listOf(targetId),
|
|
color = item.themeColor.code
|
|
)
|
|
}
|
|
}
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(intent)
|
|
when (item) {
|
|
is SlashItem.Color.Background -> {
|
|
sendAnalyticsBlockBackgroundEvent(
|
|
analytics = analytics,
|
|
color = item.themeColor.code,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
is SlashItem.Color.Text -> {
|
|
sendAnalyticsUpdateTextMarkupEvent(
|
|
analytics = analytics,
|
|
type = Content.Text.Mark.Type.TEXT_COLOR,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun onSlashMediaItemClicked(item: SlashItem.Media) {
|
|
when (item) {
|
|
SlashItem.Media.Bookmark -> {
|
|
onHideKeyboardClicked()
|
|
onAddBookmarkBlockClicked()
|
|
}
|
|
SlashItem.Media.Code -> {
|
|
onHideKeyboardClicked()
|
|
onAddTextBlockClicked(style = Content.Text.Style.CODE_SNIPPET)
|
|
}
|
|
SlashItem.Media.File -> {
|
|
onHideKeyboardClicked()
|
|
onAddFileBlockClicked(Content.File.Type.FILE)
|
|
}
|
|
SlashItem.Media.Picture -> {
|
|
onHideKeyboardClicked()
|
|
onAddFileBlockClicked(Content.File.Type.IMAGE)
|
|
}
|
|
SlashItem.Media.Video -> {
|
|
onHideKeyboardClicked()
|
|
onAddFileBlockClicked(Content.File.Type.VIDEO)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun onSlashStyleTypeItemClicked(item: SlashItem.Style.Type, targetId: Id) {
|
|
val uiBlock = item.convertToUiBlock()
|
|
onTurnIntoBlockClicked(
|
|
target = targetId,
|
|
uiBlock = uiBlock
|
|
)
|
|
}
|
|
|
|
private fun onSlashActionItemClicked(item: SlashItem.Actions, targetId: Id) {
|
|
when (item) {
|
|
SlashItem.Actions.CleanStyle -> {
|
|
viewModelScope.launch {
|
|
sendToast("CleanStyle not implemented")
|
|
}
|
|
}
|
|
SlashItem.Actions.Copy -> {
|
|
val block = blocks.first { it.id == targetId }
|
|
val intent = Intent.Clipboard.Copy(
|
|
context = context,
|
|
range = null,
|
|
blocks = listOf(block)
|
|
)
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(intent)
|
|
}
|
|
}
|
|
SlashItem.Actions.Paste -> {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Clipboard.Paste(
|
|
context = context,
|
|
focus = targetId,
|
|
range = IntRange(slashStartIndex, slashStartIndex),
|
|
selected = emptyList()
|
|
)
|
|
)
|
|
}
|
|
}
|
|
SlashItem.Actions.Delete -> {
|
|
proceedWithUnlinking(targetId)
|
|
}
|
|
SlashItem.Actions.Duplicate -> {
|
|
duplicateBlock(
|
|
blocks = listOf(targetId),
|
|
target = targetId
|
|
)
|
|
}
|
|
SlashItem.Actions.Move -> {
|
|
viewModelScope.launch {
|
|
blocks.forEach { unselect(it.id) }
|
|
mode = EditorMode.SAM
|
|
selectWithDescendants(targetId)
|
|
val updated = views.enterSAM(currentSelection())
|
|
orchestrator.stores.views.update(updated)
|
|
renderCommand.send(Unit)
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.SAM.OnQuickStart(
|
|
currentSelection().size
|
|
)
|
|
)
|
|
}
|
|
}
|
|
SlashItem.Actions.MoveTo -> {
|
|
onHideKeyboardClicked()
|
|
proceedWithMoveToButtonClicked(
|
|
blocks = listOf(targetId),
|
|
restorePosition = slashStartIndex,
|
|
restoreBlock = targetId
|
|
)
|
|
}
|
|
SlashItem.Actions.LinkTo -> {
|
|
onHideKeyboardClicked()
|
|
proceedWithLinkToButtonClicked(block = targetId, position = slashStartIndex)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun selectWithDescendants(targetId: Id) {
|
|
select(targetId)
|
|
val descendants = blocks.asMap().descendants(parent = targetId)
|
|
descendants.forEach { child -> select(child) }
|
|
}
|
|
|
|
private fun onSlashAlignmentItemClicked(item: SlashItem.Alignment, targetId: Id) {
|
|
val alignment = when (item) {
|
|
SlashItem.Alignment.Center -> Block.Align.AlignCenter
|
|
SlashItem.Alignment.Left -> Block.Align.AlignLeft
|
|
SlashItem.Alignment.Right -> Block.Align.AlignRight
|
|
}
|
|
proceedWithAlignmentUpdate(
|
|
targets = listOf(targetId),
|
|
alignment = alignment
|
|
)
|
|
}
|
|
|
|
private fun onSlashWidgetStateChanged(widgetState: SlashWidgetState) {
|
|
val panelEvent = ControlPanelMachine.Event.Slash.OnFilterChange(
|
|
widgetState = widgetState
|
|
)
|
|
controlPanelInteractor.onEvent(panelEvent)
|
|
}
|
|
|
|
private fun onSlashBackClicked() {
|
|
val items = SlashExtensions.getSlashWidgetMainItems()
|
|
val widgetState = SlashWidgetState.UpdateItems.empty().copy(
|
|
mainItems = items
|
|
)
|
|
val panelEvent = ControlPanelMachine.Event.Slash.OnFilterChange(
|
|
widgetState = widgetState
|
|
)
|
|
controlPanelInteractor.onEvent(panelEvent)
|
|
}
|
|
|
|
private var filterSearchEmptyCount = 0
|
|
private var slashStartIndex = 0
|
|
private var slashFilter = ""
|
|
private var slashViewType = 0
|
|
|
|
private fun incFilterSearchEmptyCount(widgetState: SlashWidgetState.UpdateItems) {
|
|
if (SlashExtensions.isSlashWidgetEmpty(widgetState)) {
|
|
filterSearchEmptyCount += 1
|
|
}
|
|
}
|
|
|
|
private fun onSlashRelationItemClicked(
|
|
relation: Id, targetId: Id, isBlockEmpty: Boolean
|
|
) {
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStopAndClearFocus)
|
|
val intent = if (isBlockEmpty) {
|
|
Intent.CRUD.Replace(
|
|
context = context,
|
|
target = targetId,
|
|
prototype = Prototype.Relation(key = relation)
|
|
)
|
|
} else {
|
|
Intent.CRUD.Create(
|
|
context = context,
|
|
target = targetId,
|
|
position = Position.BOTTOM,
|
|
prototype = Prototype.Relation(key = relation)
|
|
)
|
|
}
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(intent)
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
//region MARKUP TOOLBAR
|
|
|
|
fun onUnlinkPressed(blockId: String, range: IntRange) {
|
|
Timber.d("onUnlinkPressed, blockId:[$blockId] range:[$range]")
|
|
|
|
val target = blocks.first { it.id == blockId }
|
|
val content = target.content<Content.Text>()
|
|
val marks = content.marks
|
|
|
|
viewModelScope.launch {
|
|
removeLinkMark(
|
|
params = RemoveLinkMark.Params(
|
|
range = range,
|
|
marks = marks
|
|
)
|
|
).proceed(
|
|
failure = { Timber.e("Error update marks:${it.message}") },
|
|
success = {
|
|
val newContent = content.copy(marks = it)
|
|
val newBlock = target.copy(content = newContent)
|
|
rerenderingBlocks(newBlock)
|
|
proceedWithUpdatingText(
|
|
intent = Intent.Text.UpdateText(
|
|
context = context,
|
|
text = newBlock.content.asText().text,
|
|
target = target.id,
|
|
marks = it
|
|
)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onMarkupColorToggleClicked() {
|
|
Timber.d("onMarkupColorToggleClicked, ")
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.MarkupToolbar.OnMarkupColorToggleClicked
|
|
)
|
|
}
|
|
|
|
fun onMarkupHighlightToggleClicked() {
|
|
Timber.d("onMarkupHighlightToggleClicked, ")
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.MarkupToolbar.OnMarkupHighlightToggleClicked
|
|
)
|
|
}
|
|
|
|
//endregion
|
|
|
|
//region MOVE TO
|
|
private fun proceedWithMoveToButtonClicked(
|
|
blocks: List<Id>,
|
|
restorePosition: Int?,
|
|
restoreBlock: Id?
|
|
) {
|
|
dispatch(
|
|
Command.OpenMoveToScreen(
|
|
blocks = blocks,
|
|
restorePosition = restorePosition,
|
|
restoreBlock = restoreBlock,
|
|
ctx = context
|
|
)
|
|
)
|
|
}
|
|
|
|
fun proceedWithMoveToAction(target: Id, blocks: List<Id>) {
|
|
if (BuildConfig.DEBUG) {
|
|
Timber.d("onMoveToTargetClicked, target:[$target], blocks:[$blocks]")
|
|
}
|
|
viewModelScope.launch {
|
|
if (mode == EditorMode.Select) {
|
|
mode = EditorMode.Edit
|
|
clearSelections()
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.MultiSelect.OnExit)
|
|
}
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.Move(
|
|
context = context,
|
|
target = "",
|
|
targetContext = target,
|
|
blocks = blocks,
|
|
position = Position.BOTTOM
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun proceedWithMoveToExit(
|
|
blocks: List<Id>,
|
|
restorePosition: Int?,
|
|
restoreBlock: Id?
|
|
) {
|
|
if (BuildConfig.DEBUG) {
|
|
Timber.d("proceedWithMoveToExit, blocks:[$blocks], restoreBlock:[$restoreBlock] position:[$restorePosition]")
|
|
}
|
|
if (restorePosition != null && restoreBlock != null) {
|
|
proceedWithSettingTextSelection(
|
|
block = restoreBlock,
|
|
textSelection = restorePosition
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithSettingTextSelection(block: Id, textSelection: Int?) {
|
|
mode = EditorMode.Edit
|
|
val range = IntRange(textSelection ?: 0, textSelection ?: 0)
|
|
val cursor = if (textSelection != null) {
|
|
Editor.Cursor.Range(range)
|
|
} else {
|
|
Editor.Cursor.End
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(
|
|
Editor.Focus(
|
|
id = block,
|
|
cursor = cursor
|
|
)
|
|
)
|
|
orchestrator.stores.textSelection.update(
|
|
Editor.TextSelection(block, range)
|
|
)
|
|
|
|
orchestrator.stores.views.update(
|
|
views.updateCursorAndEditMode(
|
|
target = block,
|
|
cursor = range.first
|
|
)
|
|
)
|
|
}
|
|
viewModelScope.launch {
|
|
renderCommand.send(Unit)
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
//region LINK TO
|
|
private fun proceedWithLinkToButtonClicked(block: Id, position: Int?) {
|
|
dispatch(Command.OpenLinkToScreen(target = block, position = position))
|
|
}
|
|
|
|
fun proceedWithLinkToAction(link: Id, target: Id, isBookmark: Boolean) {
|
|
val targetBlock = blocks.firstOrNull { it.id == target }
|
|
if (targetBlock != null) {
|
|
val targetContent = targetBlock.content
|
|
val position = if (targetContent is Content.Text && targetContent.text.isEmpty()) {
|
|
Position.REPLACE
|
|
} else {
|
|
Position.BOTTOM
|
|
}
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Create(
|
|
context = context,
|
|
target = target,
|
|
position = position,
|
|
prototype = if (isBookmark)
|
|
Prototype.Bookmark.Existing(target = link)
|
|
else
|
|
Prototype.Link(target = link)
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
Timber.e("Can't find target block for link")
|
|
sendToast("Error while creating link")
|
|
}
|
|
}
|
|
|
|
fun proceedWithLinkToExit(block: Id, position: Int?) {
|
|
Timber.d("proceedWithLinkToExit, block:[$block], position:[$position]")
|
|
if (position != null) {
|
|
proceedWithSettingTextSelection(
|
|
block = block,
|
|
textSelection = position
|
|
)
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
//region KEY EVENTS
|
|
fun onKeyPressedEvent(event: KeyPressedEvent) {
|
|
Timber.d("onKeyPressedEvent, event:[$event]")
|
|
when (event) {
|
|
is KeyPressedEvent.OnTitleBlockEnterKeyEvent -> {
|
|
if (isObjectTypesWidgetVisible) {
|
|
dispatchObjectCreateEvent()
|
|
proceedWithHidingObjectTypeWidget()
|
|
}
|
|
proceedWithTitleEnterClicked(
|
|
title = event.target,
|
|
text = event.text,
|
|
range = event.range
|
|
)
|
|
viewModelScope.sendAnalyticsSetTitleEvent(analytics, analyticsContext)
|
|
}
|
|
is KeyPressedEvent.OnDescriptionBlockEnterKeyEvent -> {
|
|
proceedWithDescriptionEnterClicked(
|
|
description = event.target,
|
|
text = event.text,
|
|
range = event.range
|
|
)
|
|
viewModelScope.sendAnalyticsSetDescriptionEvent(analytics, analyticsContext)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun proceedWithTitleEnterClicked(
|
|
title: Id,
|
|
text: String,
|
|
range: IntRange
|
|
) {
|
|
if (text.isEndLineClick(range)) {
|
|
onEndLineEnterTitleClicked()
|
|
} else {
|
|
proceedWithSplitEvent(title, range, text, emptyList())
|
|
}
|
|
}
|
|
|
|
private fun onEndLineEnterTitleClicked() {
|
|
val description = blocks.firstOrNull { block ->
|
|
val cnt = block.content
|
|
cnt is Content.Text && cnt.style == Content.Text.Style.DESCRIPTION
|
|
}
|
|
if (description != null) {
|
|
proceedWithSettingTextSelection(
|
|
block = description.id,
|
|
textSelection = description.content<Content.Text>().text.length
|
|
)
|
|
} else {
|
|
val page = blocks.first { it.id == context }
|
|
val next = page.children.getOrElse(0) { "" }
|
|
proceedWithCreatingNewTextBlock(
|
|
id = next,
|
|
style = Content.Text.Style.P,
|
|
position = Position.TOP
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithDescriptionEnterClicked(
|
|
description: Id,
|
|
text: String,
|
|
range: IntRange
|
|
) {
|
|
proceedWithSplitEvent(description, range, text, emptyList())
|
|
}
|
|
//endregion
|
|
|
|
//region MULTI-SELECT
|
|
|
|
fun onBlockActionPanelHidden() {
|
|
proceedWithExitingMultiSelectMode()
|
|
}
|
|
|
|
fun onMultiSelectAction(action: ActionItemType) {
|
|
Timber.d("onMultiSelectAction, action:[$action]")
|
|
when (action) {
|
|
ActionItemType.AddBelow -> {
|
|
onMultiSelectAddBelow()
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.addBelow)
|
|
}
|
|
ActionItemType.Delete -> {
|
|
onMultiSelectModeDeleteClicked()
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.delete)
|
|
}
|
|
ActionItemType.Duplicate -> {
|
|
onMultiSelectDuplicateClicked()
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.duplicate)
|
|
}
|
|
ActionItemType.MoveTo -> {
|
|
proceedWithMoveToButtonClicked(
|
|
blocks = currentSelection().toList(),
|
|
restoreBlock = null,
|
|
restorePosition = null
|
|
)
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.moveTo)
|
|
}
|
|
ActionItemType.SAM -> {
|
|
onEnterScrollAndMoveClicked()
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.move)
|
|
}
|
|
ActionItemType.Style -> {
|
|
onMultiSelectStyleButtonClicked()
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.style)
|
|
}
|
|
ActionItemType.Download -> {
|
|
startDownloadingFiles(ids = currentSelection().toList())
|
|
proceedWithExitingMultiSelectMode()
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.download)
|
|
}
|
|
ActionItemType.Preview -> {
|
|
proceedWithObjectAppearanceSettingClicked()
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.preview)
|
|
}
|
|
ActionItemType.Copy -> {
|
|
onMultiSelectCopyClicked()
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.copy)
|
|
}
|
|
ActionItemType.Paste -> {
|
|
onMultiSelectPasteClicked()
|
|
proceedWithExitingMultiSelectMode()
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.paste)
|
|
}
|
|
ActionItemType.OpenObject -> {
|
|
val selected = blocks.firstOrNull { currentSelection().contains(it.id) }
|
|
proceedWithExitingMultiSelectMode()
|
|
if (selected != null) {
|
|
proceedWithMultiSelectOpenObjectAction(
|
|
selected = selected
|
|
)
|
|
} else {
|
|
sendToast("No blocks were selected. Please, try again.")
|
|
}
|
|
onSendBlockActionAnalyticsEvent(EventsDictionary.BlockAction.openObject)
|
|
}
|
|
else -> {
|
|
sendToast("TODO")
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun proceedWithMultiSelectOpenObjectAction(selected: Block) {
|
|
when (val content = selected.content) {
|
|
is Content.Bookmark -> {
|
|
val target = content.targetObjectId
|
|
if (target != null) {
|
|
proceedWithOpeningPage(target)
|
|
viewModelScope.sendAnalyticsOpenAsObject(
|
|
analytics = analytics,
|
|
type = EventsDictionary.Type.bookmark
|
|
)
|
|
}
|
|
}
|
|
else -> sendToast("Unexpected object")
|
|
}
|
|
}
|
|
|
|
private fun onSendBlockActionAnalyticsEvent(type: String) {
|
|
viewModelScope.launch {
|
|
sendAnalyticsBlockActionEvent(
|
|
analytics = analytics,
|
|
context = context,
|
|
type = type
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun onMultiSelectAddBelow() {
|
|
mode = EditorMode.Edit
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.MultiSelect.OnExit)
|
|
val target = currentSelection().first()
|
|
clearSelections()
|
|
proceedWithCreatingNewTextBlock(
|
|
id = target,
|
|
style = Content.Text.Style.P
|
|
)
|
|
}
|
|
|
|
fun onMultiSelectModeDeleteClicked() {
|
|
Timber.d("onMultiSelectModeDeleteClicked, ")
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.MultiSelect.OnDelete)
|
|
|
|
val exclude = mutableSetOf<String>()
|
|
|
|
val selected = currentSelection().toList()
|
|
|
|
blocks.filter { selected.contains(it.id) }.forEach { block ->
|
|
block.children.forEach { if (selected.contains(it)) exclude.add(it) }
|
|
}
|
|
|
|
clearSelections()
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.CRUD.Unlink(
|
|
context = context,
|
|
targets = selected - exclude,
|
|
next = null,
|
|
previous = null,
|
|
effects = listOf(SideEffect.ClearMultiSelectSelection)
|
|
)
|
|
)
|
|
}
|
|
|
|
proceedWithExitingMultiSelectMode()
|
|
}
|
|
|
|
private fun onMultiSelectDuplicateClicked() {
|
|
val parents = blocks.parents(currentSelection())
|
|
val targets = views.mapNotNull { view ->
|
|
if (parents.contains(view.id))
|
|
view.id
|
|
else
|
|
null
|
|
}
|
|
duplicateBlock(
|
|
blocks = targets,
|
|
target = targets.last()
|
|
)
|
|
}
|
|
|
|
fun onMultiSelectCopyClicked() {
|
|
Timber.d("onMultiSelectCopyClicked, ")
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Clipboard.Copy(
|
|
context = context,
|
|
blocks = blocks.filter { block ->
|
|
currentSelection().contains(block.id)
|
|
},
|
|
range = null
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun onMultiSelectPasteClicked() {
|
|
Timber.d("onMultiSelectPasteClicked, ")
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Clipboard.Paste(
|
|
context = context,
|
|
focus = Editor.Focus.EMPTY_FOCUS,
|
|
selected = currentSelection().toList(),
|
|
range = DEFAULT_RANGE
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onMultiSelectStyleButtonClicked() {
|
|
Timber.d("onMultiSelectStyleButtonClicked, ")
|
|
proceedWithMultiStyleToolbarEvent()
|
|
}
|
|
|
|
fun onMultiSelectTurnIntoButtonClicked() {
|
|
Timber.d("onMultiSelectTurnIntoButtonClicked, ")
|
|
|
|
val targets = currentSelection()
|
|
|
|
val blocks = blocks.filter { targets.contains(it.id) }
|
|
|
|
val hasTextBlocks = blocks.any { it.content is Content.Text }
|
|
|
|
when {
|
|
hasTextBlocks -> {
|
|
proceedUpdateBlockStyle(
|
|
targets = currentSelection().toList(),
|
|
uiBlock = UiBlock.PAGE,
|
|
action = {
|
|
clearSelections()
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.MultiSelect.OnTurnInto)
|
|
},
|
|
errorAction = { sendToast("Cannot convert selected blocks to PAGE") }
|
|
)
|
|
}
|
|
else -> {
|
|
sendToast("Cannot turn selected blocks into page")
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onExitMultiSelectModeClicked() {
|
|
proceedWithExitingMultiSelectMode()
|
|
}
|
|
|
|
private fun proceedWithExitingMultiSelectMode() {
|
|
Timber.d("onExitMultiSelectModeClicked, ")
|
|
mode = EditorMode.Edit
|
|
clearSelections()
|
|
viewModelScope.launch {
|
|
delay(DELAY_REFRESH_DOCUMENT_ON_EXIT_MULTI_SELECT_MODE)
|
|
orchestrator.stores.focus.update(Editor.Focus.empty())
|
|
refresh()
|
|
}
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.MultiSelect.OnExit)
|
|
}
|
|
|
|
fun onEnterMultiSelectModeClicked() {
|
|
Timber.d("onEnterMultiSelectModeClicked, ")
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.MultiSelect.OnEnter())
|
|
mode = EditorMode.Select
|
|
viewModelScope.launch { orchestrator.stores.focus.update(Editor.Focus.empty()) }
|
|
viewModelScope.launch {
|
|
delay(DELAY_REFRESH_DOCUMENT_TO_ENTER_MULTI_SELECT_MODE)
|
|
refresh()
|
|
}
|
|
}
|
|
|
|
//endregion
|
|
|
|
//region MENTION WIDGET
|
|
/**
|
|
* Current position of last mentionFilter or -1 if none
|
|
*/
|
|
private var mentionFrom = -1
|
|
private val mentionFilter = MutableStateFlow("")
|
|
val mentionSearchQuery = mentionFilter.asStateFlow()
|
|
private var jobMentionFilter: Job? = null
|
|
|
|
fun onStartMentionWidgetClicked() {
|
|
dispatch(Command.AddMentionWidgetTriggerToFocusedBlock)
|
|
viewModelScope.sendAnalyticsMentionMenuEvent(analytics)
|
|
}
|
|
|
|
fun onMentionEvent(mentionEvent: MentionEvent) {
|
|
Timber.d("onMentionEvent, mentionEvent:[$mentionEvent]")
|
|
when (mentionEvent) {
|
|
is MentionEvent.MentionSuggestText -> {
|
|
mentionFilter.value = mentionEvent.text.toString()
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.Mentions.OnQuery(
|
|
text = mentionEvent.text.toString()
|
|
)
|
|
)
|
|
}
|
|
is MentionEvent.MentionSuggestStart -> {
|
|
mentionFrom = mentionEvent.mentionStart
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.Mentions.OnStart(
|
|
cursorCoordinate = mentionEvent.cursorCoordinate,
|
|
mentionFrom = mentionEvent.mentionStart
|
|
)
|
|
)
|
|
jobMentionFilter?.cancel()
|
|
mentionFilter.value = ""
|
|
jobMentionFilter = viewModelScope.launch {
|
|
mentionSearchQuery
|
|
.debounce(300)
|
|
.collect { onMentionFilter(it) }
|
|
}
|
|
}
|
|
MentionEvent.MentionSuggestStop -> {
|
|
mentionFrom = -1
|
|
jobMentionFilter?.cancel()
|
|
mentionFilter.value = ""
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Mentions.OnStop)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onAddMentionNewPageClicked(mentionText: String) {
|
|
Timber.d("onAddMentionNewPageClicked, mentionText:[$mentionText]")
|
|
viewModelScope.launch {
|
|
getDefaultEditorType.execute(Unit).fold(
|
|
onFailure = {
|
|
Timber.e(it, "Error while getting default object type")
|
|
proceedWithCreateNewObject(objectType = null, mentionText = mentionText)
|
|
},
|
|
onSuccess = {
|
|
proceedWithCreateNewObject(objectType = it.type, mentionText = mentionText)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun proceedWithCreateNewObject(objectType: String?, mentionText: String) {
|
|
val params = CreateNewDocument.Params(
|
|
name = mentionText.removePrefix(MENTION_PREFIX),
|
|
type = objectType
|
|
)
|
|
|
|
val startTime = System.currentTimeMillis()
|
|
|
|
viewModelScope.launch {
|
|
createNewDocument(
|
|
params = params
|
|
).proceed(
|
|
failure = {
|
|
Timber.e(it, "Error while creating new page with params: $params")
|
|
},
|
|
success = { result ->
|
|
val middleTime = System.currentTimeMillis()
|
|
onCreateMentionInText(
|
|
id = result.id,
|
|
name = result.name.getMentionName(MENTION_TITLE_EMPTY),
|
|
mentionTrigger = mentionText
|
|
)
|
|
val type = objectTypesProvider.get().firstOrNull { it.url == objectType }
|
|
sendAnalyticsObjectCreateEvent(
|
|
analytics = analytics,
|
|
objType = objectType,
|
|
layout = type?.layout?.code?.toDouble(),
|
|
route = EventsDictionary.Routes.objCreateMention,
|
|
startTime = startTime,
|
|
middleTime = middleTime,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onMentionSuggestClick(mention: DefaultObjectView, mentionTrigger: String, pos: Int) {
|
|
Timber.d("onMentionSuggestClick, mention:[$mention] mentionTrigger:[$mentionTrigger]")
|
|
viewModelScope.sendAnalyticsSearchResultEvent(
|
|
analytics = analytics,
|
|
pos = pos,
|
|
length = mentionTrigger.length - 1,
|
|
context = analyticsContext
|
|
)
|
|
onCreateMentionInText(id = mention.id, name = mention.name, mentionTrigger = mentionTrigger)
|
|
}
|
|
|
|
fun onCreateMentionInText(id: Id, name: String, mentionTrigger: String) {
|
|
Timber.d("onCreateMentionInText, id:[$id], name:[$name], mentionTrigger:[$mentionTrigger]")
|
|
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Mentions.OnMentionClicked)
|
|
|
|
val target = blocks.first { it.id == focus.value }
|
|
|
|
val new = target.addMention(
|
|
mentionText = name,
|
|
mentionId = id,
|
|
from = mentionFrom,
|
|
mentionTrigger = mentionTrigger
|
|
)
|
|
|
|
val update = blocks.map { block ->
|
|
if (block.id != target.id)
|
|
block
|
|
else
|
|
new
|
|
}
|
|
|
|
orchestrator.stores.document.update(update)
|
|
|
|
viewModelScope.launch {
|
|
val position = mentionFrom + name.length + 1
|
|
orchestrator.stores.focus.update(
|
|
t = Editor.Focus(
|
|
id = new.id,
|
|
cursor = Editor.Cursor.Range(IntRange(position, position))
|
|
)
|
|
)
|
|
refresh()
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
proceedWithUpdatingText(
|
|
intent = Intent.Text.UpdateText(
|
|
context = context,
|
|
target = new.id,
|
|
text = new.content<Content.Text>().text,
|
|
marks = new.content<Content.Text>().marks
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onMentionClicked(target: String) {
|
|
proceedWithOpeningObjectByLayout(target)
|
|
}
|
|
|
|
private fun sendSearchQueryEvent(query: String) {
|
|
viewModelScope.sendAnalyticsSearchQueryEvent(
|
|
analytics = analytics,
|
|
route = EventsDictionary.Routes.mention,
|
|
length = query.length,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
|
|
private suspend fun onMentionFilter(filter: String) {
|
|
controlPanelViewState.value?.let { state ->
|
|
if (!state.mentionToolbar.isVisible) {
|
|
jobMentionFilter?.cancel()
|
|
return
|
|
}
|
|
val fullText = filter.removePrefix(MENTION_PREFIX)
|
|
val params = SearchObjects.Params(
|
|
limit = ObjectSearchViewModel.SEARCH_LIMIT,
|
|
filters = ObjectSearchConstants.filterLinkTo,
|
|
sorts = ObjectSearchConstants.sortLinkTo,
|
|
fulltext = fullText,
|
|
keys = ObjectSearchConstants.defaultKeys
|
|
)
|
|
sendSearchQueryEvent(fullText)
|
|
viewModelScope.launch {
|
|
searchObjects(params).process(
|
|
success = { result ->
|
|
val objects = result
|
|
.toView(urlBuilder, objectTypesProvider.get())
|
|
.filter {
|
|
SupportedLayouts.layouts.contains(it.layout)
|
|
&& it.type != ObjectType.TEMPLATE_URL
|
|
}
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.Mentions.OnResult(
|
|
objects,
|
|
filter
|
|
)
|
|
)
|
|
},
|
|
failure = { Timber.e(it, "Error while searching for mention objects") }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun onDragAndDrop(
|
|
dragged: Id,
|
|
target: Id,
|
|
position: Position
|
|
) {
|
|
val descendants = blocks.asMap().descendants(parent = dragged)
|
|
|
|
if (descendants.contains(target)) {
|
|
sendToast(CANNOT_MOVE_PARENT_INTO_CHILD)
|
|
return
|
|
}
|
|
|
|
val targetBlock = blocks.find { it.id == target }
|
|
|
|
val targetContext =
|
|
if (targetBlock?.content is Content.Link && position == Position.INNER) {
|
|
targetBlock.content<Content.Link>().target
|
|
} else {
|
|
context
|
|
}
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.Move(
|
|
context = context,
|
|
target = target,
|
|
targetContext = targetContext,
|
|
blocks = listOf(dragged),
|
|
position = position
|
|
)
|
|
)
|
|
sendAnalyticsBlockReorder(
|
|
analytics = analytics,
|
|
count = 1,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
//region OBJECT TYPES WIDGET
|
|
private val isObjectTypesWidgetVisible: Boolean
|
|
get() =
|
|
controlPanelViewState.value?.objectTypesToolbar?.isVisible ?: false
|
|
|
|
fun onObjectTypesWidgetItemClicked(typeId: Id) {
|
|
Timber.d("onObjectTypesWidgetItemClicked, id:[$typeId]")
|
|
dispatchObjectCreateEvent(typeId)
|
|
proceedWithHidingObjectTypeWidget()
|
|
if (typeId == ObjectType.SET_URL) {
|
|
viewModelScope.launch {
|
|
val params = ConvertObjectToSet.Params(
|
|
ctx = context,
|
|
sources = emptyList()
|
|
)
|
|
objectToSet.invoke(params).proceed(
|
|
failure = { error -> Timber.e(error, "Error convert object to set") },
|
|
success = { setId ->
|
|
proceedWithOpeningSet(target = setId, isPopUpToDashboard = true)
|
|
}
|
|
)
|
|
}
|
|
} else {
|
|
viewModelScope.launch {
|
|
orchestrator.proxies.intents.send(
|
|
Intent.Document.SetObjectType(
|
|
context = context,
|
|
typeId = typeId
|
|
)
|
|
)
|
|
}
|
|
proceedWithTemplateSelection(typeId)
|
|
}
|
|
}
|
|
|
|
fun onObjectTypesWidgetSearchClicked() {
|
|
Timber.d("onObjectTypesWidgetSearchClicked, ")
|
|
dispatch(
|
|
Command.OpenChangeObjectTypeScreen(
|
|
ctx = context,
|
|
smartBlockType = getObjectSmartBlockType(),
|
|
excludedTypes = listOf(ObjectType.BOOKMARK_TYPE),
|
|
isDraft = true
|
|
)
|
|
)
|
|
}
|
|
|
|
fun onObjectTypesWidgetDoneClicked() {
|
|
Timber.d("onObjectTypesWidgetDoneClicked, ")
|
|
proceedWithHidingObjectTypeWidget()
|
|
val details = orchestrator.stores.details.current()
|
|
val wrapper = ObjectWrapper.Basic(details.details[context]?.map ?: emptyMap())
|
|
if (wrapper.type.isNotEmpty())
|
|
proceedWithTemplateSelection(
|
|
typeId = wrapper.type.first()
|
|
)
|
|
}
|
|
|
|
private fun proceedWithShowingObjectTypesWidget(objectType: String?, blocks: List<Block>) {
|
|
val restrictions = orchestrator.stores.objectRestrictions.current()
|
|
if (restrictions.contains(ObjectRestriction.TYPE_CHANGE)) {
|
|
return
|
|
}
|
|
when (objectType) {
|
|
ObjectType.NOTE_URL -> {
|
|
val root = blocks.find { it.id == context } ?: return
|
|
if (root.children.size == 2) {
|
|
val lastBlock = blocks.find { it.id == root.children.last() }
|
|
if (lastBlock != null && lastBlock.content is Content.Text) {
|
|
if (lastBlock.content<Content.Text>().text.isEmpty()) {
|
|
proceedWithGettingObjectTypesForObjectTypeWidget()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else -> {
|
|
val root = blocks.find { it.id == context } ?: return
|
|
if (root.children.size == 1) {
|
|
val title = blocks.title() ?: return
|
|
if (title.content<Content.Text>().text.isEmpty()) {
|
|
proceedWithGettingObjectTypesForObjectTypeWidget()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun proceedWithGettingObjectTypesForObjectTypeWidget() {
|
|
val smartBlockType = getObjectSmartBlockType()
|
|
val params = GetCompatibleObjectTypes.Params(
|
|
smartBlockType = smartBlockType,
|
|
excludedTypes = listOf(ObjectType.BOOKMARK_TYPE),
|
|
isSetIncluded = true
|
|
)
|
|
viewModelScope.launch {
|
|
getCompatibleObjectTypes.invoke(params).proceed(
|
|
failure = { Timber.e(it, "Error while getting object types") },
|
|
success = { objectTypes ->
|
|
proceedWithSortingObjectTypesForObjectTypeWidget(
|
|
views = objectTypes.toObjectTypeView()
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private suspend fun proceedWithSortingObjectTypesForObjectTypeWidget(views: List<ObjectTypeView.Item>) {
|
|
getDefaultEditorType.execute(Unit).fold(
|
|
onFailure = {
|
|
Timber.e(it, "Error while getting default object type")
|
|
},
|
|
onSuccess = { response ->
|
|
val filtered = views.filter { it.id != response.type }
|
|
val result = listOf(ObjectTypeView.Search) + filtered
|
|
controlPanelInteractor.onEvent(
|
|
ControlPanelMachine.Event.ObjectTypesWidgetEvent.Show(result)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
private fun proceedWithHidingObjectTypeWidget() {
|
|
controlPanelInteractor.onEvent(ControlPanelMachine.Event.ObjectTypesWidgetEvent.Hide)
|
|
}
|
|
|
|
private fun dispatchObjectCreateEvent(objectType: String? = null) {
|
|
val details = orchestrator.stores.details.current()
|
|
val wrapper = ObjectWrapper.Basic(details.details[context]?.map ?: emptyMap())
|
|
if (wrapper.isDraft != true) return
|
|
if (objectType != null) {
|
|
val type = objectTypesProvider.get().firstOrNull { it.url == objectType }
|
|
if (type != null) {
|
|
viewModelScope.sendAnalyticsObjectCreateEvent(
|
|
analytics = analytics,
|
|
objType = type.name,
|
|
layout = type.layout.code.toDouble(),
|
|
route = EventsDictionary.Routes.objCreateHome,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
} else {
|
|
val type =
|
|
objectTypesProvider.get().firstOrNull { it.url == wrapper.type.firstOrNull() }
|
|
if (type != null) {
|
|
viewModelScope.sendAnalyticsObjectCreateEvent(
|
|
analytics = analytics,
|
|
objType = type.name,
|
|
layout = type.layout.code.toDouble(),
|
|
route = EventsDictionary.Routes.objCreateHome,
|
|
context = analyticsContext
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getObjectSmartBlockType(): SmartBlockType {
|
|
val block = blocks.firstOrNull { it.id == context }
|
|
return if (block?.content is Content.Smart) {
|
|
block.content<Content.Smart>().type
|
|
} else {
|
|
SmartBlockType.PAGE
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
//region OBJECT APPEARANCE SETTING
|
|
private fun proceedWithObjectAppearanceSettingClicked() {
|
|
val selected = currentSelection().toList()
|
|
if (selected.size == 1) {
|
|
val block = blocks.firstOrNull { it.id == selected[0] } ?: return
|
|
commands.value = EventWrapper(
|
|
Command.OpenObjectAppearanceSettingScreen(
|
|
ctx = context,
|
|
block = block.id
|
|
)
|
|
)
|
|
} else {
|
|
sendToast("Couldn't show Object Appearance Setting screen")
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
fun onCreateNewSetForType(type: Id) {
|
|
viewModelScope.launch {
|
|
createObjectSet(
|
|
CreateObjectSet.Params(
|
|
ctx = "",
|
|
type = type
|
|
)
|
|
).process(
|
|
failure = { Timber.e(it, "Error while creating a set of type: $type") },
|
|
success = { response -> proceedWithOpeningSet(response.target) }
|
|
)
|
|
}
|
|
}
|
|
|
|
//region ADD URI OR OBJECT ID TO SELECTED TEXT
|
|
fun proceedToCreateObjectAndAddToTextAsLink(name: String) {
|
|
Timber.d("proceedToCreateObjectAndAddToTextAsLink, name:[$name]")
|
|
viewModelScope.launch {
|
|
getDefaultEditorType.execute(Unit).fold(
|
|
onFailure = {
|
|
Timber.e(it, "Error while getting default object type")
|
|
},
|
|
onSuccess = { response ->
|
|
createObjectAddProceedToAddToTextAsLink(
|
|
name = name,
|
|
type = response.type
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onEditLinkClicked() {
|
|
Timber.d("onEditLinkClicked, ")
|
|
val target = orchestrator.stores.focus.current().id
|
|
val range = orchestrator.stores.textSelection.current().selection
|
|
val block = blocks.firstOrNull { it.id == target }
|
|
if (block != null && range != null) {
|
|
dispatch(
|
|
Command.OpenLinkToObjectOrWebScreen(
|
|
ctx = context,
|
|
target = target,
|
|
range = range,
|
|
isWholeBlockMarkup = false
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun onCopyLinkClicked(link: String) {
|
|
dispatch(Command.SaveTextToSystemClipboard(link))
|
|
}
|
|
|
|
private suspend fun createObjectAddProceedToAddToTextAsLink(name: String, type: String?) {
|
|
val startTime = System.currentTimeMillis()
|
|
val params = CreateNewDocument.Params(name, type)
|
|
createNewDocument.invoke(params).process(
|
|
failure = { Timber.e(it, "Error while creating new page with params: $params") },
|
|
success = { result ->
|
|
val middleTime = System.currentTimeMillis()
|
|
proceedToAddObjectToTextAsLink(id = result.id)
|
|
val objType = objectTypesProvider.get().firstOrNull { it.url == type }
|
|
viewModelScope.sendAnalyticsObjectCreateEvent(
|
|
analytics = analytics,
|
|
objType = type,
|
|
layout = objType?.layout?.code?.toDouble(),
|
|
route = EventsDictionary.Routes.objTurnInto,
|
|
context = analyticsContext,
|
|
startTime = startTime,
|
|
middleTime = middleTime
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
fun proceedToAddObjectToTextAsLink(id: Id) {
|
|
Timber.d("proceedToAddObjectToTextAsLink, target:[$id], mode:$mode")
|
|
when (mode) {
|
|
EditorMode.Edit -> {
|
|
val range = orchestrator.stores.textSelection.current().selection
|
|
if (range != null) {
|
|
dispatch(Command.ShowKeyboard)
|
|
viewModelScope.launch {
|
|
markupActionPipeline.send(
|
|
MarkupAction(
|
|
type = Markup.Type.OBJECT,
|
|
param = id
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
is EditorMode.Styling.Single -> {
|
|
val target = (mode as EditorMode.Styling.Single).target
|
|
onUpdateBlockListMarkup(
|
|
ids = listOf(target),
|
|
type = Markup.Type.OBJECT,
|
|
param = id
|
|
)
|
|
}
|
|
is EditorMode.Styling.Multi -> {
|
|
val targets = (mode as EditorMode.Styling.Multi).targets.toList()
|
|
if (targets.size == 1) {
|
|
onUpdateBlockListMarkup(
|
|
ids = targets,
|
|
type = Markup.Type.OBJECT,
|
|
param = id
|
|
)
|
|
}
|
|
}
|
|
else -> {
|
|
Timber.e("Error to proceedToAddObjectToTextAsLink, wrong mode:[$mode]")
|
|
}
|
|
}
|
|
}
|
|
|
|
fun proceedToAddUriToTextAsLink(uri: String) {
|
|
Timber.d("proceedToAddUriToTextAsLink, uri:[$uri]")
|
|
val range = orchestrator.stores.textSelection.current().selection
|
|
if (range != null) {
|
|
val target = orchestrator.stores.focus.current().id
|
|
applyLinkMarkup(
|
|
blockId = target,
|
|
link = uri,
|
|
range = range.first..range.last.dec()
|
|
)
|
|
} else {
|
|
Timber.e("Can't add uri to text, range is null")
|
|
}
|
|
}
|
|
|
|
fun onUndoRedoActionClicked() {
|
|
isUndoRedoToolbarIsVisible.value = true
|
|
}
|
|
|
|
fun onUndoRedoToolbarIsHidden() {
|
|
isUndoRedoToolbarIsVisible.value = false
|
|
}
|
|
|
|
//endregion
|
|
|
|
//region FOOTER
|
|
private fun getFooterState(root: Block, details: Block.Details): EditorFooter {
|
|
return when (details.details[root.id]?.layout?.toInt()) {
|
|
ObjectType.Layout.NOTE.code -> EditorFooter.Note
|
|
else -> EditorFooter.None
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
//region COPY FILE TO CACHE
|
|
val copyFileStatus = MutableSharedFlow<CopyFileStatus>(replay = 0)
|
|
|
|
override fun onStartCopyFileToCacheDir(uri: Uri) {
|
|
copyFileToCache.execute(
|
|
uri = uri,
|
|
scope = viewModelScope,
|
|
listener = copyFileListener
|
|
)
|
|
}
|
|
|
|
override fun onCancelCopyFileToCacheDir() {
|
|
copyFileToCache.cancel()
|
|
}
|
|
|
|
private val copyFileListener = object : OnCopyFileToCacheAction {
|
|
override fun onCopyFileStart() {
|
|
viewModelScope.launch {
|
|
copyFileStatus.emit(CopyFileStatus.Started)
|
|
}
|
|
}
|
|
|
|
override fun onCopyFileResult(result: String?) {
|
|
viewModelScope.launch {
|
|
copyFileStatus.emit(CopyFileStatus.Completed(result))
|
|
}
|
|
}
|
|
|
|
override fun onCopyFileError(msg: String) {
|
|
viewModelScope.launch {
|
|
copyFileStatus.emit(CopyFileStatus.Error(msg))
|
|
}
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
//region TEMPLATING
|
|
|
|
fun onShowTemplateClicked() {
|
|
viewModelScope.launch { onEvent(SelectTemplateEvent.OnAccepted) }
|
|
}
|
|
|
|
fun onTypeHasTemplateToolbarHidden() {
|
|
viewModelScope.launch { onEvent(SelectTemplateEvent.OnSkipped) }
|
|
}
|
|
|
|
private fun proceedWithTemplateSelection(typeId: Id) {
|
|
viewModelScope.launch {
|
|
onEvent(
|
|
SelectTemplateEvent.OnStart(
|
|
ctx = context,
|
|
type = typeId
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
//endregion
|
|
|
|
//region SIMPLE TABLES
|
|
private fun onShowSimpleTableWidgetClicked(id: Id) {
|
|
viewModelScope.launch {
|
|
onSimpleTableEvent(SimpleTableWidgetEvent.onStart(id = id))
|
|
}
|
|
}
|
|
|
|
fun onHideSimpleTableWidget() {}
|
|
|
|
private fun proceedWithSelectingCell(cellId: Id, tableId: Id) {
|
|
|
|
clearSelections()
|
|
select(listOf(cellId))
|
|
|
|
val updated = views.applyBordersToSelectedCells(
|
|
tableId = tableId,
|
|
selection = currentSelection()
|
|
)
|
|
|
|
viewModelScope.launch {
|
|
orchestrator.stores.focus.update(Editor.Focus.empty())
|
|
orchestrator.stores.views.update(updated)
|
|
renderCommand.send(Unit)
|
|
}
|
|
}
|
|
|
|
fun onSetBlockTextValueScreenDismiss() {
|
|
clearSelections()
|
|
val updated = views.removeBordersFromCells()
|
|
viewModelScope.launch {
|
|
orchestrator.stores.views.update(updated)
|
|
renderCommand.send(Unit)
|
|
}
|
|
}
|
|
//endregion
|
|
}
|
|
|
|
private const val NO_POSITION = -1
|
|
private const val PREVIEW_POSITION = 2
|
|
private const val OPEN_OBJECT_POSITION = 4 |