mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
(ISSUE-112) Link/unlink text (#158)
* IS-112 update package * IS-112 add base classes * IS-112 add link design * IS-112 add link, navigation * IS-112 add link, di * IS-112 add link as mark type * IS-112 link di * IS-112, link navigation * IS-112, add link click to page * IS-112, add ext funcs + tests * IS-112, link, view + viewmodel * IS-112 add link span * IS-112 send url link to page + send to middleware * IS-112 update navigation with Link model * IS-112 share link view model between page and link fragment * IS-112 add linkfy to edit widget * IS-112 map to link on click * IS-112 use parcelize in module * IS-112 add block markup extension funcs + tests * IS-112 send url from link markup * IS-112 remove link model * IS-112 use link view model as shared state * IS-112 add unlink usecase * IS-112 update link di * IS-112 add update link marks use case + tests * IS-112 add can unlink text use case + tests * IS-112 use listener for fragment navigation * IS-112 link view model logic update * IS-112 page view model update * IS-112 add remove link mark use case + tests * IS-112 update di * IS-112 add unlink logic + fix link * IS-112 fix after merge * IS-112 rerender blocks after link/unlink * IS-112 fix range * IS-112 fix link * IS-112 remove link from nav graph * IS-112 code style fixes * IS-112 code style fixes * IS-112 after PR fixes
This commit is contained in:
parent
a4e7f5480b
commit
ac0d64f8ab
42 changed files with 1510 additions and 41 deletions
|
@ -108,6 +108,13 @@ class ComponentManager(private val main: MainComponent) {
|
|||
.build()
|
||||
}
|
||||
|
||||
val linkAddComponent = Component {
|
||||
main
|
||||
.linkAddComponentBuilder()
|
||||
.linkModule(LinkModule())
|
||||
.build()
|
||||
}
|
||||
|
||||
class Component<T>(private val builder: () -> T) {
|
||||
|
||||
private var instance: T? = null
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package com.agileburo.anytype.di.feature
|
||||
|
||||
import com.agileburo.anytype.core_utils.di.scope.PerScreen
|
||||
import com.agileburo.anytype.domain.page.CheckForUnlink
|
||||
import com.agileburo.anytype.presentation.page.LinkAddViewModelFactory
|
||||
import com.agileburo.anytype.ui.page.modals.LinkFragment
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.Subcomponent
|
||||
|
||||
@Subcomponent(modules = [LinkModule::class])
|
||||
@PerScreen
|
||||
interface LinkSubComponent {
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
fun linkModule(module: LinkModule): Builder
|
||||
fun build(): LinkSubComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: LinkFragment)
|
||||
}
|
||||
|
||||
@Module
|
||||
class LinkModule {
|
||||
|
||||
@PerScreen
|
||||
@Provides
|
||||
fun provideCanUnlink(): CheckForUnlink = CheckForUnlink()
|
||||
|
||||
@PerScreen
|
||||
@Provides
|
||||
fun provideFactory(
|
||||
checkForUnlink: CheckForUnlink
|
||||
): LinkAddViewModelFactory = LinkAddViewModelFactory(
|
||||
unlink = checkForUnlink
|
||||
)
|
||||
}
|
|
@ -41,6 +41,8 @@ class PageModule {
|
|||
interceptEvents: InterceptEvents,
|
||||
updateCheckbox: UpdateCheckbox,
|
||||
unlinkBlocks: UnlinkBlocks,
|
||||
updateLinkMarks: UpdateLinkMarks,
|
||||
removeLinkMark: RemoveLinkMark,
|
||||
duplicateBlock: DuplicateBlock,
|
||||
updateTextStyle: UpdateTextStyle,
|
||||
updateTextColor: UpdateTextColor
|
||||
|
@ -54,7 +56,9 @@ class PageModule {
|
|||
unlinkBlocks = unlinkBlocks,
|
||||
duplicateBlock = duplicateBlock,
|
||||
updateTextStyle = updateTextStyle,
|
||||
updateTextColor = updateTextColor
|
||||
updateTextColor = updateTextColor,
|
||||
updateLinkMarks = updateLinkMarks,
|
||||
removeLinkMark = removeLinkMark
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
@ -130,6 +134,14 @@ class PageModule {
|
|||
repo = repo
|
||||
)
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideUpdateLinkMarks(): UpdateLinkMarks = UpdateLinkMarks()
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideRemoveLinkMark(): RemoveLinkMark = RemoveLinkMark()
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideUpdateTextStyleUseCase(
|
||||
|
|
|
@ -28,4 +28,5 @@ interface MainComponent {
|
|||
fun detailEditBuilder(): DetailEditSubComponent.Builder
|
||||
fun detailsReorderBuilder(): DetailsReorderSubComponent.Builder
|
||||
fun pageComponentBuilder(): PageSubComponent.Builder
|
||||
fun linkAddComponentBuilder(): LinkSubComponent.Builder
|
||||
}
|
|
@ -5,8 +5,8 @@ import android.view.View
|
|||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.agileburo.anytype.ModalsNavFragment
|
||||
import com.agileburo.anytype.ModalsNavFragment.Companion.TAG_CUSTOMIZE
|
||||
import com.agileburo.anytype.ui.database.modals.ModalsNavFragment
|
||||
import com.agileburo.anytype.ui.database.modals.ModalsNavFragment.Companion.TAG_CUSTOMIZE
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_ui.layout.ListDividerItemDecoration
|
||||
import com.agileburo.anytype.core_ui.layout.SpacingItemDecoration
|
||||
|
|
|
@ -5,8 +5,7 @@ import android.view.View
|
|||
import android.widget.TextView
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.agileburo.anytype.ModalNavigation
|
||||
import com.agileburo.anytype.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.ui.database.modals.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_utils.ext.show
|
||||
import com.agileburo.anytype.di.common.componentManager
|
||||
|
|
|
@ -6,8 +6,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.agileburo.anytype.ModalNavigation
|
||||
import com.agileburo.anytype.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.ui.database.modals.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_ui.extensions.drawable
|
||||
import com.agileburo.anytype.core_ui.extensions.invisible
|
||||
|
|
|
@ -7,8 +7,7 @@ import android.view.ViewGroup
|
|||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.agileburo.anytype.ModalNavigation
|
||||
import com.agileburo.anytype.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.ui.database.modals.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_ui.layout.ListDividerItemDecoration
|
||||
import com.agileburo.anytype.core_utils.ui.BaseBottomSheetFragment
|
||||
|
|
|
@ -8,8 +8,7 @@ import androidx.lifecycle.Observer
|
|||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.agileburo.anytype.ModalNavigation
|
||||
import com.agileburo.anytype.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.ui.database.modals.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_ui.extensions.invisible
|
||||
import com.agileburo.anytype.core_ui.extensions.visible
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
package com.agileburo.anytype
|
||||
package com.agileburo.anytype.ui.database.modals
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import com.agileburo.anytype.ui.database.modals.*
|
||||
import com.agileburo.anytype.R
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
|
||||
class ModalsNavFragment : BottomSheetDialogFragment(), ModalNavigation {
|
||||
class ModalsNavFragment : BottomSheetDialogFragment(),
|
||||
ModalNavigation {
|
||||
|
||||
companion object {
|
||||
const val TAG_CUSTOMIZE = "tag.customize"
|
|
@ -5,8 +5,7 @@ import android.view.View
|
|||
import android.widget.ImageView
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.agileburo.anytype.ModalNavigation
|
||||
import com.agileburo.anytype.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.ui.database.modals.ModalsNavFragment.Companion.ARGS_DB_ID
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_utils.ext.hide
|
||||
import com.agileburo.anytype.core_utils.ext.show
|
||||
|
|
|
@ -36,10 +36,13 @@ import com.agileburo.anytype.core_utils.ext.toast
|
|||
import com.agileburo.anytype.di.common.componentManager
|
||||
import com.agileburo.anytype.domain.block.model.Block.Content.Text
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
import com.agileburo.anytype.domain.ext.getFirstLinkMarkupParam
|
||||
import com.agileburo.anytype.domain.ext.getSubstring
|
||||
import com.agileburo.anytype.ext.extractMarks
|
||||
import com.agileburo.anytype.presentation.page.PageViewModel
|
||||
import com.agileburo.anytype.presentation.page.PageViewModelFactory
|
||||
import com.agileburo.anytype.ui.base.NavigationFragment
|
||||
import com.agileburo.anytype.ui.page.modals.LinkFragment
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import kotlinx.android.synthetic.main.fragment_page.*
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -50,7 +53,7 @@ import timber.log.Timber
|
|||
import javax.inject.Inject
|
||||
|
||||
|
||||
class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
||||
class PageFragment : NavigationFragment(R.layout.fragment_page), OnFragmentInteractionListener {
|
||||
|
||||
private val pageAdapter by lazy {
|
||||
BlockAdapter(
|
||||
|
@ -84,7 +87,6 @@ class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
|||
super.onCreate(savedInstanceState)
|
||||
|
||||
vm.open(requireArguments().getString(ID_KEY, ID_EMPTY_VALUE))
|
||||
|
||||
requireActivity()
|
||||
.onBackPressedDispatcher
|
||||
.addCallback(this) {
|
||||
|
@ -238,6 +240,14 @@ class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onAddMarkupLinkClicked(blockId: String, link: String, range: IntRange) {
|
||||
vm.onAddLinkPressed(blockId, link, range)
|
||||
}
|
||||
|
||||
override fun onRemoveMarkupLinkClicked(blockId: String, range: IntRange) {
|
||||
vm.onUnlinkPressed(blockId, range)
|
||||
}
|
||||
|
||||
private fun handleBackgroundColorClicked() {
|
||||
toast(NOT_IMPLEMENTED_MESSAGE)
|
||||
}
|
||||
|
@ -286,6 +296,15 @@ class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
|||
is PageViewModel.ViewState.Success -> {
|
||||
pageAdapter.updateWithDiffUtil(state.blocks)
|
||||
}
|
||||
is PageViewModel.ViewState.OpenLinkScreen -> {
|
||||
LinkFragment.newInstance(
|
||||
blockId = state.block.id,
|
||||
initUrl = state.block.getFirstLinkMarkupParam(state.range),
|
||||
text = state.block.getSubstring(state.range),
|
||||
rangeEnd = state.range.last,
|
||||
rangeStart = state.range.first
|
||||
).show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -370,4 +389,9 @@ class PageFragment : NavigationFragment(R.layout.fragment_page) {
|
|||
const val ID_EMPTY_VALUE = ""
|
||||
const val NOT_IMPLEMENTED_MESSAGE = "Not implemented."
|
||||
}
|
||||
}
|
||||
|
||||
interface OnFragmentInteractionListener {
|
||||
fun onAddMarkupLinkClicked(blockId: String, link: String, range: IntRange)
|
||||
fun onRemoveMarkupLinkClicked(blockId: String, range: IntRange)
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package com.agileburo.anytype.ui.page.modals
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_utils.ui.BaseBottomSheetFragment
|
||||
import com.agileburo.anytype.di.common.componentManager
|
||||
import com.agileburo.anytype.presentation.page.LinkAddViewModel
|
||||
import com.agileburo.anytype.presentation.page.LinkAddViewModelFactory
|
||||
import com.agileburo.anytype.presentation.page.LinkViewState
|
||||
import com.agileburo.anytype.ui.page.OnFragmentInteractionListener
|
||||
import kotlinx.android.synthetic.main.fragment_link.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class LinkFragment : BaseBottomSheetFragment() {
|
||||
|
||||
companion object {
|
||||
const val ARG_URL = "arg.link.url"
|
||||
const val ARG_TEXT = "arg.link.text"
|
||||
const val ARG_RANGE_START = "arg.link.range.start"
|
||||
const val ARG_RANGE_END = "arg.link.range.end"
|
||||
const val ARG_BLOCK_ID = "arg.link.block.id"
|
||||
|
||||
fun newInstance(
|
||||
text: String,
|
||||
initUrl: String?,
|
||||
rangeStart: Int,
|
||||
rangeEnd: Int,
|
||||
blockId: String
|
||||
) =
|
||||
LinkFragment().apply {
|
||||
arguments = bundleOf(
|
||||
ARG_TEXT to text,
|
||||
ARG_URL to initUrl,
|
||||
ARG_RANGE_START to rangeStart,
|
||||
ARG_RANGE_END to rangeEnd,
|
||||
ARG_BLOCK_ID to blockId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var factory: LinkAddViewModelFactory
|
||||
|
||||
private val vm by lazy {
|
||||
ViewModelProviders
|
||||
.of(this, factory)
|
||||
.get(LinkAddViewModel::class.java)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogStyle)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_link, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
vm.state.observe(viewLifecycleOwner, Observer { state -> render(state) })
|
||||
arguments?.let {
|
||||
vm.onViewCreated(
|
||||
text = it.getString(ARG_TEXT, ""),
|
||||
initUrl = it.getString(ARG_URL),
|
||||
range = IntRange(it.getInt(ARG_RANGE_START), it.getInt(ARG_RANGE_END))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun render(state: LinkViewState) {
|
||||
when (state) {
|
||||
is LinkViewState.Init -> {
|
||||
text.text = state.text
|
||||
link.setText(state.url)
|
||||
buttonLink.setOnClickListener {
|
||||
vm.onLinkButtonClicked(link.text.toString())
|
||||
}
|
||||
buttonUnlink.setOnClickListener {
|
||||
vm.onUnlinkButtonClicked()
|
||||
}
|
||||
}
|
||||
is LinkViewState.AddLink -> {
|
||||
(parentFragment as? OnFragmentInteractionListener)?.onAddMarkupLinkClicked(
|
||||
link = state.link,
|
||||
range = state.range,
|
||||
blockId = arguments?.getString(ARG_BLOCK_ID, "").orEmpty()
|
||||
)
|
||||
dismiss()
|
||||
}
|
||||
is LinkViewState.Unlink -> {
|
||||
(parentFragment as? OnFragmentInteractionListener)?.onRemoveMarkupLinkClicked(
|
||||
range = state.range,
|
||||
blockId = arguments?.getString(ARG_BLOCK_ID, "").orEmpty()
|
||||
)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun injectDependencies() {
|
||||
componentManager().linkAddComponent.get().inject(this)
|
||||
}
|
||||
|
||||
override fun releaseDependencies() {
|
||||
componentManager().linkAddComponent.release()
|
||||
}
|
||||
}
|
104
app/src/main/res/layout/fragment_link.xml
Normal file
104
app/src/main/res/layout/fragment_link.xml
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?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="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/modal_rect_margin_top"
|
||||
android:contentDescription="@string/content_description_modal_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/sheet_top" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:textColor="#ACA996"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||
tools:text="receiving notifications because" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="@dimen/modal_main_margin_start"
|
||||
android:layout_marginTop="13dp"
|
||||
android:layout_marginEnd="@dimen/modal_main_margin_end"
|
||||
android:background="@color/divider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/link"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="13dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="@null"
|
||||
android:hint="@string/hint_link"
|
||||
android:inputType="textUri"
|
||||
android:textColor="#ACA996"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider1" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="@dimen/modal_main_margin_start"
|
||||
android:layout_marginTop="13dp"
|
||||
android:layout_marginEnd="@dimen/modal_main_margin_end"
|
||||
android:background="@color/divider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/link" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/buttonUnlink"
|
||||
android:layout_width="@dimen/modal_button_width"
|
||||
android:layout_height="@dimen/modal_button_height"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/rounded_button_cancel"
|
||||
android:text="@string/button_unlink"
|
||||
android:textAllCaps="false"
|
||||
android:fontFamily="@font/graphik_medium"
|
||||
android:textColor="#ACA996"
|
||||
android:stateListAnimator="@animator/scale_shrink"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider2" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/buttonLink"
|
||||
android:layout_width="@dimen/modal_button_width"
|
||||
android:layout_height="@dimen/modal_button_height"
|
||||
android:layout_marginEnd="@dimen/modal_main_margin_end"
|
||||
android:background="@drawable/rounded_button_add"
|
||||
android:fontFamily="@font/graphik_medium"
|
||||
android:stateListAnimator="@animator/scale_shrink"
|
||||
android:text="@string/button_link"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/buttonUnlink" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -5,6 +5,6 @@
|
|||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:context=".ModalsNavFragment">
|
||||
tools:context=".ui.database.modals.ModalsNavFragment">
|
||||
|
||||
</FrameLayout>
|
|
@ -87,5 +87,8 @@ Do the computation of an expensive paragraph of text on a background thread:
|
|||
<string name="create_a_new_profile">Create a new profile</string>
|
||||
<string name="no_peers">No peers</string>
|
||||
<string name="content_description_back_button_icon">Back button icon</string>
|
||||
<string name="hint_link">Paste or type a URL</string>
|
||||
<string name="button_unlink">Unlink</string>
|
||||
<string name="button_link">Link</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -59,4 +59,10 @@
|
|||
<item name="android:background">@drawable/selector_pin_code_keyboard</item>
|
||||
</style>
|
||||
|
||||
<style name="DialogStyle" parent="Theme.Design.Light.BottomSheetDialog">
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowSoftInputMode">adjustResize</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -5,10 +5,8 @@ import android.graphics.Typeface
|
|||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.*
|
||||
import android.text.util.Linkify
|
||||
|
||||
/**
|
||||
* Classes implementing this interface should support markup rendering.
|
||||
|
@ -48,7 +46,8 @@ interface Markup {
|
|||
ITALIC,
|
||||
BOLD,
|
||||
STRIKETHROUGH,
|
||||
TEXT_COLOR
|
||||
TEXT_COLOR,
|
||||
LINK
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,6 +78,12 @@ fun Markup.toSpannable() = SpannableString(body).apply {
|
|||
mark.to,
|
||||
Spannable.SPAN_INCLUSIVE_INCLUSIVE
|
||||
)
|
||||
Markup.Type.LINK -> setSpan(
|
||||
URLSpan(mark.param as String),
|
||||
mark.from,
|
||||
mark.to,
|
||||
Spannable.SPAN_INCLUSIVE_INCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +118,14 @@ fun Editable.setMarkup(markup: Markup) {
|
|||
mark.to,
|
||||
Spannable.SPAN_INCLUSIVE_INCLUSIVE
|
||||
)
|
||||
Markup.Type.LINK -> {
|
||||
setSpan(
|
||||
URLSpan(mark.param as String),
|
||||
mark.from,
|
||||
mark.to,
|
||||
Spannable.SPAN_INCLUSIVE_INCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@ package com.agileburo.anytype.core_ui.widgets.text
|
|||
|
||||
import android.content.Context
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.util.Linkify
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import timber.log.Timber
|
||||
|
@ -20,6 +22,10 @@ class TextInputWidget : AppCompatEditText {
|
|||
defStyle
|
||||
)
|
||||
|
||||
init {
|
||||
makeLinksActive()
|
||||
}
|
||||
|
||||
override fun addTextChangedListener(watcher: TextWatcher) {
|
||||
watchers.add(watcher)
|
||||
super.addTextChangedListener(watcher)
|
||||
|
@ -40,4 +46,12 @@ class TextInputWidget : AppCompatEditText {
|
|||
selectionDetector?.invoke(selStart..selEnd)
|
||||
super.onSelectionChanged(selStart, selEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes all links in the TextView object active.
|
||||
*/
|
||||
private fun makeLinksActive() {
|
||||
this.movementMethod = LinkMovementMethod.getInstance()
|
||||
Linkify.addLinks(this, Linkify.WEB_URLS)
|
||||
}
|
||||
}
|
|
@ -40,10 +40,11 @@ class MarkupToolbarWidget : ConstraintLayout {
|
|||
LayoutInflater.from(context).inflate(R.layout.widget_markup_toolbar, this)
|
||||
}
|
||||
|
||||
fun markupClicks() = flowOf(bold(), italic(), strike()).flattenMerge()
|
||||
fun markupClicks() = flowOf(bold(), italic(), strike(), link()).flattenMerge()
|
||||
private fun bold() = bold.clicks().map { Markup.Type.BOLD }
|
||||
private fun italic() = italic.clicks().map { Markup.Type.ITALIC }
|
||||
private fun strike() = strike.clicks().map { Markup.Type.STRIKETHROUGH }
|
||||
private fun link() = link.clicks().map { Markup.Type.LINK }
|
||||
|
||||
fun colorClicks() = color.clicks()
|
||||
fun hideKeyboardClicks() = keyboard.clicks()
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<corners android:radius="4dp" />
|
||||
<solid android:color="#FFB522" />
|
||||
<stroke
|
||||
android:width="1px"
|
||||
android:width="1dp"
|
||||
android:color="#FFB522" />
|
||||
|
||||
</shape>
|
|
@ -4,7 +4,7 @@
|
|||
<corners android:radius="4dp" />
|
||||
<solid android:color="@color/white" />
|
||||
<stroke
|
||||
android:width="1px"
|
||||
android:width="1dp"
|
||||
android:color="#DFDDD0" />
|
||||
|
||||
</shape>
|
|
@ -10,9 +10,6 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion config["min_sdk"]
|
||||
targetSdkVersion config["target_sdk"]
|
||||
versionCode config["version_code"]
|
||||
versionName config["version_name"]
|
||||
|
||||
testInstrumentationRunner config["test_runner"]
|
||||
}
|
||||
|
||||
|
@ -22,13 +19,13 @@ android {
|
|||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
def applicationDependencies = rootProject.ext.mainApplication
|
||||
def unitTestDependencies = rootProject.ext.unitTesting
|
||||
def acceptanceTesting = rootProject.ext.acceptanceTesting
|
||||
|
||||
kapt applicationDependencies.daggerCompiler
|
||||
|
||||
|
@ -51,4 +48,9 @@ dependencies {
|
|||
|
||||
implementation applicationDependencies.navigation
|
||||
implementation applicationDependencies.navigationUi
|
||||
|
||||
testImplementation unitTestDependencies.robolectric
|
||||
androidTestImplementation acceptanceTesting.androidJUnit
|
||||
androidTestImplementation acceptanceTesting.testRules
|
||||
testImplementation unitTestDependencies.archCoreTesting
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.res.Resources
|
|||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.text.Spanned
|
||||
import android.view.TouchDelegate
|
||||
import android.view.View
|
||||
import timber.log.Timber
|
||||
|
@ -73,4 +74,9 @@ private fun expandViewHitArea(parent: View, child: View) {
|
|||
childRect.bottom = parentRect.height()
|
||||
parent.touchDelegate = TouchDelegate(childRect, child)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> hasSpan(spanned: Spanned, clazz: Class<T>): Boolean {
|
||||
val limit = spanned.length
|
||||
return spanned.nextSpanTransition(0, limit, clazz) < limit
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.agileburo.anytype
|
||||
|
||||
import android.graphics.Color
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import com.agileburo.anytype.core_utils.ext.hasSpan
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(manifest = Config.NONE)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class SpanExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `should find spans`() {
|
||||
val text = "Testing Spans"
|
||||
val spannable = SpannableString(text)
|
||||
spannable.setSpan(
|
||||
URLSpan("https://anytype.io/"),
|
||||
0,
|
||||
text.lastIndex,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
spannable.setSpan(
|
||||
BackgroundColorSpan(Color.RED),
|
||||
0,
|
||||
6,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
|
||||
assertTrue(hasSpan(spannable, URLSpan::class.java))
|
||||
assertTrue(hasSpan(spannable, BackgroundColorSpan::class.java))
|
||||
assertFalse(hasSpan(spannable, UnderlineSpan::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not find spans`() {
|
||||
val text = "Testing Spans"
|
||||
val spannable = SpannableString(text)
|
||||
|
||||
assertFalse(hasSpan(spannable, URLSpan::class.java))
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ ext {
|
|||
rxbinding_version = '3.0.0'
|
||||
|
||||
// Unit Testing
|
||||
robolectric_version = '4.3.1'
|
||||
robolectric_version = '4.3'
|
||||
junit_version = '4.12'
|
||||
mockito_version = '1.4.0'
|
||||
kluent_version = '1.14'
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package com.agileburo.anytype.domain.block.interactor
|
||||
|
||||
import com.agileburo.anytype.domain.base.BaseUseCase
|
||||
import com.agileburo.anytype.domain.base.Either
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.domain.ext.rangeIntersection
|
||||
|
||||
/**
|
||||
* Remove all link marks from list with intersected ranges
|
||||
* with given range.
|
||||
*/
|
||||
|
||||
class RemoveLinkMark : BaseUseCase<List<Block.Content.Text.Mark>, RemoveLinkMark.Params>() {
|
||||
|
||||
override suspend fun run(params: Params): Either<Throwable, List<Block.Content.Text.Mark>> =
|
||||
try {
|
||||
val result = mutableListOf<Block.Content.Text.Mark>()
|
||||
params.marks.forEach {
|
||||
if (it.type != Block.Content.Text.Mark.Type.LINK) {
|
||||
result.add(it)
|
||||
} else {
|
||||
if (it.rangeIntersection(params.range) == 0) {
|
||||
result.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
Either.Right(result.toList())
|
||||
} catch (t: Throwable) {
|
||||
Either.Left(t)
|
||||
}
|
||||
|
||||
/**
|
||||
* @property marks Collection to remove link marks
|
||||
* @property range Given range to find intersections
|
||||
*/
|
||||
class Params(
|
||||
val marks: List<Block.Content.Text.Mark>,
|
||||
val range: IntRange
|
||||
)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package com.agileburo.anytype.domain.block.interactor
|
||||
|
||||
import com.agileburo.anytype.domain.base.BaseUseCase
|
||||
import com.agileburo.anytype.domain.base.Either
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.domain.ext.rangeIntersection
|
||||
|
||||
/**
|
||||
* Adds new link mark to the list of marks and
|
||||
* remove all link marks with intersected ranges
|
||||
* with new mark.
|
||||
*/
|
||||
|
||||
class UpdateLinkMarks : BaseUseCase<List<Block.Content.Text.Mark>, UpdateLinkMarks.Params>() {
|
||||
|
||||
override suspend fun run(params: Params): Either<Throwable, List<Block.Content.Text.Mark>> =
|
||||
try {
|
||||
val result = mutableListOf<Block.Content.Text.Mark>()
|
||||
params.marks.forEach {
|
||||
if (it.type != Block.Content.Text.Mark.Type.LINK) {
|
||||
result.add(it)
|
||||
} else {
|
||||
if (it.rangeIntersection(params.newMark.range) == 0) {
|
||||
result.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
result.add(params.newMark)
|
||||
Either.Right(result.toList())
|
||||
} catch (t: Throwable) {
|
||||
Either.Left(t)
|
||||
}
|
||||
|
||||
/**
|
||||
* @property marks Collection of marks to update
|
||||
* @property newMark Link mark to add
|
||||
*/
|
||||
class Params(
|
||||
val marks: List<Block.Content.Text.Mark>,
|
||||
val newMark: Block.Content.Text.Mark
|
||||
)
|
||||
}
|
|
@ -42,6 +42,21 @@ fun Block.textStyle(): Block.Content.Text.Style {
|
|||
throw UnsupportedOperationException("Wrong block content type: ${content.javaClass}")
|
||||
}
|
||||
|
||||
fun Block.Content.Text.Mark.rangeIntersection(range: IntRange): Int {
|
||||
val markRange = IntRange(start = this.range.first, endInclusive = this.range.last)
|
||||
val set = markRange.intersect(range)
|
||||
return set.size
|
||||
}
|
||||
|
||||
fun Block.getFirstLinkMarkupParam(range: IntRange): String? {
|
||||
val marks = this.content.asText().marks
|
||||
return marks.filter { mark -> mark.type == Block.Content.Text.Mark.Type.LINK }
|
||||
.firstOrNull { mark: Block.Content.Text.Mark ->
|
||||
mark.rangeIntersection(range) > 0
|
||||
}.let { mark: Block.Content.Text.Mark? -> mark?.param as? String }
|
||||
}
|
||||
|
||||
fun Block.getSubstring(range: IntRange): String = content.asText().text.substring(range)
|
||||
fun Block.textColor(): String? {
|
||||
if (content is Block.Content.Text)
|
||||
return content.color
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.agileburo.anytype.domain.page
|
||||
|
||||
import com.agileburo.anytype.domain.base.BaseUseCase
|
||||
import com.agileburo.anytype.domain.base.Either
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 2020-01-23.
|
||||
*/
|
||||
/**
|
||||
* Use-case for unlinking urls from text.
|
||||
*/
|
||||
|
||||
class CheckForUnlink : BaseUseCase<Boolean, CheckForUnlink.Params>() {
|
||||
|
||||
override suspend fun run(params: Params): Either<Throwable, Boolean> = try {
|
||||
if (params.link.isNullOrEmpty()) {
|
||||
Either.Left(NothingToUnlinkException())
|
||||
} else {
|
||||
Either.Right(true)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Either.Left(e)
|
||||
}
|
||||
|
||||
class Params(val link: String?)
|
||||
}
|
||||
|
||||
class NothingToUnlinkException : Exception("No text to unlink")
|
|
@ -0,0 +1,128 @@
|
|||
package com.agileburo.anytype.domain.block.interactor
|
||||
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class RemoveLinkMarkTest {
|
||||
|
||||
lateinit var removeLinkMark: RemoveLinkMark
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
removeLinkMark = RemoveLinkMark()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should remove link mark with range`() {
|
||||
runBlocking {
|
||||
val marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
param = "wwww.yahoo.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
param = "wwww.google.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
param = "wwww.yandex.ru",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(21, 56),
|
||||
param = "wwww.allmusic.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val range = IntRange(6, 16)
|
||||
|
||||
val expected = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
param = "wwww.yahoo.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
param = "wwww.yandex.ru",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(21, 56),
|
||||
param = "wwww.allmusic.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val result = removeLinkMark.run(params = RemoveLinkMark.Params(marks, range))
|
||||
|
||||
result.either(
|
||||
{ fail() },
|
||||
{ kotlin.test.assertEquals(expected, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not remove link mark`() {
|
||||
runBlocking {
|
||||
val marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
param = "wwww.yahoo.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
param = "wwww.google.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(110, 220),
|
||||
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
)
|
||||
)
|
||||
|
||||
val range = IntRange(110, 220)
|
||||
|
||||
val expected = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
param = "wwww.yahoo.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
param = "wwww.google.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(110, 220),
|
||||
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
)
|
||||
)
|
||||
|
||||
val result = removeLinkMark.run(params = RemoveLinkMark.Params(marks, range))
|
||||
|
||||
result.either(
|
||||
{ fail() },
|
||||
{ kotlin.test.assertEquals(expected, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
package com.agileburo.anytype.domain.block.interactor
|
||||
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class UpdateLinkMarksTest {
|
||||
|
||||
lateinit var updateLinkMarks: UpdateLinkMarks
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
updateLinkMarks = UpdateLinkMarks()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return updated list without link marks with intersections`() {
|
||||
runBlocking {
|
||||
val marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
param = "wwww.yahoo.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
param = "wwww.google.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
param = "wwww.yandex.ru",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(21, 56),
|
||||
param = "wwww.allmusic.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val newMark = Block.Content.Text.Mark(
|
||||
range = IntRange(4, 20),
|
||||
param = "wwww.anytype.io",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(21, 56),
|
||||
param = "wwww.allmusic.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(4, 20),
|
||||
param = "wwww.anytype.io",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val result = updateLinkMarks.run(params = UpdateLinkMarks.Params(marks, newMark))
|
||||
|
||||
result.either(
|
||||
{ Assert.fail() },
|
||||
{ assertEquals(expected, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return updated list of marks, when no intersections`() {
|
||||
runBlocking {
|
||||
val marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
param = "wwww.yahoo.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
param = "wwww.google.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
param = "wwww.yandex.ru",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val newMark = Block.Content.Text.Mark(
|
||||
range = IntRange(25, 35),
|
||||
param = "wwww.anytype.io",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
param = "wwww.yahoo.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
param = "wwww.google.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
param = "wwww.yandex.ru",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(25, 35),
|
||||
param = "wwww.anytype.io",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val result = updateLinkMarks.run(params = UpdateLinkMarks.Params(marks, newMark))
|
||||
|
||||
result.either(
|
||||
{ Assert.fail() },
|
||||
{ assertEquals(expected, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return updated list of marks, when no link marks and no intersections`() {
|
||||
runBlocking {
|
||||
val marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
)
|
||||
)
|
||||
|
||||
val newMark = Block.Content.Text.Mark(
|
||||
range = IntRange(25, 35),
|
||||
param = "wwww.anytype.io",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(25, 35),
|
||||
param = "wwww.anytype.io",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val result = updateLinkMarks.run(params = UpdateLinkMarks.Params(marks, newMark))
|
||||
|
||||
result.either(
|
||||
{ Assert.fail() },
|
||||
{ assertEquals(expected, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return updated list of marks, when no link marks`() {
|
||||
runBlocking {
|
||||
val marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
)
|
||||
)
|
||||
|
||||
val newMark = Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
param = "wwww.anytype.io",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
param = "wwww.anytype.io",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val result = updateLinkMarks.run(params = UpdateLinkMarks.Params(marks, newMark))
|
||||
|
||||
result.either(
|
||||
{ Assert.fail() },
|
||||
{ assertEquals(expected, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return updated list when new mark param is empty`() {
|
||||
runBlocking {
|
||||
val marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(0, 5),
|
||||
param = "wwww.yahoo.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(6, 16),
|
||||
param = "wwww.google.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(18, 24),
|
||||
param = "wwww.yandex.ru",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(21, 56),
|
||||
param = "wwww.allmusic.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val newMark = Block.Content.Text.Mark(
|
||||
range = IntRange(4, 20),
|
||||
param = "",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(21, 56),
|
||||
param = "wwww.allmusic.com",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(4, 20),
|
||||
param = "",
|
||||
type = Block.Content.Text.Mark.Type.LINK
|
||||
)
|
||||
)
|
||||
|
||||
val result = updateLinkMarks.run(params = UpdateLinkMarks.Params(marks, newMark))
|
||||
|
||||
result.either(
|
||||
{ Assert.fail() },
|
||||
{ assertEquals(expected, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,9 @@ import com.agileburo.anytype.domain.block.model.Block
|
|||
import com.agileburo.anytype.domain.common.MockDataFactory
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertSame
|
||||
|
||||
class BlockExtensionTest {
|
||||
|
||||
|
@ -441,4 +444,287 @@ class BlockExtensionTest {
|
|||
actual = rendering[4]
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return substring of block text by range`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Text(
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P,
|
||||
text = "Test block 123"
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
|
||||
val range = IntRange(5, 12)
|
||||
|
||||
val result = block.getSubstring(range)
|
||||
|
||||
assertEquals("block 12", result)
|
||||
}
|
||||
|
||||
@Test(expected = IndexOutOfBoundsException::class)
|
||||
fun `should return error when range is out of bounds`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Text(
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P,
|
||||
text = "Test"
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(0, 4)
|
||||
|
||||
block.getSubstring(range)
|
||||
}
|
||||
|
||||
@Test(expected = ClassCastException::class)
|
||||
fun `should return error when block not text`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Page(
|
||||
style = Block.Content.Page.Style.EMPTY
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(0, 44)
|
||||
|
||||
block.getSubstring(range)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return not empty range intersection`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Text(
|
||||
marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 5,
|
||||
endInclusive = 8
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
)
|
||||
),
|
||||
style = Block.Content.Text.Style.P,
|
||||
text = "Test Bold text"
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(7, 144)
|
||||
|
||||
val result = block.content.asText().marks[0].rangeIntersection(range)
|
||||
|
||||
assertEquals(2, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return empty range intersection`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Text(
|
||||
marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 5,
|
||||
endInclusive = 8
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
)
|
||||
),
|
||||
style = Block.Content.Text.Style.P,
|
||||
text = "Test Bold text"
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(0, 4)
|
||||
|
||||
val result = block.content.asText().marks[0].rangeIntersection(range)
|
||||
|
||||
assertEquals(0, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return link markup`() {
|
||||
val link = Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 10,
|
||||
endInclusive = 13
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.LINK,
|
||||
param = "www.anytype.io/test"
|
||||
)
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Text(
|
||||
marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 5,
|
||||
endInclusive = 8
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 10,
|
||||
endInclusive = 13
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
),
|
||||
link
|
||||
),
|
||||
style = Block.Content.Text.Style.P,
|
||||
text = "Test Bold text"
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(10, 13)
|
||||
|
||||
val result = block.getFirstLinkMarkupParam(range)
|
||||
|
||||
assertEquals(link.param, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return nullable markup`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Text(
|
||||
marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 5,
|
||||
endInclusive = 8
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 10,
|
||||
endInclusive = 13
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
)
|
||||
),
|
||||
style = Block.Content.Text.Style.P,
|
||||
text = "Test Bold text"
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(10, 13)
|
||||
|
||||
val result = block.getFirstLinkMarkupParam(range)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return first link markup`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Text(
|
||||
marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 0,
|
||||
endInclusive = 8
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.LINK,
|
||||
param = "https://kotlinlang.ru"
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 0,
|
||||
endInclusive = 8
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.LINK,
|
||||
param = "https://ya.ru/"
|
||||
)
|
||||
),
|
||||
style = Block.Content.Text.Style.P,
|
||||
text = "Test Bold text"
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(0, 8)
|
||||
|
||||
val result = block.getFirstLinkMarkupParam(range)
|
||||
|
||||
assertEquals("https://kotlinlang.ru", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return nullable markup when no marks in block`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Text(
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P,
|
||||
text = "Test Bold text"
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(10, 13)
|
||||
|
||||
val result = block.getFirstLinkMarkupParam(range)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return nullable markup when link param not string`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Text(
|
||||
marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 0,
|
||||
endInclusive = 8
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.LINK,
|
||||
param = 45678
|
||||
)
|
||||
),
|
||||
style = Block.Content.Text.Style.P,
|
||||
text = "Test Bold text"
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(0, 8)
|
||||
|
||||
val result = block.getFirstLinkMarkupParam(range)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test(expected = ClassCastException::class)
|
||||
fun `should throw exception when block is not text`() {
|
||||
val block = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Dashboard(
|
||||
type = Block.Content.Dashboard.Type.MAIN_SCREEN
|
||||
),
|
||||
children = emptyList()
|
||||
)
|
||||
val range = IntRange(10, 13)
|
||||
|
||||
val result = block.getFirstLinkMarkupParam(range)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package com.agileburo.anytype.domain.page
|
||||
|
||||
import com.agileburo.anytype.domain.common.CoroutineTestRule
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
|
||||
class UnlinkTextTest {
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@get:Rule
|
||||
var rule = CoroutineTestRule()
|
||||
|
||||
lateinit var checkForUnlink: CheckForUnlink
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
checkForUnlink = CheckForUnlink()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return NothingToUnlinkException when url is empty`() {
|
||||
runBlocking {
|
||||
val params = CheckForUnlink.Params(link = "")
|
||||
|
||||
val result = checkForUnlink.run(params)
|
||||
|
||||
result.either(
|
||||
{ throwable ->
|
||||
assertEquals("No text to unlink", throwable.localizedMessage)
|
||||
},
|
||||
{ b: Boolean ->
|
||||
Assert.fail()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return NothingToUnlinkException when url is null`() {
|
||||
runBlocking {
|
||||
val params = CheckForUnlink.Params( link = null)
|
||||
|
||||
val result = checkForUnlink.run(params)
|
||||
|
||||
result.either(
|
||||
{ throwable ->
|
||||
assertEquals("No text to unlink", throwable.localizedMessage)
|
||||
},
|
||||
{ b: Boolean ->
|
||||
Assert.fail()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -80,6 +80,14 @@ fun BlockEntity.Content.Text.Mark.toMiddleware(): Block.Content.Text.Mark {
|
|||
.setParam(param as String)
|
||||
.build()
|
||||
}
|
||||
BlockEntity.Content.Text.Mark.Type.LINK -> {
|
||||
Block.Content.Text.Mark
|
||||
.newBuilder()
|
||||
.setType(Block.Content.Text.Mark.Type.Link)
|
||||
.setRange(rangeModel)
|
||||
.setParam(param as String)
|
||||
.build()
|
||||
}
|
||||
else -> throw IllegalStateException("Unsupported mark type: ${type.name}")
|
||||
}
|
||||
}
|
||||
|
@ -149,6 +157,9 @@ fun Block.text(): BlockEntity.Content.Text = BlockEntity.Content.Text(
|
|||
Block.Content.Text.Mark.Type.BackgroundColor -> {
|
||||
BlockEntity.Content.Text.Mark.Type.BACKGROUND_COLOR
|
||||
}
|
||||
Block.Content.Text.Mark.Type.Link -> {
|
||||
BlockEntity.Content.Text.Mark.Type.LINK
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected mark type: ${mark.type.name}")
|
||||
}
|
||||
)
|
||||
|
|
|
@ -429,6 +429,9 @@ class BlockMiddleware(
|
|||
Models.Block.Content.Text.Mark.Type.BackgroundColor -> {
|
||||
BlockEntity.Content.Text.Mark.Type.BACKGROUND_COLOR
|
||||
}
|
||||
Models.Block.Content.Text.Mark.Type.Link -> {
|
||||
BlockEntity.Content.Text.Mark.Type.LINK
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected mark type: ${mark.type.name}")
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
|
||||
|
@ -14,6 +15,10 @@ android {
|
|||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
@ -112,6 +112,14 @@ private fun mapMarks(content: Block.Content.Text): List<Markup.Mark> =
|
|||
param = checkNotNull(mark.param)
|
||||
)
|
||||
}
|
||||
Block.Content.Text.Mark.Type.LINK -> {
|
||||
Markup.Mark(
|
||||
from = mark.range.first,
|
||||
to = mark.range.last,
|
||||
type = Markup.Type.LINK,
|
||||
param = checkNotNull(mark.param)
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,11 +49,11 @@ interface AppNavigation {
|
|||
object StartDesktopFromLogin : Command()
|
||||
object StartSplashFromDesktop : Command()
|
||||
object OpenContactsScreen : Command()
|
||||
object OpenDatabaseViewAddView: Command()
|
||||
object OpenEditDatabase: Command()
|
||||
object OpenSwitchDisplayView: Command()
|
||||
object OpenCustomizeDisplayView: Command()
|
||||
object OpenKanbanScreen: Command()
|
||||
object OpenDatabaseViewAddView : Command()
|
||||
object OpenEditDatabase : Command()
|
||||
object OpenSwitchDisplayView : Command()
|
||||
object OpenCustomizeDisplayView : Command()
|
||||
object OpenKanbanScreen : Command()
|
||||
object OpenGoalsScreen : Command()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package com.agileburo.anytype.presentation.page
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import com.agileburo.anytype.domain.page.CheckForUnlink
|
||||
import timber.log.Timber
|
||||
|
||||
sealed class LinkViewState {
|
||||
|
||||
data class Init(val text: String, val url: String?) : LinkViewState()
|
||||
data class AddLink(val link: String, val range: IntRange) : LinkViewState()
|
||||
data class Unlink(val range: IntRange) : LinkViewState()
|
||||
}
|
||||
|
||||
class LinkAddViewModel(
|
||||
private val checkForUnlink: CheckForUnlink
|
||||
) : ViewModel() {
|
||||
|
||||
lateinit var range: IntRange
|
||||
private var initUrl: String? = null
|
||||
private val stateData = MutableLiveData<LinkViewState>()
|
||||
val state: LiveData<LinkViewState> = stateData
|
||||
|
||||
fun onViewCreated(initUrl: String?, text: String, range: IntRange) {
|
||||
this.range = range
|
||||
this.initUrl = initUrl
|
||||
stateData.value = LinkViewState.Init(
|
||||
text = text,
|
||||
url = initUrl
|
||||
)
|
||||
}
|
||||
|
||||
fun onLinkButtonClicked(text: String) {
|
||||
if (text.isNotEmpty()) {
|
||||
stateData.value = LinkViewState.AddLink(link = text, range = range)
|
||||
}
|
||||
}
|
||||
|
||||
fun onUnlinkButtonClicked() =
|
||||
checkForUnlink.invoke(viewModelScope, CheckForUnlink.Params(link = initUrl)) { result ->
|
||||
result.either(
|
||||
fnL = { Timber.e("Can't proceed to unlink:${it.message}") },
|
||||
fnR = { stateData.postValue(LinkViewState.Unlink(range = range)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class LinkAddViewModelFactory(
|
||||
private val unlink: CheckForUnlink
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return LinkAddViewModel(unlink) as T
|
||||
}
|
||||
}
|
|
@ -42,7 +42,9 @@ class PageViewModel(
|
|||
private val unlinkBlocks: UnlinkBlocks,
|
||||
private val duplicateBlock: DuplicateBlock,
|
||||
private val updateTextStyle: UpdateTextStyle,
|
||||
private val updateTextColor: UpdateTextColor
|
||||
private val updateTextColor: UpdateTextColor,
|
||||
private val updateLinkMarks: UpdateLinkMarks,
|
||||
private val removeLinkMark: RemoveLinkMark
|
||||
) : ViewStateViewModel<PageViewModel.ViewState>(),
|
||||
SupportNavigation<EventWrapper<AppNavigation.Command>> {
|
||||
|
||||
|
@ -162,11 +164,61 @@ class PageViewModel(
|
|||
.filter { (_, selection) -> selection.first != selection.last }
|
||||
) { a, b -> Pair(a, b) }
|
||||
.onEach { (action, selection) ->
|
||||
applyMarkup(selection, action)
|
||||
|
||||
when (action.type) {
|
||||
Markup.Type.LINK -> {
|
||||
val block = blocks.first { it.id == selection.first }
|
||||
val range = IntRange(
|
||||
start = selection.second.first,
|
||||
endInclusive = selection.second.last.dec()
|
||||
)
|
||||
stateData.value = ViewState.OpenLinkScreen(pageId, block, range)
|
||||
}
|
||||
else -> {
|
||||
applyMarkup(selection, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun applyLinkMarkup(
|
||||
blockId: String, link: String, range: IntRange
|
||||
) {
|
||||
val targetBlock = blocks.first { it.id == blockId }
|
||||
val targetContent = targetBlock.content as Block.Content.Text
|
||||
val linkMark = Block.Content.Text.Mark(
|
||||
type = Block.Content.Text.Mark.Type.LINK,
|
||||
range = IntRange(start = range.first, endInclusive = range.last.inc()),
|
||||
param = link
|
||||
)
|
||||
val marks = targetContent.marks
|
||||
|
||||
updateLinkMarks.invoke(
|
||||
viewModelScope,
|
||||
UpdateLinkMarks.Params(marks = marks, newMark = linkMark)
|
||||
) { result ->
|
||||
result.either(
|
||||
fnL = {
|
||||
throwable -> Timber.e("Error update marks:${throwable.message}")
|
||||
},
|
||||
fnR = {
|
||||
val newContent = targetContent.copy(marks = it)
|
||||
val newBlock = targetBlock.copy(content = newContent)
|
||||
rerenderingBlocks(newBlock)
|
||||
proceedWithUpdatingBlock(
|
||||
params = UpdateBlock.Params(
|
||||
contextId = pageId,
|
||||
text = newBlock.content.asText().text,
|
||||
blockId = targetBlock.id,
|
||||
marks = it
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun applyMarkup(
|
||||
selection: Pair<String, IntRange>,
|
||||
action: MarkupAction
|
||||
|
@ -181,6 +233,7 @@ class PageViewModel(
|
|||
Markup.Type.ITALIC -> Block.Content.Text.Mark.Type.ITALIC
|
||||
Markup.Type.STRIKETHROUGH -> Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
Markup.Type.TEXT_COLOR -> Block.Content.Text.Mark.Type.TEXT_COLOR
|
||||
Markup.Type.LINK -> Block.Content.Text.Mark.Type.LINK
|
||||
},
|
||||
param = action.param
|
||||
)
|
||||
|
@ -214,6 +267,18 @@ class PageViewModel(
|
|||
)
|
||||
}
|
||||
|
||||
private fun rerenderingBlocks(block: Block) =
|
||||
viewModelScope.launch {
|
||||
val update = blocks.map {
|
||||
if (it.id != block.id)
|
||||
it
|
||||
else
|
||||
block
|
||||
}
|
||||
blocks = update
|
||||
renderingChannel.send(blocks)
|
||||
}
|
||||
|
||||
private fun processRendering() {
|
||||
viewModelScope.launch {
|
||||
renderings.withLatestFrom(focusChanges) { models, focus ->
|
||||
|
@ -288,6 +353,37 @@ class PageViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onAddLinkPressed(blockId: String, link: String, range: IntRange) {
|
||||
applyLinkMarkup(blockId, link, range)
|
||||
}
|
||||
|
||||
fun onUnlinkPressed(blockId: String, range: IntRange) {
|
||||
val targetBlock = blocks.first { it.id == blockId }
|
||||
val targetContent = targetBlock.content as Block.Content.Text
|
||||
val marks = targetContent.marks
|
||||
|
||||
removeLinkMark.invoke(
|
||||
viewModelScope, RemoveLinkMark.Params(range = range, marks = marks)
|
||||
) { result ->
|
||||
result.either(
|
||||
fnL = { Timber.e("Error update marks:${it.message}") },
|
||||
fnR = {
|
||||
val newContent = targetContent.copy(marks = it)
|
||||
val newBlock = targetBlock.copy(content = newContent)
|
||||
rerenderingBlocks(newBlock)
|
||||
proceedWithUpdatingBlock(
|
||||
params = UpdateBlock.Params(
|
||||
contextId = pageId,
|
||||
text = newBlock.content.asText().text,
|
||||
blockId = targetBlock.id,
|
||||
marks = it
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSystemBackPressed() {
|
||||
closePage.invoke(viewModelScope, ClosePage.Params(pageId)) { result ->
|
||||
result.either(
|
||||
|
@ -542,6 +638,8 @@ class PageViewModel(
|
|||
object Loading : ViewState()
|
||||
data class Success(val blocks: List<BlockView>) : ViewState()
|
||||
data class Error(val message: String) : ViewState()
|
||||
data class OpenLinkScreen(val pageId: String, val block: Block, val range: IntRange) :
|
||||
ViewState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -17,7 +17,9 @@ class PageViewModelFactory(
|
|||
private val unlinkBlocks: UnlinkBlocks,
|
||||
private val duplicateBlock: DuplicateBlock,
|
||||
private val updateTextStyle: UpdateTextStyle,
|
||||
private val updateTextColor: UpdateTextColor
|
||||
private val updateTextColor: UpdateTextColor,
|
||||
private val updateLinkMarks: UpdateLinkMarks,
|
||||
private val removeLinkMark: RemoveLinkMark
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -32,7 +34,9 @@ class PageViewModelFactory(
|
|||
duplicateBlock = duplicateBlock,
|
||||
unlinkBlocks = unlinkBlocks,
|
||||
updateTextStyle = updateTextStyle,
|
||||
updateTextColor = updateTextColor
|
||||
updateTextColor = updateTextColor,
|
||||
updateLinkMarks = updateLinkMarks,
|
||||
removeLinkMark = removeLinkMark
|
||||
) as T
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue