mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
Feature/search on page. First iteration (#998)
This commit is contained in:
parent
4233273717
commit
004e49906c
41 changed files with 957 additions and 132 deletions
|
@ -151,11 +151,6 @@ open class PageFragment :
|
|||
private val pageAdapter by lazy {
|
||||
BlockAdapter(
|
||||
blocks = mutableListOf(),
|
||||
onTitleTextChanged = { editable ->
|
||||
vm.onTitleTextChanged(
|
||||
text = editable.toString()
|
||||
)
|
||||
},
|
||||
onTextChanged = { id, editable ->
|
||||
vm.onTextChanged(
|
||||
id = id,
|
||||
|
@ -164,6 +159,7 @@ open class PageFragment :
|
|||
)
|
||||
},
|
||||
onTextBlockTextChanged = vm::onTextBlockTextChanged,
|
||||
onTitleBlockTextChanged = vm::onTitleBlockTextChanged,
|
||||
onSelectionChanged = vm::onSelectionChanged,
|
||||
onCheckboxClicked = vm::onCheckboxClicked,
|
||||
onFocusChanged = vm::onBlockFocusChanged,
|
||||
|
@ -480,6 +476,10 @@ open class PageFragment :
|
|||
vm.onCloseBlockStyleToolbarClicked()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
searchToolbar.events().collect { vm.onSearchToolbarEvent(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onApplyScrollAndMoveClicked() {
|
||||
|
@ -695,7 +695,8 @@ open class PageFragment :
|
|||
onArchiveClicked = vm::onArchiveThisPageClicked,
|
||||
onRedoClicked = vm::onActionRedoClicked,
|
||||
onUndoClicked = vm::onActionUndoClicked,
|
||||
onEnterMultiSelect = vm::onEnterMultiSelectModeClicked
|
||||
onEnterMultiSelect = vm::onEnterMultiSelectModeClicked,
|
||||
onSearchClicked = vm::onEnterSearchModeClicked
|
||||
).show()
|
||||
}
|
||||
is Command.OpenProfileMenu -> {
|
||||
|
@ -704,7 +705,8 @@ open class PageFragment :
|
|||
view = topToolbar.menu,
|
||||
onRedoClicked = vm::onActionRedoClicked,
|
||||
onUndoClicked = vm::onActionUndoClicked,
|
||||
onEnterMultiSelect = vm::onEnterMultiSelectModeClicked
|
||||
onEnterMultiSelect = vm::onEnterMultiSelectModeClicked,
|
||||
onSearchClicked = vm::onEnterSearchModeClicked
|
||||
).show()
|
||||
}
|
||||
is Command.OpenFullScreenImage -> {
|
||||
|
@ -895,6 +897,14 @@ open class PageFragment :
|
|||
recycler.removeItemDecoration(footerMentionDecorator)
|
||||
}
|
||||
}
|
||||
|
||||
state.searchToolbar.apply {
|
||||
if (isVisible) {
|
||||
searchToolbar.visible()
|
||||
} else {
|
||||
searchToolbar.gone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMentionToolbar(state: ControlPanelState.Toolbar.MentionToolbar) {
|
||||
|
|
|
@ -50,6 +50,15 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.anytypeio.anytype.core_ui.widgets.toolbar.SearchToolbarWidget
|
||||
android:id="@+id/searchToolbar"
|
||||
android:layout_width="0dp"
|
||||
android:visibility="gone"
|
||||
android:layout_height="@dimen/default_toolbar_height"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package com.anytypeio.anytype.core_ui.common
|
||||
|
||||
import android.graphics.Color
|
||||
import android.text.style.BackgroundColorSpan
|
||||
|
||||
class SearchHighlightSpan(color: Int = COLOR) : BackgroundColorSpan(color) {
|
||||
companion object {
|
||||
val COLOR = Color.parseColor("#332AA7EE")
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.other
|
|||
|
||||
import android.content.res.ColorStateList
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.FrameLayout
|
||||
|
@ -9,6 +10,7 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.core.view.postDelayed
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.common.SearchHighlightSpan
|
||||
import com.anytypeio.anytype.core_ui.extensions.avatarColor
|
||||
import com.anytypeio.anytype.core_ui.features.editor.holders.`interface`.TextHolder
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockView
|
||||
|
@ -19,6 +21,7 @@ import com.anytypeio.anytype.core_ui.tools.DefaultTextWatcher
|
|||
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
|
||||
import com.anytypeio.anytype.core_utils.ext.firstDigitByHash
|
||||
import com.anytypeio.anytype.core_utils.ext.imm
|
||||
import com.anytypeio.anytype.core_utils.ext.removeSpans
|
||||
import com.anytypeio.anytype.core_utils.ext.visible
|
||||
import com.anytypeio.anytype.emojifier.Emojifier
|
||||
import com.bumptech.glide.Glide
|
||||
|
@ -62,6 +65,18 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder {
|
|||
}
|
||||
}
|
||||
|
||||
fun applySearchHighlights(item: BlockView.Searchable) {
|
||||
content.editableText.removeSpans<SearchHighlightSpan>()
|
||||
item.highlights.forEach { highlight ->
|
||||
content.editableText.setSpan(
|
||||
SearchHighlightSpan(),
|
||||
highlight.first,
|
||||
highlight.last,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open fun setImage(item: BlockView.Title) {
|
||||
item.image?.let { url ->
|
||||
image.visible()
|
||||
|
@ -101,10 +116,12 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder {
|
|||
focus(item.isFocused)
|
||||
}
|
||||
if (payload.readWriteModeChanged()) {
|
||||
if (item.mode == BlockView.Mode.EDIT)
|
||||
enableEditMode()
|
||||
else
|
||||
enableReadMode()
|
||||
content.pauseTextWatchers {
|
||||
if (item.mode == BlockView.Mode.EDIT)
|
||||
enableEditMode()
|
||||
else
|
||||
enableReadMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,16 +150,20 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder {
|
|||
|
||||
fun bind(
|
||||
item: BlockView.Title.Document,
|
||||
onTitleTextChanged: (Editable) -> Unit,
|
||||
onTitleTextChanged: (BlockView.Title) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit,
|
||||
onPageIconClicked: () -> Unit
|
||||
) {
|
||||
super.bind(
|
||||
item = item,
|
||||
onTitleTextChanged = onTitleTextChanged,
|
||||
onTitleTextChanged = {
|
||||
item.text = it.toString()
|
||||
onTitleTextChanged(item)
|
||||
},
|
||||
onFocusChanged = onFocusChanged
|
||||
)
|
||||
setEmoji(item)
|
||||
applySearchHighlights(item)
|
||||
if (item.mode == BlockView.Mode.EDIT) {
|
||||
icon.setOnClickListener { onPageIconClicked() }
|
||||
}
|
||||
|
@ -159,6 +180,9 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder {
|
|||
setEmoji(item)
|
||||
setImage(item)
|
||||
}
|
||||
if (payload.isSearchHighlightChanged) {
|
||||
applySearchHighlights(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -229,12 +253,20 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder {
|
|||
|
||||
fun bind(
|
||||
item: BlockView.Title.Profile,
|
||||
onTitleTextChanged: (Editable) -> Unit,
|
||||
onTitleTextChanged: (BlockView.Title) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit,
|
||||
onProfileIconClicked: () -> Unit
|
||||
) {
|
||||
Timber.d("Binding profile title view: $item")
|
||||
super.bind(item, onTitleTextChanged, onFocusChanged)
|
||||
super.bind(
|
||||
item = item,
|
||||
onFocusChanged = onFocusChanged,
|
||||
onTitleTextChanged = {
|
||||
item.text = it.toString()
|
||||
onTitleTextChanged(item)
|
||||
},
|
||||
)
|
||||
applySearchHighlights(item)
|
||||
if (item.mode == BlockView.Mode.EDIT) {
|
||||
icon.setOnClickListener { onProfileIconClicked() }
|
||||
}
|
||||
|
@ -276,6 +308,9 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder {
|
|||
if (payload.isTitleIconChanged) {
|
||||
setImage(item)
|
||||
}
|
||||
if (payload.isSearchHighlightChanged) {
|
||||
applySearchHighlights(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,11 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.text
|
|||
|
||||
import android.os.Build
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.common.SearchHighlightSpan
|
||||
import com.anytypeio.anytype.core_ui.common.getBlockTextColor
|
||||
import com.anytypeio.anytype.core_ui.extensions.applyMovementMethod
|
||||
import com.anytypeio.anytype.core_ui.extensions.color
|
||||
|
@ -120,6 +122,16 @@ abstract class Text(
|
|||
clicked = clicked,
|
||||
textColor = item.getBlockTextColor()
|
||||
)
|
||||
if (item is BlockView.Searchable) {
|
||||
item.highlights.forEach { highlight ->
|
||||
content.editableText.setSpan(
|
||||
SearchHighlightSpan(),
|
||||
highlight.first,
|
||||
highlight.last,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setStyle(item: BlockView.TextBlockProps) {
|
||||
|
@ -141,7 +153,6 @@ abstract class Text(
|
|||
) {
|
||||
|
||||
content.apply {
|
||||
|
||||
setOnLongClickListener(
|
||||
EditorLongClickListener(
|
||||
t = item.id,
|
||||
|
|
|
@ -74,7 +74,7 @@ class BlockAdapter(
|
|||
private var blocks: List<BlockView>,
|
||||
private val onTextBlockTextChanged: (BlockView.Text) -> Unit,
|
||||
private val onTextChanged: (String, Editable) -> Unit,
|
||||
private val onTitleTextChanged: (Editable) -> Unit,
|
||||
private val onTitleBlockTextChanged: (BlockView.Title) -> Unit,
|
||||
private val onTitleTextInputClicked: () -> Unit,
|
||||
private val onSelectionChanged: (String, IntRange) -> Unit,
|
||||
private val onCheckboxClicked: (BlockView.Text.Checkbox) -> Unit,
|
||||
|
@ -764,7 +764,7 @@ class BlockAdapter(
|
|||
holder.apply {
|
||||
bind(
|
||||
item = blocks[position] as BlockView.Title.Document,
|
||||
onTitleTextChanged = onTitleTextChanged,
|
||||
onTitleTextChanged = onTitleBlockTextChanged,
|
||||
onFocusChanged = onFocusChanged,
|
||||
onPageIconClicked = onPageIconClicked
|
||||
)
|
||||
|
@ -793,7 +793,7 @@ class BlockAdapter(
|
|||
holder.apply {
|
||||
bind(
|
||||
item = blocks[position] as BlockView.Title.Profile,
|
||||
onTitleTextChanged = onTitleTextChanged,
|
||||
onTitleTextChanged = onTitleBlockTextChanged,
|
||||
onFocusChanged = onFocusChanged,
|
||||
onProfileIconClicked = onProfileIconClicked
|
||||
)
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.anytypeio.anytype.core_ui.features.page.BlockViewHolder.Companion.HOL
|
|||
import com.anytypeio.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_PLACEHOLDER
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_UPLOAD
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.parcel.RawValue
|
||||
|
||||
/**
|
||||
* UI-models for different types of blocks.
|
||||
|
@ -105,6 +106,16 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
val cursor: Int?
|
||||
}
|
||||
|
||||
/**
|
||||
* Views implementing this interface are supposed to highlight search results.
|
||||
* @property highlights search results that meet query.
|
||||
* @property target currently selected search result
|
||||
*/
|
||||
interface Searchable {
|
||||
val highlights: Set<IntRange>
|
||||
val target: IntRange
|
||||
}
|
||||
|
||||
interface TextBlockProps :
|
||||
Markup,
|
||||
Focusable,
|
||||
|
@ -117,7 +128,7 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
val id: String
|
||||
}
|
||||
|
||||
sealed class Text : BlockView(), TextBlockProps {
|
||||
sealed class Text : BlockView(), TextBlockProps, Searchable {
|
||||
|
||||
// Dynamic properties (expected to be synchronised with framework widget)
|
||||
|
||||
|
@ -153,7 +164,9 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override val mode: Mode = Mode.EDIT,
|
||||
override val isSelected: Boolean = false,
|
||||
override val alignment: Alignment? = null,
|
||||
override val cursor: Int? = null
|
||||
override val cursor: Int? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY
|
||||
) : Text() {
|
||||
override fun getViewType() = HOLDER_PARAGRAPH
|
||||
override val body: String get() = text
|
||||
|
@ -180,7 +193,9 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override val mode: Mode = Mode.EDIT,
|
||||
override val isSelected: Boolean = false,
|
||||
override val alignment: Alignment? = null,
|
||||
override val cursor: Int? = null
|
||||
override val cursor: Int? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY
|
||||
) : Header() {
|
||||
override fun getViewType() = HOLDER_HEADER_ONE
|
||||
override val body: String get() = text
|
||||
|
@ -205,7 +220,9 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override val mode: Mode = Mode.EDIT,
|
||||
override val isSelected: Boolean = false,
|
||||
override val alignment: Alignment? = null,
|
||||
override val cursor: Int? = null
|
||||
override val cursor: Int? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY
|
||||
) : Header() {
|
||||
override fun getViewType() = HOLDER_HEADER_TWO
|
||||
override val body: String get() = text
|
||||
|
@ -230,7 +247,9 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override val mode: Mode = Mode.EDIT,
|
||||
override val isSelected: Boolean = false,
|
||||
override val alignment: Alignment? = null,
|
||||
override val cursor: Int? = null
|
||||
override val cursor: Int? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY
|
||||
) : Header() {
|
||||
override fun getViewType() = HOLDER_HEADER_THREE
|
||||
override val body: String get() = text
|
||||
|
@ -255,7 +274,9 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override val mode: Mode = Mode.EDIT,
|
||||
override val isSelected: Boolean = false,
|
||||
override val cursor: Int? = null,
|
||||
override val alignment: Alignment? = null
|
||||
override val alignment: Alignment? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY
|
||||
) : Text() {
|
||||
override fun getViewType() = HOLDER_HIGHLIGHT
|
||||
override val body: String get() = text
|
||||
|
@ -280,7 +301,9 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override val mode: Mode = Mode.EDIT,
|
||||
override val isSelected: Boolean = false,
|
||||
override val cursor: Int? = null,
|
||||
override val alignment: Alignment? = null
|
||||
override val alignment: Alignment? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY
|
||||
) : Text(), Checkable {
|
||||
override fun getViewType() = HOLDER_CHECKBOX
|
||||
override val body: String get() = text
|
||||
|
@ -305,7 +328,9 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override val mode: Mode = Mode.EDIT,
|
||||
override val isSelected: Boolean = false,
|
||||
override val cursor: Int? = null,
|
||||
override val alignment: Alignment? = null
|
||||
override val alignment: Alignment? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY
|
||||
) : Text() {
|
||||
override fun getViewType() = HOLDER_BULLET
|
||||
override val body: String get() = text
|
||||
|
@ -331,6 +356,8 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override val isSelected: Boolean = false,
|
||||
override val cursor: Int? = null,
|
||||
override val alignment: Alignment? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY,
|
||||
val number: Int
|
||||
) : Text() {
|
||||
override fun getViewType() = HOLDER_NUMBERED
|
||||
|
@ -357,6 +384,8 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override val isSelected: Boolean = false,
|
||||
override val cursor: Int? = null,
|
||||
override val alignment: Alignment? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY,
|
||||
val toggled: Boolean = false,
|
||||
val isEmpty: Boolean = false
|
||||
) : Text() {
|
||||
|
@ -383,8 +412,10 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
val emoji: String? = null,
|
||||
override val image: String? = null,
|
||||
override val mode: Mode = Mode.EDIT,
|
||||
override val cursor: Int? = null
|
||||
) : BlockView.Title() {
|
||||
override val cursor: Int? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY,
|
||||
) : BlockView.Title(), Searchable {
|
||||
override fun getViewType() = HOLDER_TITLE
|
||||
}
|
||||
|
||||
|
@ -401,8 +432,10 @@ sealed class BlockView : ViewType, Parcelable {
|
|||
override var text: String?,
|
||||
override val image: String? = null,
|
||||
override val mode: Mode = Mode.EDIT,
|
||||
override val cursor: Int? = null
|
||||
) : BlockView.Title() {
|
||||
override val cursor: Int? = null,
|
||||
override val highlights: @RawValue Set<IntRange> = emptySet(),
|
||||
override val target: @RawValue IntRange = IntRange.EMPTY,
|
||||
) : BlockView.Title(), Searchable {
|
||||
override fun getViewType() = HOLDER_PROFILE_TITLE
|
||||
}
|
||||
|
||||
|
|
|
@ -105,6 +105,11 @@ class BlockViewDiffUtil(
|
|||
changes.add(ALIGNMENT_CHANGED)
|
||||
}
|
||||
|
||||
if (newBlock is BlockView.Searchable && oldBlock is BlockView.Searchable) {
|
||||
if (newBlock.highlights != oldBlock.highlights)
|
||||
changes.add(SEARCH_HIGHLIGHT_CHANGED)
|
||||
}
|
||||
|
||||
return if (changes.isNotEmpty())
|
||||
Payload(changes).also { Timber.d("Returning payload: $it") }
|
||||
else
|
||||
|
@ -128,6 +133,7 @@ class BlockViewDiffUtil(
|
|||
val isModeChanged: Boolean get() = changes.contains(READ_WRITE_MODE_CHANGED)
|
||||
val isSelectionChanged: Boolean get() = changes.contains(SELECTION_CHANGED)
|
||||
val isTitleIconChanged: Boolean get() = changes.contains(TITLE_ICON_CHANGED)
|
||||
val isSearchHighlightChanged: Boolean get() = changes.contains(SEARCH_HIGHLIGHT_CHANGED)
|
||||
val isAlignmentChanged: Boolean get() = changes.contains(ALIGNMENT_CHANGED)
|
||||
|
||||
fun markupChanged() = changes.contains(MARKUP_CHANGED)
|
||||
|
@ -154,5 +160,6 @@ class BlockViewDiffUtil(
|
|||
const val ALIGNMENT_CHANGED = 11
|
||||
const val CURSOR_CHANGED = 12
|
||||
const val TITLE_ICON_CHANGED = 13
|
||||
const val SEARCH_HIGHLIGHT_CHANGED = 14
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package com.anytypeio.anytype.core_ui.features.page
|
||||
|
||||
fun List<BlockView>.toReadMode(): List<BlockView> = map { view ->
|
||||
when (view) {
|
||||
is BlockView.Text.Paragraph -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Text.Checkbox -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Text.Bulleted -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Text.Numbered -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Text.Highlight -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Text.Header.One -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Text.Header.Two -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Text.Header.Three -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Text.Toggle -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Title.Document -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Title.Profile -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Title.Archive -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Code -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Error.File -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Error.Video -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Error.Picture -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Error.Bookmark -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Upload.File -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Upload.Video -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Upload.Picture -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.MediaPlaceholder.File -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.MediaPlaceholder.Video -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.MediaPlaceholder.Bookmark -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.MediaPlaceholder.Picture -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Media.File -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Media.Video -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Media.Bookmark -> view.copy(mode = BlockView.Mode.READ)
|
||||
is BlockView.Media.Picture -> view.copy(mode = BlockView.Mode.READ)
|
||||
else -> view
|
||||
}
|
||||
}
|
||||
|
||||
fun List<BlockView>.toEditMode(): List<BlockView> = map { view ->
|
||||
when (view) {
|
||||
is BlockView.Text.Paragraph -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Text.Checkbox -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Text.Bulleted -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Text.Numbered -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Text.Highlight -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Text.Header.One -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Text.Header.Two -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Text.Header.Three -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Text.Toggle -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Title.Document -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Title.Profile -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Title.Archive -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Code -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Error.File -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Error.Video -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Error.Picture -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Error.Bookmark -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Upload.File -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Upload.Video -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Upload.Picture -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.MediaPlaceholder.File -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.MediaPlaceholder.Video -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.MediaPlaceholder.Bookmark -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.MediaPlaceholder.Picture -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Media.File -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Media.Video -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Media.Bookmark -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
is BlockView.Media.Picture -> view.copy(mode = BlockView.Mode.EDIT)
|
||||
else -> view
|
||||
}
|
||||
}
|
||||
|
||||
fun List<BlockView>.clearSearchHighlights(): List<BlockView> = map { view ->
|
||||
when (view) {
|
||||
is BlockView.Text.Paragraph -> view.copy(highlights = emptySet())
|
||||
is BlockView.Text.Numbered -> view.copy(highlights = emptySet())
|
||||
is BlockView.Text.Bulleted -> view.copy(highlights = emptySet())
|
||||
is BlockView.Text.Checkbox -> view.copy(highlights = emptySet())
|
||||
is BlockView.Text.Toggle -> view.copy(highlights = emptySet())
|
||||
is BlockView.Text.Header.One -> view.copy(highlights = emptySet())
|
||||
is BlockView.Text.Header.Two -> view.copy(highlights = emptySet())
|
||||
is BlockView.Text.Header.Three -> view.copy(highlights = emptySet())
|
||||
is BlockView.Text.Highlight -> view.copy(highlights = emptySet())
|
||||
is BlockView.Title.Document -> view.copy(highlights = emptySet())
|
||||
is BlockView.Title.Profile -> view.copy(highlights = emptySet())
|
||||
else -> view
|
||||
}
|
||||
}
|
||||
|
||||
fun List<BlockView>.highlight(highlighter: (String) -> Set<IntRange>) = map { view ->
|
||||
when (view) {
|
||||
is BlockView.Text.Paragraph -> {
|
||||
view.copy(highlights = highlighter(view.text))
|
||||
}
|
||||
is BlockView.Text.Numbered -> {
|
||||
view.copy(highlights = highlighter(view.text))
|
||||
}
|
||||
is BlockView.Text.Bulleted -> {
|
||||
view.copy(highlights = highlighter(view.text))
|
||||
}
|
||||
is BlockView.Text.Checkbox -> {
|
||||
view.copy(highlights = highlighter(view.text))
|
||||
}
|
||||
is BlockView.Text.Toggle -> {
|
||||
view.copy(highlights = highlighter(view.text))
|
||||
}
|
||||
is BlockView.Text.Header.One -> {
|
||||
view.copy(highlights = highlighter(view.text))
|
||||
}
|
||||
is BlockView.Text.Header.Two -> {
|
||||
view.copy(highlights = highlighter(view.text))
|
||||
}
|
||||
is BlockView.Text.Header.Three -> {
|
||||
view.copy(highlights = highlighter(view.text))
|
||||
}
|
||||
is BlockView.Text.Highlight -> {
|
||||
view.copy(highlights = highlighter(view.text))
|
||||
}
|
||||
is BlockView.Title.Document -> {
|
||||
view.copy(highlights = highlighter(view.text ?: ""))
|
||||
}
|
||||
is BlockView.Title.Profile -> {
|
||||
view.copy(highlights = highlighter(view.text ?: ""))
|
||||
}
|
||||
else -> view
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
package com.anytypeio.anytype.core_ui.features.page
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.widget.TextView
|
||||
import com.anytypeio.anytype.core_ui.common.*
|
||||
import com.anytypeio.anytype.core_ui.extensions.applyMovementMethod
|
||||
import com.anytypeio.anytype.core_ui.extensions.cursorYBottomCoordinate
|
||||
import com.anytypeio.anytype.core_ui.extensions.preserveSelection
|
||||
import com.anytypeio.anytype.core_ui.extensions.range
|
||||
import com.anytypeio.anytype.core_ui.extensions.applyMovementMethod
|
||||
import com.anytypeio.anytype.core_ui.features.editor.holders.`interface`.TextHolder
|
||||
import com.anytypeio.anytype.core_ui.menu.EditorContextMenu
|
||||
import com.anytypeio.anytype.core_ui.tools.DefaultSpannableFactory
|
||||
|
@ -14,6 +15,7 @@ import com.anytypeio.anytype.core_ui.tools.DefaultTextWatcher
|
|||
import com.anytypeio.anytype.core_ui.tools.MentionTextWatcher
|
||||
import com.anytypeio.anytype.core_ui.widgets.text.MentionSpan
|
||||
import com.anytypeio.anytype.core_utils.ext.hideKeyboard
|
||||
import com.anytypeio.anytype.core_utils.ext.removeSpans
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
|
@ -193,7 +195,21 @@ interface TextBlockHolder : TextHolder {
|
|||
)
|
||||
}
|
||||
} else if (payload.markupChanged()) {
|
||||
setMarkup(item, clicked, item.getBlockTextColor())
|
||||
content.pauseTextWatchers { setMarkup(item, clicked, item.getBlockTextColor()) }
|
||||
}
|
||||
|
||||
if (payload.isSearchHighlightChanged) {
|
||||
if (item is BlockView.Searchable) {
|
||||
content.editableText.removeSpans<SearchHighlightSpan>()
|
||||
item.highlights.forEach { highlight ->
|
||||
content.editableText.setSpan(
|
||||
SearchHighlightSpan(),
|
||||
highlight.first,
|
||||
highlight.last,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.textColorChanged()) {
|
||||
|
@ -210,19 +226,21 @@ interface TextBlockHolder : TextHolder {
|
|||
}
|
||||
|
||||
if (payload.readWriteModeChanged()) {
|
||||
if (item.mode == BlockView.Mode.EDIT) {
|
||||
content.clearTextWatchers()
|
||||
setupTextWatcher(item) { id, editable ->
|
||||
item.apply {
|
||||
text = editable.toString()
|
||||
marks = editable.marks()
|
||||
content.pauseTextWatchers {
|
||||
if (item.mode == BlockView.Mode.EDIT) {
|
||||
content.clearTextWatchers()
|
||||
setupTextWatcher(item) { id, editable ->
|
||||
item.apply {
|
||||
text = editable.toString()
|
||||
marks = editable.marks()
|
||||
}
|
||||
onTextChanged(item)
|
||||
}
|
||||
onTextChanged(item)
|
||||
content.selectionWatcher = { onSelectionChanged(item.id, it) }
|
||||
enableEditMode()
|
||||
} else {
|
||||
enableReadMode()
|
||||
}
|
||||
content.selectionWatcher = { onSelectionChanged(item.id, it) }
|
||||
enableEditMode()
|
||||
} else {
|
||||
enableReadMode()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import com.anytypeio.anytype.core_ui.R
|
|||
import com.anytypeio.anytype.core_ui.common.Markup
|
||||
import com.anytypeio.anytype.core_ui.common.Span
|
||||
import com.anytypeio.anytype.core_ui.common.ThemeColor
|
||||
import com.anytypeio.anytype.core_ui.common.setMarkup
|
||||
import com.anytypeio.anytype.core_ui.extensions.drawable
|
||||
import com.anytypeio.anytype.core_ui.widgets.text.MentionSpan
|
||||
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
|
||||
|
|
|
@ -11,7 +11,8 @@ class DocumentPopUpMenu(
|
|||
onArchiveClicked: () -> Unit,
|
||||
onRedoClicked: () -> Unit,
|
||||
onUndoClicked: () -> Unit,
|
||||
onEnterMultiSelect: () -> Unit
|
||||
onEnterMultiSelect: () -> Unit,
|
||||
onSearchClicked: () -> Unit
|
||||
) : PopupMenu(context, view) {
|
||||
|
||||
init {
|
||||
|
@ -22,6 +23,7 @@ class DocumentPopUpMenu(
|
|||
R.id.undo -> onUndoClicked()
|
||||
R.id.redo -> onRedoClicked()
|
||||
R.id.select -> onEnterMultiSelect()
|
||||
R.id.search -> onSearchClicked()
|
||||
else -> throw IllegalStateException("Unexpected menu item: $item")
|
||||
}
|
||||
true
|
||||
|
|
|
@ -10,7 +10,8 @@ class ProfilePopUpMenu(
|
|||
view: View,
|
||||
onRedoClicked: () -> Unit,
|
||||
onUndoClicked: () -> Unit,
|
||||
onEnterMultiSelect: () -> Unit
|
||||
onEnterMultiSelect: () -> Unit,
|
||||
onSearchClicked: () -> Unit
|
||||
) : PopupMenu(context, view) {
|
||||
|
||||
init {
|
||||
|
@ -20,6 +21,7 @@ class ProfilePopUpMenu(
|
|||
R.id.undo -> onUndoClicked()
|
||||
R.id.redo -> onRedoClicked()
|
||||
R.id.select -> onEnterMultiSelect()
|
||||
R.id.search -> onSearchClicked()
|
||||
else -> throw IllegalStateException("Unexpected menu item: $item")
|
||||
}
|
||||
true
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package com.anytypeio.anytype.core_ui.reactive
|
||||
|
||||
import android.os.Looper
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.CheckResult
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
|
@ -19,6 +23,36 @@ fun View.clicks(): Flow<Unit> = callbackFlow<Unit> {
|
|||
awaitClose { setOnClickListener(null) }
|
||||
}.conflate()
|
||||
|
||||
@CheckResult
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun EditText.textChanges(): Flow<CharSequence> = callbackFlow<CharSequence> {
|
||||
checkMainThread()
|
||||
val listener = object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun afterTextChanged(s: Editable) = Unit
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
safeOffer(s)
|
||||
}
|
||||
}
|
||||
addTextChangedListener(listener)
|
||||
awaitClose { removeTextChangedListener(listener) }
|
||||
}.conflate()
|
||||
|
||||
@CheckResult
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun EditText.afterTextChanges(): Flow<CharSequence> = callbackFlow<CharSequence> {
|
||||
checkMainThread()
|
||||
val listener = object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
safeOffer(s.toString())
|
||||
}
|
||||
}
|
||||
addTextChangedListener(listener)
|
||||
awaitClose { removeTextChangedListener(listener) }
|
||||
}.conflate()
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
fun <E> SendChannel<E>.safeOffer(value: E): Boolean {
|
||||
return runCatching { offer(value) }.getOrDefault(false)
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.anytypeio.anytype.core_ui.state
|
|||
import com.anytypeio.anytype.core_ui.common.Alignment
|
||||
import com.anytypeio.anytype.core_ui.common.Markup
|
||||
import com.anytypeio.anytype.core_ui.features.page.styling.StylingMode
|
||||
import com.anytypeio.anytype.core_ui.features.page.styling.StylingType
|
||||
import com.anytypeio.anytype.core_ui.model.StyleConfig
|
||||
import com.anytypeio.anytype.core_ui.widgets.toolbar.adapter.Mention
|
||||
|
||||
|
@ -18,7 +17,8 @@ data class ControlPanelState(
|
|||
val mainToolbar: Toolbar.Main,
|
||||
val stylingToolbar: Toolbar.Styling,
|
||||
val multiSelect: Toolbar.MultiSelect,
|
||||
val mentionToolbar: Toolbar.MentionToolbar
|
||||
val mentionToolbar: Toolbar.MentionToolbar,
|
||||
val searchToolbar: Toolbar.SearchToolbar = Toolbar.SearchToolbar(isVisible = false)
|
||||
) {
|
||||
|
||||
sealed class Toolbar {
|
||||
|
@ -154,6 +154,13 @@ data class ControlPanelState(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search toolbar.
|
||||
*/
|
||||
data class SearchToolbar(
|
||||
override val isVisible: Boolean
|
||||
) : Toolbar()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,6 +203,9 @@ data class ControlPanelState(
|
|||
updateList = false,
|
||||
mentionFrom = null,
|
||||
mentions = emptyList()
|
||||
),
|
||||
searchToolbar = Toolbar.SearchToolbar(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package com.anytypeio.anytype.core_ui.widgets.toolbar
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.anytypeio.anytype.core_ui.R
|
||||
import com.anytypeio.anytype.core_ui.reactive.afterTextChanges
|
||||
import com.anytypeio.anytype.core_ui.reactive.clicks
|
||||
import kotlinx.android.synthetic.main.widget_doc_search_engine_toolbar.view.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class SearchToolbarWidget : ConstraintLayout {
|
||||
|
||||
constructor(
|
||||
context: Context
|
||||
) : this(context, null)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?
|
||||
) : this(context, attrs, 0)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr) {
|
||||
inflate()
|
||||
}
|
||||
|
||||
fun events(): Flow<Event> {
|
||||
val next = docSearchNextSearchResult.clicks().map { Event.Next }
|
||||
val previous = docSearchPreviousSearchResult.clicks().map { Event.Previous }
|
||||
val cancel = docSearchCancelButton.clicks().map { Event.Cancel }
|
||||
val clear = docSearchClearIcon.clicks().onEach { docSearchInputField.setText("") }
|
||||
.map { Event.Clear }
|
||||
val queries = docSearchInputField.afterTextChanges().map { Event.Query(it.toString()) }
|
||||
return flowOf(cancel, clear, queries, next, previous).flattenMerge()
|
||||
}
|
||||
|
||||
private fun inflate() {
|
||||
LayoutInflater.from(context).inflate(R.layout.widget_doc_search_engine_toolbar, this)
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
object Clear : Event()
|
||||
object Cancel : Event()
|
||||
object Next : Event()
|
||||
object Previous : Event()
|
||||
data class Query(val query: String) : Event()
|
||||
}
|
||||
|
||||
companion object
|
||||
}
|
14
core-ui/src/main/res/drawable/ic_doc_search.xml
Normal file
14
core-ui/src/main/res/drawable/ic_doc_search.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M10.5,15.5C13.2614,15.5 15.5,13.2614 15.5,10.5C15.5,7.7386 13.2614,5.5 10.5,5.5C7.7386,5.5 5.5,7.7386 5.5,10.5C5.5,13.2614 7.7386,15.5 10.5,15.5ZM10.5,17C14.0899,17 17,14.0899 17,10.5C17,6.9102 14.0899,4 10.5,4C6.9102,4 4,6.9102 4,10.5C4,14.0899 6.9102,17 10.5,17Z"
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:pathData="M14.2929,14.2929C14.6834,13.9024 15.3166,13.9024 15.7071,14.2929L19.7071,18.2929C20.0976,18.6834 20.0976,19.3166 19.7071,19.7071C19.3166,20.0976 18.6834,20.0976 18.2929,19.7071L14.2929,15.7071C13.9024,15.3166 13.9024,14.6834 14.2929,14.2929Z"
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
10
core-ui/src/main/res/drawable/ic_doc_search_delete.xml
Normal file
10
core-ui/src/main/res/drawable/ic_doc_search_delete.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M22.022,12C22.022,17.5228 17.5448,22 12.022,22C6.4991,22 2.022,17.5228 2.022,12C2.022,6.4771 6.4991,2 12.022,2C17.5448,2 22.022,6.4771 22.022,12ZM16.5876,7.4343C16.9,7.7467 16.9,8.2532 16.5876,8.5656L13.1533,11.9999L16.5877,15.4343C16.9001,15.7467 16.9001,16.2532 16.5877,16.5656C16.2753,16.8781 15.7688,16.8781 15.4563,16.5656L12.022,13.1313L8.5876,16.5656C8.2752,16.8781 7.7687,16.8781 7.4562,16.5656C7.1438,16.2532 7.1438,15.7467 7.4562,15.4343L10.8906,11.9999L7.4563,8.5656C7.1439,8.2532 7.1439,7.7467 7.4563,7.4343C7.7688,7.1219 8.2753,7.1219 8.5877,7.4343L12.022,10.8685L15.4562,7.4343C15.7687,7.1219 16.2752,7.1219 16.5876,7.4343Z"
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
10
core-ui/src/main/res/drawable/ic_search_next_result.xml
Normal file
10
core-ui/src/main/res/drawable/ic_search_next_result.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M20.7071,8.2929C20.3166,7.9023 19.6834,7.9023 19.2929,8.2929L12,15.5858L4.7071,8.2929C4.3166,7.9023 3.6834,7.9023 3.2929,8.2929C2.9024,8.6834 2.9024,9.3166 3.2929,9.7071L12,18.4142L20.7071,9.7071C21.0976,9.3166 21.0976,8.6834 20.7071,8.2929Z"
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
10
core-ui/src/main/res/drawable/ic_search_previous_result.xml
Normal file
10
core-ui/src/main/res/drawable/ic_search_previous_result.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3.2929,15.7071C3.6834,16.0977 4.3166,16.0977 4.7071,15.7071L12,8.4142L19.2929,15.7071C19.6834,16.0977 20.3166,16.0977 20.7071,15.7071C21.0976,15.3166 21.0976,14.6834 20.7071,14.2929L12,5.5858L3.2929,14.2929C2.9024,14.6834 2.9024,15.3166 3.2929,15.7071Z"
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
6
core-ui/src/main/res/drawable/rectangle_doc_search.xml
Normal file
6
core-ui/src/main/res/drawable/rectangle_doc_search.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="8dp" />
|
||||
<solid android:color="#F3F2EC" />
|
||||
</shape>
|
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:background="@color/white"
|
||||
android:layout_height="52dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/docSearchPreviousSearchResult"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:contentDescription="@string/content_description_document_previous_search_result_icon"
|
||||
android:src="@drawable/ic_search_previous_result"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/docSearchNextSearchResult"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:contentDescription="@string/content_description_document_next_search_result_icon"
|
||||
android:src="@drawable/ic_search_next_result"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/docSearchPreviousSearchResult"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/docSearchCancelButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/docSearchRectangle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/rectangle_doc_search"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/docSearchCancelButton"
|
||||
app:layout_constraintStart_toEndOf="@+id/docSearchNextSearchResult"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/docSearchSearchIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/content_description_document_search_icon"
|
||||
android:src="@drawable/ic_doc_search"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/docSearchRectangle"
|
||||
app:layout_constraintStart_toStartOf="@+id/docSearchRectangle"
|
||||
app:layout_constraintTop_toTopOf="@+id/docSearchRectangle" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/docSearchClearIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@string/content_description_document_clear_search_icon"
|
||||
android:src="@drawable/ic_doc_search_delete"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/docSearchRectangle"
|
||||
app:layout_constraintEnd_toEndOf="@+id/docSearchRectangle"
|
||||
app:layout_constraintTop_toTopOf="@+id/docSearchRectangle" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/docSearchInputField"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@null"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:hint="@string/your_search_query"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/docSearchRectangle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/docSearchClearIcon"
|
||||
app:layout_constraintStart_toEndOf="@+id/docSearchSearchIcon"
|
||||
app:layout_constraintTop_toTopOf="@+id/docSearchRectangle" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -12,4 +12,7 @@
|
|||
<item
|
||||
android:id="@+id/select"
|
||||
android:title="@string/select" />
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:title="@string/search" />
|
||||
</menu>
|
|
@ -26,6 +26,10 @@
|
|||
<string name="content_description_bookmark_image">Bookmark image</string>
|
||||
<string name="content_description_toggle_icon">Toggle icon</string>
|
||||
<string name="content_description_turn_into_arrow">Turn into arrow</string>
|
||||
<string name="content_description_document_previous_search_result_icon">Document previous search result icon</string>
|
||||
<string name="content_description_document_next_search_result_icon">Document next search result icon</string>
|
||||
<string name="content_description_document_search_icon">Document search icon</string>
|
||||
<string name="content_description_document_clear_search_icon">Document clear search icon</string>
|
||||
|
||||
<string name="option_toolbar_header_turn_into">Turn into</string>
|
||||
<string name="option_toolbar_header_add_block">Add block</string>
|
||||
|
@ -213,6 +217,7 @@
|
|||
<string name="redo">Redo</string>
|
||||
<string name="copy">Copy</string>
|
||||
<string name="select">Select</string>
|
||||
<string name="search">Search</string>
|
||||
|
||||
<string name="choose_emoji">Choose emoji</string>
|
||||
<string name="logo_transition">logo_transition</string>
|
||||
|
@ -227,6 +232,7 @@
|
|||
<string name="move">Move</string>
|
||||
<string name="mention_suggester_new_page">Create new page</string>
|
||||
<string name="widget_archive_select_pages">Select pages</string>
|
||||
<string name="your_search_query">Search</string>
|
||||
<plurals name="page_selected">
|
||||
<item quantity="zero">Select pages</item>
|
||||
<item quantity="one">%d page selected</item>
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.graphics.drawable.ColorDrawable
|
|||
import android.os.Build
|
||||
import android.text.Editable
|
||||
import android.text.InputType
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.marginLeft
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -28,7 +27,9 @@ import com.anytypeio.anytype.core_ui.features.editor.holders.text.*
|
|||
import com.anytypeio.anytype.core_ui.features.editor.holders.upload.FileUpload
|
||||
import com.anytypeio.anytype.core_ui.features.editor.holders.upload.PictureUpload
|
||||
import com.anytypeio.anytype.core_ui.features.editor.holders.upload.VideoUpload
|
||||
import com.anytypeio.anytype.core_ui.features.page.*
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockAdapter
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockView
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockViewDiffUtil
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.BACKGROUND_COLOR_CHANGED
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.CURSOR_CHANGED
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.FOCUS_CHANGED
|
||||
|
@ -36,6 +37,7 @@ import com.anytypeio.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.R
|
|||
import com.anytypeio.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.SELECTION_CHANGED
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.TEXT_CHANGED
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.TEXT_COLOR_CHANGED
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockViewHolder
|
||||
import com.anytypeio.anytype.core_ui.tools.ClipboardInterceptor
|
||||
import com.anytypeio.anytype.core_utils.ext.dimen
|
||||
import com.anytypeio.anytype.core_utils.ext.hexColorCode
|
||||
|
@ -1495,7 +1497,7 @@ class BlockAdapterTest {
|
|||
|
||||
// Setup
|
||||
|
||||
val events = mutableListOf<Editable>()
|
||||
val events = mutableListOf<BlockView.Title>()
|
||||
|
||||
val title = BlockView.Title.Document(
|
||||
text = MockDataFactory.randomString(),
|
||||
|
@ -1507,9 +1509,7 @@ class BlockAdapterTest {
|
|||
|
||||
val adapter = buildAdapter(
|
||||
views = views,
|
||||
onTitleTextChanged = { editable ->
|
||||
events.add(editable)
|
||||
}
|
||||
onTitleBlockTextChanged = { events.add(it) }
|
||||
)
|
||||
|
||||
val recycler = RecyclerView(context).apply {
|
||||
|
@ -1526,9 +1526,11 @@ class BlockAdapterTest {
|
|||
|
||||
assertTrue { events.isEmpty() }
|
||||
|
||||
holder.content.setText(MockDataFactory.randomString())
|
||||
val changed = MockDataFactory.randomString()
|
||||
|
||||
assertTrue { events.size == 1 && events.first() == holder.content.text }
|
||||
holder.content.setText(changed)
|
||||
|
||||
assertTrue { events.size == 1 && events.first() == title.copy(text = changed) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3346,7 +3348,7 @@ class BlockAdapterTest {
|
|||
views: List<BlockView>,
|
||||
onSplitLineEnterClicked: (String, Editable, IntRange) -> Unit = { _, _, _ -> },
|
||||
onFocusChanged: (String, Boolean) -> Unit = { _, _ -> },
|
||||
onTitleTextChanged: (Editable) -> Unit = {},
|
||||
onTitleBlockTextChanged: (BlockView.Title) -> Unit = {},
|
||||
onTextChanged: (String, Editable) -> Unit = { _, _ -> }
|
||||
): BlockAdapter {
|
||||
return BlockAdapter(
|
||||
|
@ -3364,7 +3366,7 @@ class BlockAdapterTest {
|
|||
onTogglePlaceholderClicked = {},
|
||||
onToggleClicked = {},
|
||||
onTextBlockTextChanged = {},
|
||||
onTitleTextChanged = onTitleTextChanged,
|
||||
onTitleBlockTextChanged = onTitleBlockTextChanged,
|
||||
onContextMenuStyleClick = {},
|
||||
onTitleTextInputClicked = {},
|
||||
onClickListener = {},
|
||||
|
|
|
@ -816,4 +816,38 @@ class BlockViewDiffUtilTest {
|
|||
actual = payload
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should detect search highlight changes in paragraph block`() {
|
||||
val index = 0
|
||||
|
||||
val id = MockDataFactory.randomUuid()
|
||||
|
||||
val oldBlock = BlockView.Text.Paragraph(
|
||||
id = id,
|
||||
highlights = emptySet(),
|
||||
text = MockDataFactory.randomString()
|
||||
)
|
||||
|
||||
val newBlock: BlockView = oldBlock.copy(
|
||||
highlights = setOf(0..1)
|
||||
)
|
||||
|
||||
val old = listOf(oldBlock)
|
||||
|
||||
val new = listOf(newBlock)
|
||||
|
||||
val diff = BlockViewDiffUtil(old = old, new = new)
|
||||
|
||||
val payload = diff.getChangePayload(index, index)
|
||||
|
||||
val expected = Payload(
|
||||
changes = listOf(BlockViewDiffUtil.SEARCH_HIGHLIGHT_CHANGED)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = expected,
|
||||
actual = payload
|
||||
)
|
||||
}
|
||||
}
|
|
@ -371,7 +371,7 @@ class HeaderBlockTest {
|
|||
onTogglePlaceholderClicked = {},
|
||||
onToggleClicked = {},
|
||||
onTextBlockTextChanged = {},
|
||||
onTitleTextChanged = onTitleTextChanged,
|
||||
onTitleBlockTextChanged = {},
|
||||
onContextMenuStyleClick = {},
|
||||
onTitleTextInputClicked = {},
|
||||
onClickListener = {},
|
||||
|
|
|
@ -107,7 +107,7 @@ class HighlightingBlockTest {
|
|||
onTogglePlaceholderClicked = {},
|
||||
onToggleClicked = {},
|
||||
onTextBlockTextChanged = {},
|
||||
onTitleTextChanged = onTitleTextChanged,
|
||||
onTitleBlockTextChanged = {},
|
||||
onContextMenuStyleClick = {},
|
||||
onTitleTextInputClicked = {},
|
||||
onClickListener = {},
|
||||
|
|
|
@ -371,7 +371,7 @@ class BlockAdapterCursorBindingTest {
|
|||
onTogglePlaceholderClicked = {},
|
||||
onToggleClicked = {},
|
||||
onTextBlockTextChanged = {},
|
||||
onTitleTextChanged = onTitleTextChanged,
|
||||
onTitleBlockTextChanged = {},
|
||||
onContextMenuStyleClick = {},
|
||||
onTitleTextInputClicked = {},
|
||||
onClickListener = {},
|
||||
|
|
|
@ -42,7 +42,7 @@ open class BlockAdapterTestSetup {
|
|||
onTogglePlaceholderClicked = {},
|
||||
onToggleClicked = onToggleClicked,
|
||||
onTextBlockTextChanged = onTextBlockTextChanged,
|
||||
onTitleTextChanged = onTitleTextChanged,
|
||||
onTitleBlockTextChanged = {},
|
||||
onContextMenuStyleClick = {},
|
||||
onTitleTextInputClicked = {},
|
||||
onClickListener = {},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
package com.anytypeio.anytype.domain.page
|
||||
|
||||
enum class EditorMode {
|
||||
EDITING, MULTI_SELECT, SCROLL_AND_MOVE, ACTION_MODE
|
||||
EDITING, MULTI_SELECT, SCROLL_AND_MOVE, ACTION_MODE, SEARCH
|
||||
}
|
|
@ -124,9 +124,16 @@ sealed class ControlPanelMachine {
|
|||
val style: Block.Content.Text.Style
|
||||
) : Event()
|
||||
|
||||
data class OnBlockActionToolbarStyleClicked(val target: Block,
|
||||
val focused: Boolean,
|
||||
val selection: IntRange?) : Event()
|
||||
data class OnBlockActionToolbarStyleClicked(
|
||||
val target: Block,
|
||||
val focused: Boolean,
|
||||
val selection: IntRange?
|
||||
) : Event()
|
||||
|
||||
sealed class SearchToolbar : Event() {
|
||||
object OnEnterSearchMode : SearchToolbar()
|
||||
object OnExitSearchMode : SearchToolbar()
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling-toolbar-related events
|
||||
|
@ -299,6 +306,21 @@ sealed class ControlPanelMachine {
|
|||
is Event.MultiSelect -> {
|
||||
handleMultiSelectEvent(event, state)
|
||||
}
|
||||
is Event.SearchToolbar.OnEnterSearchMode -> state.copy(
|
||||
searchToolbar = Toolbar.SearchToolbar(isVisible = true),
|
||||
mainToolbar = Toolbar.Main(isVisible = false),
|
||||
multiSelect = Toolbar.MultiSelect(
|
||||
isVisible = false,
|
||||
isScrollAndMoveEnabled = false,
|
||||
count = 0
|
||||
),
|
||||
stylingToolbar = Toolbar.Styling.reset(),
|
||||
navigationToolbar = Toolbar.Navigation(isVisible = false)
|
||||
)
|
||||
is Event.SearchToolbar.OnExitSearchMode -> state.copy(
|
||||
searchToolbar = state.searchToolbar.copy(isVisible = false),
|
||||
navigationToolbar = state.navigationToolbar.copy(isVisible = true)
|
||||
)
|
||||
is Event.SAM -> {
|
||||
handleScrollAndMoveEvent(event, state)
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import com.anytypeio.anytype.core_ui.features.page.styling.StylingMode
|
|||
import com.anytypeio.anytype.core_ui.model.UiBlock
|
||||
import com.anytypeio.anytype.core_ui.state.ControlPanelState
|
||||
import com.anytypeio.anytype.core_ui.widgets.ActionItemType
|
||||
import com.anytypeio.anytype.core_ui.widgets.toolbar.SearchToolbarWidget
|
||||
import com.anytypeio.anytype.core_ui.widgets.toolbar.adapter.Mention
|
||||
import com.anytypeio.anytype.core_ui.widgets.toolbar.adapter.MentionAdapter
|
||||
import com.anytypeio.anytype.core_ui.widgets.toolbar.adapter.getMentionName
|
||||
|
@ -73,6 +74,7 @@ import com.anytypeio.anytype.presentation.page.editor.*
|
|||
import com.anytypeio.anytype.presentation.page.model.TextUpdate
|
||||
import com.anytypeio.anytype.presentation.page.render.BlockViewRenderer
|
||||
import com.anytypeio.anytype.presentation.page.render.DefaultBlockViewRenderer
|
||||
import com.anytypeio.anytype.presentation.page.search.search
|
||||
import com.anytypeio.anytype.presentation.page.selection.SelectionStateHolder
|
||||
import com.anytypeio.anytype.presentation.page.toggle.ToggleStateHolder
|
||||
import com.anytypeio.anytype.presentation.util.Bridge
|
||||
|
@ -82,6 +84,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class PageViewModel(
|
||||
private val openPage: OpenPage,
|
||||
|
@ -648,8 +651,15 @@ class PageViewModel(
|
|||
viewModelScope.launch { orchestrator.proxies.changes.send(update) }
|
||||
}
|
||||
|
||||
fun onTitleTextChanged(text: String) {
|
||||
viewModelScope.launch { titleChannel.send(text) }
|
||||
fun onTitleBlockTextChanged(view: BlockView.Title) {
|
||||
val new = views.map { if (it.id == view.id) view else it }
|
||||
val update = TextUpdate.Default(
|
||||
target = view.id,
|
||||
text = view.text ?: EMPTY_TEXT,
|
||||
markup = emptyList()
|
||||
)
|
||||
viewModelScope.launch { orchestrator.stores.views.update(new) }
|
||||
viewModelScope.launch { orchestrator.proxies.changes.send(update) }
|
||||
}
|
||||
|
||||
fun onTextBlockTextChanged(
|
||||
|
@ -1331,6 +1341,39 @@ class PageViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun 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 onSearchToolbarEvent(event: SearchToolbarWidget.Event) {
|
||||
when (event) {
|
||||
is SearchToolbarWidget.Event.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 { txt -> txt.search(pattern) }
|
||||
}
|
||||
viewModelScope.launch { orchestrator.stores.views.update(update) }
|
||||
viewModelScope.launch { renderCommand.send(Unit) }
|
||||
}
|
||||
is SearchToolbarWidget.Event.Cancel -> {
|
||||
mode = EditorMode.EDITING
|
||||
val update = views.clearSearchHighlights().toEditMode()
|
||||
viewModelScope.launch { orchestrator.stores.views.update(update) }
|
||||
viewModelScope.launch { renderCommand.send(Unit) }
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.SearchToolbar.OnExitSearchMode)
|
||||
}
|
||||
else -> _toasts.offer("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
fun onAddTextBlockClicked(style: Content.Text.Style) {
|
||||
|
||||
val target = blocks.first { it.id == orchestrator.stores.focus.current().id }
|
||||
|
@ -2768,6 +2811,7 @@ class PageViewModel(
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val EMPTY_TEXT = ""
|
||||
const val EMPTY_CONTEXT = ""
|
||||
const val EMPTY_FOCUS_ID = ""
|
||||
const val TEXT_CHANGES_DEBOUNCE_DURATION = 500L
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package com.anytypeio.anytype.presentation.page.search
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
fun String.search(pattern: Pattern): Set<IntRange> {
|
||||
val result = mutableSetOf<IntRange>()
|
||||
val matcher = pattern.matcher(this)
|
||||
while (matcher.find()) {
|
||||
result.add(matcher.start()..matcher.end())
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -3377,68 +3377,6 @@ open class PageViewModelTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should start updating title on title-text-changed event with delay`() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val root = MockDataFactory.randomUuid()
|
||||
val title = MockBlockFactory.makeTitleBlock()
|
||||
|
||||
val page = listOf(
|
||||
Block(
|
||||
id = root,
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Smart(Block.Content.Smart.Type.PAGE),
|
||||
children = listOf(title.id)
|
||||
),
|
||||
title
|
||||
)
|
||||
|
||||
val flow: Flow<List<Event.Command>> = flow {
|
||||
delay(100)
|
||||
emit(
|
||||
listOf(
|
||||
Event.Command.ShowBlock(
|
||||
root = root,
|
||||
blocks = page,
|
||||
context = root
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
stubObserveEvents(flow)
|
||||
stubOpenPage()
|
||||
buildViewModel()
|
||||
|
||||
stubUpdateTitle()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
coroutineTestRule.advanceTime(100)
|
||||
|
||||
// TESTING
|
||||
|
||||
val update = MockDataFactory.randomString()
|
||||
|
||||
vm.onTitleTextChanged(update)
|
||||
|
||||
runBlockingTest {
|
||||
verify(updateTitle, never()).invoke(
|
||||
params = any()
|
||||
)
|
||||
}
|
||||
|
||||
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
|
||||
|
||||
runBlockingTest {
|
||||
verify(updateTitle, times(1)).invoke(
|
||||
params = any()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should enter multi-select mode and select blocks`() {
|
||||
|
||||
|
|
|
@ -2,12 +2,16 @@ package com.anytypeio.anytype.presentation.page.editor
|
|||
|
||||
import MockDataFactory
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.anytypeio.anytype.core_ui.features.page.BlockView
|
||||
import com.anytypeio.anytype.core_ui.state.ControlPanelState
|
||||
import com.anytypeio.anytype.domain.block.interactor.UpdateText
|
||||
import com.anytypeio.anytype.domain.block.model.Block
|
||||
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
|
||||
import com.anytypeio.anytype.presentation.page.PageViewModel
|
||||
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
|
||||
import com.jraska.livedata.test
|
||||
import com.nhaarman.mockitokotlin2.verifyBlocking
|
||||
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
|
@ -168,4 +172,57 @@ class EditorTitleTest : EditorPresentationTestSetup() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should start updating title on title-text-changed event with delay`() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val page = listOf(
|
||||
Block(
|
||||
id = root,
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Smart(Block.Content.Smart.Type.PAGE),
|
||||
children = listOf(header.id)
|
||||
),
|
||||
header,
|
||||
title
|
||||
)
|
||||
|
||||
stubInterceptEvents()
|
||||
stubOpenDocument(page)
|
||||
stubUpdateText()
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.onStart(root)
|
||||
|
||||
// TESTING
|
||||
|
||||
val update = MockDataFactory.randomString()
|
||||
|
||||
vm.onTitleBlockTextChanged(
|
||||
BlockView.Title.Document(
|
||||
id = title.id,
|
||||
text = update
|
||||
)
|
||||
)
|
||||
|
||||
verifyZeroInteractions(updateTitle)
|
||||
verifyZeroInteractions(updateText)
|
||||
|
||||
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
|
||||
|
||||
verifyZeroInteractions(updateTitle)
|
||||
|
||||
verifyBlocking(updateText) {
|
||||
invoke(
|
||||
UpdateText.Params(
|
||||
context = root,
|
||||
text = update,
|
||||
target = title.id,
|
||||
marks = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package com.anytypeio.anytype.presentation.page.search
|
||||
|
||||
import org.junit.Test
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DocumentSearchEngineTest {
|
||||
|
||||
@Test
|
||||
fun `should find nothing`() {
|
||||
|
||||
val text = "FooBarFoo"
|
||||
|
||||
val query = "Far"
|
||||
|
||||
val pattern = Pattern.compile(query, Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
|
||||
|
||||
val result = text.search(pattern)
|
||||
|
||||
assertEquals(
|
||||
expected = setOf(),
|
||||
actual = result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find 'foo' twice at the start and at the end`() {
|
||||
|
||||
val text = "FooBarFoo"
|
||||
|
||||
val query = "Foo"
|
||||
|
||||
val pattern = Pattern.compile(query, Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
|
||||
|
||||
val result = text.search(pattern)
|
||||
|
||||
assertEquals(
|
||||
expected = setOf(0..3, 6..9),
|
||||
actual = result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find 'bar' once in the middle`() {
|
||||
|
||||
val text = "FooBarFoo"
|
||||
|
||||
val query = "Bar"
|
||||
|
||||
val pattern = Pattern.compile(query, Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
|
||||
|
||||
val result = text.search(pattern)
|
||||
|
||||
assertEquals(
|
||||
expected = setOf(3..6),
|
||||
actual = result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find 'bar' surrounded by empty spaces only once in the middle`() {
|
||||
|
||||
val text = "Foo Bar Foo"
|
||||
|
||||
val query = "Bar"
|
||||
|
||||
val pattern = Pattern.compile(query, Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
|
||||
|
||||
val result = text.search(pattern)
|
||||
|
||||
assertEquals(
|
||||
expected = setOf(4..7),
|
||||
actual = result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find 'f' in all words case-insensitive`() {
|
||||
|
||||
val text = "Five fast flying machines"
|
||||
|
||||
val query = "f"
|
||||
|
||||
val pattern = Pattern.compile(query, Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
|
||||
|
||||
val result = text.search(pattern)
|
||||
|
||||
assertEquals(
|
||||
expected = setOf(0..1, 5..6, 10..11),
|
||||
actual = result
|
||||
)
|
||||
}
|
||||
}
|
|
@ -19,12 +19,16 @@
|
|||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name=".DisabledAnimationActivity">
|
||||
<activity
|
||||
android:name=".search.SearchOnPageActivity"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".DisabledAnimationActivity" />
|
||||
<activity android:name=".ScrollAndMove" />
|
||||
<activity android:name=".StyleActivity" />
|
||||
<activity android:name=".MainActivity" />
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package com.anytypeio.anytype.sample.search
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.anytypeio.anytype.sample.R
|
||||
import kotlinx.android.synthetic.main.activity_search_on_page.*
|
||||
|
||||
class SearchOnPageActivity : AppCompatActivity(R.layout.activity_search_on_page) {
|
||||
|
||||
private val items = mutableListOf<SearchOnPageAdapter.Item>().apply {
|
||||
repeat(10) {
|
||||
add(
|
||||
SearchOnPageAdapter.Item(
|
||||
id = it,
|
||||
txt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val mockAdapter = SearchOnPageAdapter(
|
||||
items = items
|
||||
)
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
recycler.apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = mockAdapter
|
||||
}
|
||||
search.doOnTextChanged { text, start, before, count ->
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.anytypeio.anytype.sample.search
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.anytypeio.anytype.sample.R
|
||||
import com.anytypeio.anytype.sample.adapter.AbstractAdapter
|
||||
import com.anytypeio.anytype.sample.adapter.AbstractHolder
|
||||
import kotlinx.android.synthetic.main.item_editable.view.*
|
||||
|
||||
class SearchOnPageAdapter(
|
||||
private var items: List<Item>
|
||||
) : AbstractAdapter<SearchOnPageAdapter.Item>(items) {
|
||||
|
||||
override fun update(update: List<Item>) {
|
||||
this.items = update
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractHolder<Item> {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view = inflater.inflate(R.layout.item_editable, parent, false)
|
||||
return ItemViewHolder(view)
|
||||
}
|
||||
|
||||
data class Item(
|
||||
val id: Int,
|
||||
val txt: String
|
||||
)
|
||||
|
||||
class ItemViewHolder(view: View) : AbstractHolder<Item>(view) {
|
||||
override fun bind(item: Item) {
|
||||
itemView.input.setText(item.txt)
|
||||
}
|
||||
}
|
||||
}
|
35
sample/src/main/res/layout/activity_search_on_page.xml
Normal file
35
sample/src/main/res/layout/activity_search_on_page.xml
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".search.SearchOnPageActivity">
|
||||
|
||||
<EditText
|
||||
android:fontFamily="monospace"
|
||||
android:id="@+id/search"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:fontFamily="monospace"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/search" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Add table
Add a link
Reference in a new issue