mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
Update coroutines library (#1171)
* update lib * ci * fixes * Update workflow.yml
This commit is contained in:
parent
3a5eb40665
commit
bd87851956
143 changed files with 103 additions and 11824 deletions
|
@ -29,7 +29,7 @@ dependencies {
|
|||
def app = rootProject.ext.mainApplication
|
||||
def analytics = rootProject.ext.analytics
|
||||
implementation app.kotlin
|
||||
implementation app.coroutines
|
||||
implementation app.coroutinesAndroid
|
||||
implementation app.timber
|
||||
implementation analytics.amplitude
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ dependencies {
|
|||
|
||||
//Application dependencies
|
||||
implementation applicationDependencies.kotlin
|
||||
implementation applicationDependencies.coroutines
|
||||
implementation applicationDependencies.coroutinesAndroid
|
||||
implementation applicationDependencies.fragment
|
||||
implementation applicationDependencies.navigation
|
||||
implementation applicationDependencies.navigationUi
|
||||
|
@ -118,7 +118,6 @@ dependencies {
|
|||
implementation applicationDependencies.dagger
|
||||
implementation applicationDependencies.timber
|
||||
implementation applicationDependencies.gson
|
||||
implementation applicationDependencies.rxRelay
|
||||
implementation applicationDependencies.tableView
|
||||
implementation applicationDependencies.permissionDisp
|
||||
implementation applicationDependencies.pickT
|
||||
|
|
|
@ -2,30 +2,19 @@ package com.anytypeio.anytype.ui.database.kanban
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_ui.layout.SpacingItemDecoration
|
||||
import com.anytypeio.anytype.core_utils.ext.disposedBy
|
||||
import com.anytypeio.anytype.core_utils.ext.px
|
||||
import com.anytypeio.anytype.core_utils.ext.toast
|
||||
import com.anytypeio.anytype.core_utils.ui.ViewState
|
||||
import com.anytypeio.anytype.presentation.databaseview.KanbanBoardViewModel
|
||||
import com.anytypeio.anytype.ui.database.kanban.adapter.KanbanColumnAdapter
|
||||
import com.anytypeio.anytype.ui.database.kanban.helpers.KanbanBoardCallback
|
||||
import com.anytypeio.anytype.ui.database.kanban.helpers.KanbanBoardListener
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.fragment_kanban.*
|
||||
|
||||
const val COLUMN_WIDTH = 1.46 // На эту величину делим ширину экрана
|
||||
|
||||
class KanbanBoardFragment : Fragment(R.layout.fragment_kanban) {
|
||||
|
||||
private val subscriptions by lazy { CompositeDisposable() }
|
||||
|
||||
private val vm by lazy {
|
||||
ViewModelProviders.of(this).get(KanbanBoardViewModel::class.java)
|
||||
}
|
||||
|
@ -59,64 +48,59 @@ class KanbanBoardFragment : Fragment(R.layout.fragment_kanban) {
|
|||
}
|
||||
|
||||
private fun startObservingViewModel() {
|
||||
vm.observeKanbanBoard().subscribe { state ->
|
||||
when (state) {
|
||||
is ViewState.Success -> {
|
||||
state.data.forEachIndexed { index, kanbanColumnView ->
|
||||
val header =
|
||||
View.inflate(
|
||||
requireContext(), R.layout.item_kanban_column_header, null
|
||||
)
|
||||
.apply {
|
||||
findViewById<TextView>(R.id.columnName).apply {
|
||||
text = kanbanColumnView.name
|
||||
(layoutParams as ConstraintLayout.LayoutParams).run {
|
||||
if (index != 0) this.marginStart = 10.px
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
var startSpace = 0
|
||||
val space = 10.px
|
||||
if (index != 0) {
|
||||
startSpace = 10.px
|
||||
}
|
||||
|
||||
mBoardView.addColumn(
|
||||
KanbanColumnAdapter(
|
||||
kanbanColumnView.rows
|
||||
),
|
||||
header,
|
||||
header,
|
||||
true,
|
||||
LinearLayoutManager(requireContext())
|
||||
)
|
||||
.apply {
|
||||
addItemDecoration(
|
||||
SpacingItemDecoration(
|
||||
spacingStart = startSpace,
|
||||
spacingTop = space,
|
||||
spacingEnd = space,
|
||||
spacingBottom = space
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ViewState.Error -> {
|
||||
requireActivity().toast(state.error)
|
||||
}
|
||||
|
||||
is ViewState.Loading -> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
}.disposedBy(subscriptions)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
subscriptions.clear()
|
||||
// vm.observeKanbanBoard().subscribe { state ->
|
||||
// when (state) {
|
||||
// is ViewState.Success -> {
|
||||
// state.data.forEachIndexed { index, kanbanColumnView ->
|
||||
// val header =
|
||||
// View.inflate(
|
||||
// requireContext(), R.layout.item_kanban_column_header, null
|
||||
// )
|
||||
// .apply {
|
||||
// findViewById<TextView>(R.id.columnName).apply {
|
||||
// text = kanbanColumnView.name
|
||||
// (layoutParams as ConstraintLayout.LayoutParams).run {
|
||||
// if (index != 0) this.marginStart = 10.px
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// var startSpace = 0
|
||||
// val space = 10.px
|
||||
// if (index != 0) {
|
||||
// startSpace = 10.px
|
||||
// }
|
||||
//
|
||||
// mBoardView.addColumn(
|
||||
// KanbanColumnAdapter(
|
||||
// kanbanColumnView.rows
|
||||
// ),
|
||||
// header,
|
||||
// header,
|
||||
// true,
|
||||
// LinearLayoutManager(requireContext())
|
||||
// )
|
||||
// .apply {
|
||||
// addItemDecoration(
|
||||
// SpacingItemDecoration(
|
||||
// spacingStart = startSpace,
|
||||
// spacingTop = space,
|
||||
// spacingEnd = space,
|
||||
// spacingBottom = space
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// is ViewState.Error -> {
|
||||
// requireActivity().toast(state.error)
|
||||
// }
|
||||
//
|
||||
// is ViewState.Loading -> {
|
||||
// TODO()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ dependencies {
|
|||
implementation project(':data')
|
||||
|
||||
implementation applicationDependencies.kotlin
|
||||
implementation applicationDependencies.coroutines
|
||||
implementation applicationDependencies.coroutinesAndroid
|
||||
implementation applicationDependencies.timber
|
||||
|
||||
testImplementation unitTestDependencies.junit
|
||||
|
|
|
@ -50,7 +50,7 @@ dependencies {
|
|||
|
||||
implementation applicationDependencies.appcompat
|
||||
implementation applicationDependencies.kotlin
|
||||
implementation applicationDependencies.coroutines
|
||||
implementation applicationDependencies.coroutinesAndroid
|
||||
implementation applicationDependencies.androidxCore
|
||||
|
||||
implementation applicationDependencies.design
|
||||
|
|
|
@ -32,7 +32,7 @@ dependencies {
|
|||
implementation applicationDependencies.appcompat
|
||||
|
||||
implementation applicationDependencies.kotlin
|
||||
implementation applicationDependencies.coroutines
|
||||
implementation applicationDependencies.coroutinesAndroid
|
||||
implementation applicationDependencies.dagger
|
||||
|
||||
implementation applicationDependencies.timber
|
||||
|
@ -40,9 +40,6 @@ dependencies {
|
|||
|
||||
implementation applicationDependencies.constraintLayout
|
||||
|
||||
implementation applicationDependencies.rxjava2
|
||||
implementation applicationDependencies.rxAndroid
|
||||
|
||||
testImplementation unitTestDependencies.junit
|
||||
testImplementation unitTestDependencies.kotlinTest
|
||||
testImplementation unitTestDependencies.mockito
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
package com.anytypeio.anytype.core_utils.ext
|
||||
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 02.04.2019.
|
||||
*/
|
||||
interface BaseSchedulerProvider {
|
||||
fun io(): Scheduler
|
||||
fun computation(): Scheduler
|
||||
fun ui(): Scheduler
|
||||
}
|
||||
|
||||
class SchedulerProvider : BaseSchedulerProvider {
|
||||
override fun computation() = Schedulers.computation()
|
||||
override fun ui() = AndroidSchedulers.mainThread()
|
||||
override fun io() = Schedulers.io()
|
||||
}
|
||||
|
||||
class TrampolineSchedulerProvider : BaseSchedulerProvider {
|
||||
override fun computation() = Schedulers.trampoline()
|
||||
override fun ui() = Schedulers.trampoline()
|
||||
override fun io() = Schedulers.trampoline()
|
||||
}
|
||||
|
||||
class TestSchedulerProvider(private val scheduler: TestScheduler) :
|
||||
BaseSchedulerProvider {
|
||||
override fun computation() = scheduler
|
||||
override fun ui() = scheduler
|
||||
override fun io() = scheduler
|
||||
}
|
||||
|
||||
|
||||
fun Disposable.disposedBy(subscriptions: CompositeDisposable) {
|
||||
subscriptions.add(this)
|
||||
}
|
|
@ -7,7 +7,7 @@ allprojects {
|
|||
|
||||
ext {
|
||||
// Kotlin
|
||||
kotlin_coroutines_version = '1.3.9'
|
||||
kotlin_coroutines_version = '1.4.2'
|
||||
kotlinx_serialization_json_version = '1.0.0'
|
||||
|
||||
// AndroidX
|
||||
|
@ -39,13 +39,9 @@ ext {
|
|||
javaxInject_version = '1'
|
||||
retrofit_version = '2.3.0'
|
||||
okhttp_logging_interceptor_version = '3.8.1'
|
||||
rxjava2_version = '2.1.1'
|
||||
moshi_version = '1.8.0'
|
||||
gson_version = '2.8.6'
|
||||
rxrelay_version = '2.1.0'
|
||||
better_link_method_version = '2.2.0'
|
||||
table_view_version = '0.8.9.2'
|
||||
rxbinding_version = '3.0.0'
|
||||
permission_disp_version = '4.6.0'
|
||||
pickt_version = "0.1.11"
|
||||
zxing_version = "4.1.0"
|
||||
|
@ -56,7 +52,7 @@ ext {
|
|||
junit_version = '4.12'
|
||||
mockito_version = '1.4.0'
|
||||
kluent_version = '1.14'
|
||||
coroutine_testing_version = '1.3.2'
|
||||
coroutine_testing_version = '1.4.2'
|
||||
live_data_testing_version = '1.1.0'
|
||||
mockito_kotlin_version = '2.2.0'
|
||||
mockito_android_version = '2.25.0'
|
||||
|
@ -94,6 +90,7 @@ ext {
|
|||
mainApplication = [
|
||||
kotlin: "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version",
|
||||
coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version",
|
||||
coroutinesAndroid: "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version",
|
||||
androidxCore: "androidx.core:core-ktx:$androidx_core_version",
|
||||
fragment: "androidx.fragment:fragment-ktx:$fragment_version",
|
||||
navigation: "androidx.navigation:navigation-fragment-ktx:$navigation_version",
|
||||
|
@ -120,15 +117,10 @@ ext {
|
|||
javaxAnnotation: "javax.annotation:jsr250-api:$javaxAnnotations_version",
|
||||
javaxInject: "javax.inject:javax.inject:$javaxInject_version",
|
||||
gson: "com.google.code.gson:gson:$gson_version",
|
||||
rxRelay: "com.jakewharton.rxrelay2:rxrelay:$rxrelay_version",
|
||||
retrofit: "com.squareup.retrofit2:converter-gson:$retrofit_version",
|
||||
okhttpLoggingInterceptor: "com.squareup.okhttp3:logging-interceptor:$okhttp_logging_interceptor_version",
|
||||
timber: "com.jakewharton.timber:timber:$timber_version",
|
||||
rxjava2: "io.reactivex.rxjava2:rxjava:$rxjava2_version",
|
||||
rxAndroid: "io.reactivex.rxjava2:rxandroid:$rxjava2_version",
|
||||
moshiKotlin: "com.squareup.moshi:moshi-kotlin:$moshi_version",
|
||||
tableView: "com.evrencoskun.library:tableview:$table_view_version",
|
||||
rxBinding: "com.jakewharton.rxbinding3:rxbinding:$rxbinding_version",
|
||||
exoPlayer: "com.google.android.exoplayer:exoplayer:$exoplayer_version",
|
||||
permissionDisp: "org.permissionsdispatcher:permissionsdispatcher:$permission_disp_version",
|
||||
permissionDispCompiler: "org.permissionsdispatcher:permissionsdispatcher-processor:$permission_disp_version",
|
||||
|
|
|
@ -46,7 +46,7 @@ dependencies {
|
|||
implementation project(':data')
|
||||
|
||||
implementation applicationDependencies.kotlin
|
||||
implementation applicationDependencies.coroutines
|
||||
implementation applicationDependencies.coroutinesAndroid
|
||||
implementation applicationDependencies.androidxCore
|
||||
|
||||
implementation applicationDependencies.timber
|
||||
|
|
1
feature_editor/.gitignore
vendored
1
feature_editor/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -1,70 +0,0 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
def config = rootProject.extensions.getByName("ext")
|
||||
|
||||
compileSdkVersion config["compile_sdk"]
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion config["min_sdk"]
|
||||
targetSdkVersion config["target_sdk"]
|
||||
testInstrumentationRunner config["test_runner"]
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
buildConfigField "String", "TEST_JSON", '"marks.json"'
|
||||
}
|
||||
|
||||
debug {
|
||||
buildConfigField "String", "TEST_JSON", '"marks.json"'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets { main { assets.srcDirs = ['src/main/assets', 'src/main/assets/'] } }
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core-utils')
|
||||
|
||||
def applicationDependencies = rootProject.ext.mainApplication
|
||||
def unitTestDependencies = rootProject.ext.unitTesting
|
||||
def acceptenceTesting = rootProject.ext.acceptanceTesting
|
||||
|
||||
kapt applicationDependencies.daggerCompiler
|
||||
compileOnly applicationDependencies.javaxInject
|
||||
|
||||
implementation applicationDependencies.kotlin
|
||||
implementation applicationDependencies.viewModel
|
||||
implementation applicationDependencies.viewModelExtensions
|
||||
implementation applicationDependencies.appcompat
|
||||
implementation applicationDependencies.design
|
||||
implementation applicationDependencies.constraintLayout
|
||||
implementation applicationDependencies.recyclerView
|
||||
|
||||
implementation applicationDependencies.rxjava2
|
||||
implementation applicationDependencies.rxRelay
|
||||
implementation applicationDependencies.rxAndroid
|
||||
implementation applicationDependencies.dagger
|
||||
implementation applicationDependencies.timber
|
||||
implementation applicationDependencies.gson
|
||||
implementation applicationDependencies.betterLinkMovement
|
||||
|
||||
implementation applicationDependencies.glide
|
||||
|
||||
kapt applicationDependencies.glideCompiler
|
||||
|
||||
testImplementation unitTestDependencies.junit
|
||||
testImplementation unitTestDependencies.kotlinTest
|
||||
testImplementation unitTestDependencies.mockito
|
||||
|
||||
androidTestImplementation acceptenceTesting.espressoCore
|
||||
androidTestImplementation acceptenceTesting.androidJUnit
|
||||
androidTestImplementation acceptenceTesting.testRules
|
||||
}
|
21
feature_editor/proguard-rules.pro
vendored
21
feature_editor/proguard-rules.pro
vendored
|
@ -1,21 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -1,251 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableString
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.agileburo.anytype.feature_editor.domain.*
|
||||
import com.agileburo.anytype.feature_editor.factory.AndroidDataFactory
|
||||
import com.agileburo.anytype.feature_editor.presentation.mapper.BlockModelMapper
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView
|
||||
import junit.framework.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 2019-05-15.
|
||||
*/
|
||||
class BlockModelMapperTest {
|
||||
|
||||
lateinit var blockMapper: BlockModelMapper
|
||||
|
||||
val BOLD = "BOLD"
|
||||
val ITALIC = "ITALIC"
|
||||
val STRIKE = "STRIKE_THROUGH"
|
||||
|
||||
val spannableText = SpannableString("This can be simply solved by using Spannable String").apply {
|
||||
setSpan(StyleSpan(Typeface.BOLD), 0, 3, 0)
|
||||
setSpan(StyleSpan(Typeface.BOLD), 19, 24, 0)
|
||||
setSpan(StyleSpan(Typeface.ITALIC), 12, 17, 0)
|
||||
setSpan(StrikethroughSpan(), 27, 33, 0)
|
||||
}
|
||||
|
||||
val blockView = BlockView.ParagraphView(
|
||||
id = "2321",
|
||||
text = spannableText
|
||||
)
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
blockMapper = BlockModelMapper()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShouldBeTheSameMarks() {
|
||||
val block = blockMapper.mapToModel(blockView)
|
||||
val content = block.content as Content.Text
|
||||
|
||||
assertEquals(4, content.marks.size)
|
||||
assertEquals(3, content.marks[0].end)
|
||||
assertEquals(BOLD, content.marks[0].type.name)
|
||||
assertEquals(24, content.marks[1].end)
|
||||
assertEquals(BOLD, content.marks[1].type.name)
|
||||
assertEquals(12, content.marks[2].start)
|
||||
assertEquals(ITALIC, content.marks[2].type.name)
|
||||
assertEquals(33, content.marks[3].end)
|
||||
assertEquals(STRIKE, content.marks[3].type.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun paragraphViewConvertedCorrectly() {
|
||||
|
||||
val view = BlockView.ParagraphView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString())
|
||||
)
|
||||
|
||||
val block = blockMapper.mapToModel(view)
|
||||
val content = block.content as Content.Text
|
||||
|
||||
assertEquals(view.text.toString(), content.text)
|
||||
assertEquals(view.id, view.id)
|
||||
assertEquals(block.blockType, BlockType.Editable)
|
||||
assertEquals(block.contentType, ContentType.P)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bulletViewConvertedCorrectly() {
|
||||
|
||||
val view = BlockView.BulletView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString())
|
||||
)
|
||||
|
||||
val block = blockMapper.mapToModel(view)
|
||||
val content = block.content as Content.Text
|
||||
|
||||
assertEquals(view.text.toString(), content.text)
|
||||
assertEquals(view.id, view.id)
|
||||
assertEquals(block.blockType, BlockType.Editable)
|
||||
assertEquals(block.contentType, ContentType.UL)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun quoteViewConvertedCorrectly() {
|
||||
|
||||
val view = BlockView.QuoteView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString())
|
||||
)
|
||||
|
||||
val block = blockMapper.mapToModel(view)
|
||||
val content = block.content as Content.Text
|
||||
|
||||
assertEquals(view.text.toString(), content.text)
|
||||
assertEquals(view.id, view.id)
|
||||
assertEquals(block.blockType, BlockType.Editable)
|
||||
assertEquals(block.contentType, ContentType.Quote)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkboxViewConvertedCorrectly() {
|
||||
|
||||
val view = BlockView.CheckboxView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString()),
|
||||
isChecked = AndroidDataFactory.randomBoolean()
|
||||
)
|
||||
|
||||
val block = blockMapper.mapToModel(view)
|
||||
val content = block.content as Content.Text
|
||||
|
||||
assertEquals(view.text.toString(), content.text)
|
||||
assertEquals(view.id, view.id)
|
||||
assertEquals(block.blockType, BlockType.Editable)
|
||||
assertEquals(block.contentType, ContentType.Check)
|
||||
assertEquals(content.param.checked, view.isChecked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun numberedListItemConvertedCorrectly() {
|
||||
|
||||
val view = BlockView.NumberListItemView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString()),
|
||||
number = AndroidDataFactory.randomInt()
|
||||
)
|
||||
|
||||
val block = blockMapper.mapToModel(view)
|
||||
val content = block.content as Content.Text
|
||||
|
||||
assertEquals(view.text.toString(), content.text)
|
||||
assertEquals(view.id, view.id)
|
||||
assertEquals(block.blockType, BlockType.Editable)
|
||||
assertEquals(block.contentType, ContentType.NumberedList)
|
||||
assertEquals(content.param.number, view.number)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun codeSnippetViewConvertedCorrectly() {
|
||||
|
||||
val view = BlockView.CodeSnippetView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString())
|
||||
)
|
||||
|
||||
val block = blockMapper.mapToModel(view)
|
||||
val content = block.content as Content.Text
|
||||
|
||||
assertEquals(view.text.toString(), content.text)
|
||||
assertEquals(view.id, view.id)
|
||||
assertEquals(block.blockType, BlockType.Editable)
|
||||
assertEquals(block.contentType, ContentType.Code)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun headerOneViewConvertedCorrectly() {
|
||||
|
||||
val headerOneView = BlockView.HeaderView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString()),
|
||||
type = BlockView.HeaderView.HeaderType.ONE
|
||||
)
|
||||
|
||||
|
||||
val headerOneBlock = blockMapper.mapToModel(headerOneView)
|
||||
val content = headerOneBlock.content as Content.Text
|
||||
|
||||
|
||||
assertEquals(headerOneView.text.toString(), content.text)
|
||||
assertEquals(headerOneView.id, headerOneView.id)
|
||||
assertEquals(headerOneBlock.blockType, BlockType.Editable)
|
||||
assertEquals(headerOneBlock.contentType, ContentType.H1)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun headerTwoViewConvertedCorrectly() {
|
||||
|
||||
val headerTwoView = BlockView.HeaderView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString()),
|
||||
type = BlockView.HeaderView.HeaderType.TWO
|
||||
)
|
||||
|
||||
val headerTwoBlock = blockMapper.mapToModel(headerTwoView)
|
||||
val content = headerTwoBlock.content as Content.Text
|
||||
|
||||
|
||||
assertEquals(headerTwoView.text.toString(), content.text)
|
||||
assertEquals(headerTwoView.id, headerTwoView.id)
|
||||
assertEquals(headerTwoBlock.blockType, BlockType.Editable)
|
||||
assertEquals(headerTwoBlock.contentType, ContentType.H2)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun headerThreeViewConvertedCorrectly() {
|
||||
|
||||
val headerThreeView = BlockView.HeaderView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString()),
|
||||
type = BlockView.HeaderView.HeaderType.THREE
|
||||
)
|
||||
|
||||
val headerThreeBlock = blockMapper.mapToModel(headerThreeView)
|
||||
val content = headerThreeBlock.content as Content.Text
|
||||
|
||||
|
||||
assertEquals(headerThreeView.text.toString(), content.text)
|
||||
assertEquals(headerThreeView.id, headerThreeView.id)
|
||||
assertEquals(headerThreeBlock.blockType, BlockType.Editable)
|
||||
assertEquals(headerThreeBlock.contentType, ContentType.H3)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun headerFourViewConvertedCorrectly() {
|
||||
|
||||
val headerFourView = BlockView.HeaderView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString()),
|
||||
type = BlockView.HeaderView.HeaderType.FOUR
|
||||
)
|
||||
|
||||
val headerFourBlock = blockMapper.mapToModel(headerFourView)
|
||||
val content = headerFourBlock.content as Content.Text
|
||||
|
||||
|
||||
assertEquals(headerFourView.text.toString(), content.text)
|
||||
assertEquals(headerFourView.id, headerFourView.id)
|
||||
assertEquals(headerFourBlock.blockType, BlockType.Editable)
|
||||
assertEquals(headerFourBlock.contentType, ContentType.H4)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor
|
||||
|
||||
import android.text.SpannableString
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.agileburo.anytype.feature_editor.factory.AndroidDataFactory
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView
|
||||
import com.agileburo.anytype.feature_editor.presentation.util.BlockViewDiffUtil
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class BlockViewDiffUtilTest {
|
||||
|
||||
|
||||
private lateinit var diffUtil : DiffUtil.Callback
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
|
||||
val firstBlock = BlockView.ParagraphView(
|
||||
id = AndroidDataFactory.randomString(),
|
||||
text = SpannableString(AndroidDataFactory.randomString())
|
||||
)
|
||||
|
||||
val second = firstBlock.copy()
|
||||
|
||||
diffUtil = BlockViewDiffUtil(listOf(firstBlock), listOf(second))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun blocksShouldBeTheSame() {
|
||||
val result = diffUtil.areItemsTheSame(0, 0)
|
||||
assert(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun blockShouldBeEquals() {
|
||||
val result = diffUtil.areContentsTheSame(0,0)
|
||||
Assert.assertTrue(result)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.factory
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
|
||||
object AndroidDataFactory {
|
||||
|
||||
fun randomUuid(): String {
|
||||
return UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
fun randomString(): String {
|
||||
return randomUuid()
|
||||
}
|
||||
|
||||
fun randomInt(): Int {
|
||||
return ThreadLocalRandom.current().nextInt(0, 1000 + 1)
|
||||
}
|
||||
|
||||
fun randomInt(min : Int = 0, max: Int): Int {
|
||||
return ThreadLocalRandom.current().nextInt(min, max)
|
||||
}
|
||||
|
||||
fun randomLong(): Long {
|
||||
return randomInt().toLong()
|
||||
}
|
||||
|
||||
fun randomFloat(): Float {
|
||||
return randomInt().toFloat()
|
||||
}
|
||||
|
||||
fun randomDouble(): Double {
|
||||
return randomInt().toDouble()
|
||||
}
|
||||
|
||||
fun randomBoolean(): Boolean {
|
||||
return Math.random() < 0.5
|
||||
}
|
||||
|
||||
fun makeStringList(count: Int): List<String> {
|
||||
val items = mutableListOf<String>()
|
||||
repeat(count) {
|
||||
items.add(randomUuid())
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.agileburo.anytype.feature_editor" />
|
|
@ -1,78 +0,0 @@
|
|||
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"id": "c0301f2b-b532-55e1-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Первый",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "fdresaf2b-b532-5345-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Третий",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "ytewty6q-b532-5345-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Пятый",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "6ytewty6q-b532-5345-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Шестой",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "7ytewty6q-b532-5345-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Седьмой",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "8ytewty6q-b532-5345-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Восьмой",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"id": "c0301f2b-b532-55e1-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Первый",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "fdresaf2b-b532-5345-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Третий",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "ytewty6q-b532-5345-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 11,
|
||||
"content": {
|
||||
"text": "Пятый",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,365 +0,0 @@
|
|||
{
|
||||
"blocks": [
|
||||
{
|
||||
"id": "0",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 3,
|
||||
"content": {"text":"Разработка AnyType","marks":[]},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id":"1",
|
||||
"parentId":"",
|
||||
"type":6,
|
||||
"contentType":0,
|
||||
"content":{
|
||||
"original":{
|
||||
"key":"TACT7msSX8vkkFBhanRYT2wkA77hjfowo7WGyrrL9SEeEVEixy3LpDwp8j46",
|
||||
"type":"image/jpeg",
|
||||
"name":"1280px-Francesco_Salviati_005.jpg",
|
||||
"hash":"QmXzbG8L8aeZ3VUnMJDJzVCKNTf1MMrGPdUsgsR2F3f3vu",
|
||||
"size":311430,
|
||||
"time":1558123043583
|
||||
},
|
||||
"size":{
|
||||
"width":770,
|
||||
"div":0.73
|
||||
}
|
||||
},
|
||||
"width":0,
|
||||
"children":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"id":"2",
|
||||
"parentId":"",
|
||||
"type": 3,
|
||||
"contentType": 9,
|
||||
"content":{"text":"First toggle","marks":[]},
|
||||
"width": 0,
|
||||
"children":[
|
||||
{
|
||||
"id":"3",
|
||||
"parentId":"2",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content":{"text":"First toggle first paragraph","marks":[]},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id":"4",
|
||||
"parentId":"2",
|
||||
"type": 3,
|
||||
"contentType": 9,
|
||||
"content":{"text":"Second toggle","marks":[]},
|
||||
"width": 0,
|
||||
"children": [
|
||||
{
|
||||
"id":"5",
|
||||
"parentId":"4",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content":{"text":"Second toggle first paragraph","marks":[]},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id":"6",
|
||||
"parentId":"4",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content":{"text":"Second toggle second paragraph","marks":[]},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id":"7",
|
||||
"parentId":"4",
|
||||
"type": 3,
|
||||
"contentType": 9,
|
||||
"content":{"text":"Third toggle","marks":[]},
|
||||
"width": 0,
|
||||
"children": [
|
||||
{
|
||||
"id":"8",
|
||||
"parentId":"7",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content":{"text":"Third toggle first paragraph","marks":[]},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id":"9",
|
||||
"parentId":"7",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content":{"text":"Third toggle second paragraph","marks":[]},
|
||||
"width": 0,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Так говорила в июле 1805 года известная Анна Павловна Шерер, фрейлина и приближенная императрицы Марии Феодоровны, встречая важного и чиновного князя Василия, первого приехавшего на ее вечер. Анна Павловна кашляла несколько дней, у нее был грипп,как она говорила (грипп был тогда новое слово, употреблявшееся только редкими). В записочках, разосланных утрoом с красным лакеем, было написано без различия во всех:",
|
||||
"marks": [
|
||||
{
|
||||
"type": "i",
|
||||
"start": 240,
|
||||
"end": 246
|
||||
},
|
||||
{
|
||||
"type": "i",
|
||||
"start": 264,
|
||||
"end": 269
|
||||
},
|
||||
{
|
||||
"type": "b",
|
||||
"start": 340,
|
||||
"end": 374
|
||||
},
|
||||
{
|
||||
"type": "i",
|
||||
"start": 150,
|
||||
"end": 157
|
||||
},
|
||||
{
|
||||
"type": "i",
|
||||
"start": 40,
|
||||
"end": 59
|
||||
},
|
||||
{
|
||||
"type": "i",
|
||||
"start": 97,
|
||||
"end": 113
|
||||
},
|
||||
{
|
||||
"type": "i",
|
||||
"start": 192,
|
||||
"end": 205
|
||||
},
|
||||
{
|
||||
"type": "b",
|
||||
"start": 167,
|
||||
"end": 178
|
||||
},
|
||||
{
|
||||
"type": "b",
|
||||
"start": 125,
|
||||
"end": 130
|
||||
},
|
||||
{
|
||||
"type": "s",
|
||||
"start": 254,
|
||||
"end": 259
|
||||
},
|
||||
{
|
||||
"type": "i",
|
||||
"start": 397,
|
||||
"end": 402
|
||||
},
|
||||
{
|
||||
"type": "kbd",
|
||||
"start": 328,
|
||||
"end": 338
|
||||
},
|
||||
{
|
||||
"type": "a",
|
||||
"start": 316,
|
||||
"end": 323,
|
||||
"param": "https://yandex.ru/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "«Si vous n'avez rien de mieux à faire, Monsieur le comte (или mon prince), et si la perspective de passer la soirée chez une pauvre malade ne vous effraye pas trop, je serai charmée de vous voir chez moi entre 7 et 10 heures. Annette Scherer» ",
|
||||
"marks": [
|
||||
{
|
||||
"type": "s",
|
||||
"start": 0,
|
||||
"end": 242
|
||||
}
|
||||
]
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Dieu, quelle virulente sortie! — отвечал, нисколько не смутясь такою встречей, вошедший князь, в придворном, шитом мундире, в чулках, башмаках и звездах, с светлым выражением плоского лица.",
|
||||
"marks": [
|
||||
{
|
||||
"type": "a",
|
||||
"param": "https://ilibrary.ru/text/11/p.1/index.html#fn4",
|
||||
"start": 0,
|
||||
"end": 30
|
||||
},
|
||||
{
|
||||
"type": "b",
|
||||
"start": 33,
|
||||
"end": 40
|
||||
}
|
||||
]
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Жена узнала, что муж был в связи с бывшею в их доме француженкою-гувернанткой, и объявила мужу, что не может жить с ним в одном доме. ",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 10,
|
||||
"content": {
|
||||
"text": "На третий день после ссоры князь Степан Аркадьич Облонский — Стива, как его звали в свете, — в обычный час, то есть в восемь часов утра, проснулся не в спальне жены, а в своем кабинете, на сафьянном диване.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 2,
|
||||
"content": {
|
||||
"text": "Он повернул свое полное, выхоленное тело на пружинах дивана, как бы желая опять заснуть надолго, с другой стороны крепко обнял подушку и прижался к ней щекой; но вдруг вскочил, сел на диван и открыл глаза.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "16",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "«Да, да, как это было? — думал он, вспоминая сон.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "17",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Первый",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Третий",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 11,
|
||||
"content": {
|
||||
"text": "Пятый",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "20",
|
||||
"parentId": "",
|
||||
"type": 7,
|
||||
"contentType": 0,
|
||||
"content": {
|
||||
"id": "12D3KooWRUkB3qUaxiz1VCGP3Uoy4jv8Nx7kCKdMSuhmXqwsjMJ6"
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id":"21",
|
||||
"parentId":"",
|
||||
"type":4,
|
||||
"contentType":0,
|
||||
"content":"",
|
||||
"width":0,
|
||||
"children":[
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "22",
|
||||
"parentId": "",
|
||||
"type": 9,
|
||||
"contentType": 0,
|
||||
"content": {
|
||||
"bookMark": {
|
||||
"type": "profile",
|
||||
"url": "https://rateyourmusic.com/artist/the_tallest_man_on_earth",
|
||||
"title": "The Tallest Man on Earth discography",
|
||||
"description": "The Tallest Man on Earth discography and songs: Music profile for The Tallest Man on Earth, born 30 April 1983. Genres: Contemporary Folk, Singer/Songwriter, Indie Folk. Albums include The Wild Hunt, Shallow Grave, and There's No Leaving Now.",
|
||||
"site": "RateYourMusic",
|
||||
"icon": "",
|
||||
"images": [
|
||||
{
|
||||
"url": "https://e.snmc.io/i/600/s/a9eec69d93bfd25142741293d34de3f8/4095112",
|
||||
"secure": "",
|
||||
"type": "",
|
||||
"width": 0,
|
||||
"height": 0
|
||||
}
|
||||
],
|
||||
"audios": [],
|
||||
"videos": []
|
||||
}
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
{
|
||||
"blocks": [
|
||||
{
|
||||
"id": "1cb83d95-7913-5c2e-99e8-1272110ab38f",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Все счастливые семьи похожи друг на друга, каждая несчастливая семья несчастлива по-своему.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "02ca4410-2cff-5978-81e5-09dc76dd003c",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 4,
|
||||
"content": {
|
||||
"text": "Все смешалось в доме Облонских.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "116271cc-c6e3-5c58-8598-9342567b9a66",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 5,
|
||||
"content": {
|
||||
"text": "Жена узнала, что муж был в связи с бывшею в их доме француженкою-гувернанткой, и объявила мужу, что не может жить с ним в одном доме. ",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "567980cc-fght-wedr-5679-9342567b9a76",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 11,
|
||||
"content": {
|
||||
"text": "Положение это продолжалось уже третий день и мучительно чувствовалось и самими супругами, и всеми членами семьи, и домочадцами.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "93f8810e-590f-5962-a662-2c1fe17e5cbe",
|
||||
"parentId": "",
|
||||
"type": 1,
|
||||
"contentType": 0,
|
||||
"content": {},
|
||||
"children": [
|
||||
{
|
||||
"id": "22a8f9f5-b042-5532-bb5d-cbda07f579e0",
|
||||
"parentId": "93f8810e-590f-5962-a662-2c1fe17e5cbe",
|
||||
"type": 3,
|
||||
"contentType": 8,
|
||||
"content": {
|
||||
"text": "Все члены семьи и домочадцы чувствовали, что нет смысла в их сожительстве и что на каждом постоялом дворе случайно сошедшиеся люди более связаны между собой, чем они, члены семьи и домочадцы Облонских.",
|
||||
"marks": []
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "1983",
|
||||
"parentId": "92152a56-40d3-53ec-a1fa-ec4e4d92b0ec",
|
||||
"type": 3,
|
||||
"contentType": 7,
|
||||
"content": {
|
||||
"text": "Жена не выходила из своих комнат, мужа третий день не было дома.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "92152a56-40d3-53ec-a1fa-ec4e4d92b0ec",
|
||||
"parentId": "93f8810e-590f-5962-a662-2c1fe17e5cbe",
|
||||
"type": 3,
|
||||
"contentType": 6,
|
||||
"content": {
|
||||
"text": "Дети бегали по всему дому, как потерянные; англичанка поссорилась с экономкой и написала записку приятельнице, прося приискать ей новое место; повар ушел вчера со двора, во время самого обеда; черная кухарка и кучер просили расчета.",
|
||||
"marks": [],
|
||||
"number": 1
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "21fe093a-5f74-583b-9c17-ee798e1bfc7e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 10,
|
||||
"content": {
|
||||
"text": "На третий день после ссоры князь Степан Аркадьич Облонский — Стива, как его звали в свете, — в обычный час, то есть в восемь часов утра, проснулся не в спальне жены, а в своем кабинете, на сафьянном диване.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "f0c40441-2a3f-5150-bad3-e0d43c72a997",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 2,
|
||||
"content": {
|
||||
"text": "Он повернул свое полное, выхоленное тело на пружинах дивана, как бы желая опять заснуть надолго, с другой стороны крепко обнял подушку и прижался к ней щекой; но вдруг вскочил, сел на диван и открыл глаза.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "01f2a219-94ba-5bd6-a48e-5b9af61e1a10",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "«Да, да, как это было? — думал он, вспоминая сон.",
|
||||
"marks": []
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "c0301f2b-b532-55e1-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Первый",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "fdresaf2b-b532-5345-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 1,
|
||||
"content": {
|
||||
"text": "Третий",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "ytewty6q-b532-5345-92ba-2b2fc4af237e",
|
||||
"parentId": "",
|
||||
"type": 3,
|
||||
"contentType": 11,
|
||||
"content": {
|
||||
"text": "Пятый",
|
||||
"marks": []
|
||||
},
|
||||
"width": 0,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor
|
||||
|
||||
import android.content.Context
|
||||
import com.agileburo.anytype.core_utils.di.scope.PerFeature
|
||||
import com.agileburo.anytype.core_utils.ext.BaseSchedulerProvider
|
||||
import com.agileburo.anytype.feature_editor.data.*
|
||||
import com.agileburo.anytype.feature_editor.data.datasource.BlockDataSource
|
||||
import com.agileburo.anytype.feature_editor.data.datasource.IPFSDataSourceImpl
|
||||
import com.agileburo.anytype.feature_editor.data.parser.ContentModelParser
|
||||
import com.agileburo.anytype.feature_editor.domain.EditorInteractor
|
||||
import com.agileburo.anytype.feature_editor.domain.EditorInteractorImpl
|
||||
import com.agileburo.anytype.feature_editor.presentation.converter.BlockContentTypeConverter
|
||||
import com.agileburo.anytype.feature_editor.presentation.converter.BlockContentTypeConverterImpl
|
||||
import com.agileburo.anytype.feature_editor.presentation.mvvm.EditorViewModelFactory
|
||||
import com.agileburo.anytype.feature_editor.ui.EditorFragment
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.Subcomponent
|
||||
|
||||
@PerFeature
|
||||
@Subcomponent(modules = [EditorModule::class])
|
||||
interface EditorComponent {
|
||||
|
||||
fun inject(fragment: EditorFragment)
|
||||
}
|
||||
|
||||
@Module
|
||||
@PerFeature
|
||||
class EditorModule {
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideGson(): Gson = GsonBuilder().create()
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideBlockConverter(
|
||||
contentConverter: ContentConverter,
|
||||
contentModelParser : ContentModelParser
|
||||
): BlockConverter =
|
||||
BlockConverterImpl(contentConverter = contentConverter, contentModelParser = contentModelParser)
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideRepo(blockConverter: BlockConverter, dataSource: BlockDataSource): EditorRepo =
|
||||
EditorRepoImpl(dataSource = dataSource, blockConverter = blockConverter)
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideInteractor(repo: EditorRepo): EditorInteractor = EditorInteractorImpl(repo = repo)
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideDataSource(context: Context, gson: Gson): BlockDataSource =
|
||||
IPFSDataSourceImpl(context = context, gson = gson)
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideFactory(
|
||||
interactor: EditorInteractor,
|
||||
contentTypeConverter: BlockContentTypeConverter,
|
||||
baseSchedulerProvider: BaseSchedulerProvider
|
||||
): EditorViewModelFactory =
|
||||
EditorViewModelFactory(interactor, contentTypeConverter, baseSchedulerProvider)
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideBlockContentConverter(markConverter: MarkConverter): ContentConverter =
|
||||
ContentConverterImpl(markConverter = markConverter)
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideMarkConverter(): MarkConverter = MarkConverterImpl()
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideContentModelParser(gson : Gson) : ContentModelParser {
|
||||
return ContentModelParser(gson)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@PerFeature
|
||||
fun provideContentTypeConverter(): BlockContentTypeConverter =
|
||||
BlockContentTypeConverterImpl()
|
||||
|
||||
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor
|
||||
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 21.03.2019.
|
||||
*/
|
||||
|
||||
fun Disposable.disposedBy(subscriptions: CompositeDisposable) {
|
||||
subscriptions.add(this)
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data
|
||||
|
||||
import com.agileburo.anytype.feature_editor.data.converter.toBlockType
|
||||
import com.agileburo.anytype.feature_editor.data.converter.toContentType
|
||||
import com.agileburo.anytype.feature_editor.data.parser.ContentModelParser
|
||||
import com.agileburo.anytype.feature_editor.domain.Block
|
||||
import com.agileburo.anytype.feature_editor.domain.BlockType
|
||||
import com.agileburo.anytype.feature_editor.domain.Content
|
||||
import com.agileburo.anytype.feature_editor.domain.ContentType
|
||||
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 20.03.2019.
|
||||
*/
|
||||
interface BlockConverter {
|
||||
fun modelToDomain(model: BlockModel): Block
|
||||
fun modelTreeToDomainTree(model: BlockModel): Block
|
||||
}
|
||||
|
||||
class BlockConverterImpl(
|
||||
private val contentConverter: ContentConverter,
|
||||
private val contentModelParser: ContentModelParser
|
||||
) : BlockConverter {
|
||||
|
||||
override fun modelTreeToDomainTree(model: BlockModel): Block {
|
||||
|
||||
val type = model.type.toBlockType()
|
||||
|
||||
when (type) {
|
||||
is BlockType.Editable -> {
|
||||
|
||||
val parsed = contentModelParser.parse(model.content, type)
|
||||
|
||||
val content = contentConverter.modelToDomain(parsed as ContentModel.Text)
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
content = content,
|
||||
parentId = model.parentId,
|
||||
contentType = model.contentType.toContentType(),
|
||||
blockType = model.type.toBlockType(),
|
||||
children = model.children.map { modelTreeToDomainTree(it) }.toMutableList()
|
||||
)
|
||||
|
||||
}
|
||||
is BlockType.Page -> {
|
||||
|
||||
val parsed = contentModelParser.parse(model.content, type)
|
||||
|
||||
val content = contentConverter.modelToDomain(parsed as ContentModel.Page)
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
content = content,
|
||||
parentId = model.parentId,
|
||||
contentType = model.contentType.toContentType(),
|
||||
blockType = model.type.toBlockType(),
|
||||
children = model.children.map { modelTreeToDomainTree(it) }.toMutableList()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is BlockType.BookMark -> {
|
||||
|
||||
val parsed = contentModelParser.parse(model.content, type)
|
||||
|
||||
val content = contentConverter.modelToDomain(parsed as ContentModel.Bookmark)
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
content = content,
|
||||
parentId = model.parentId,
|
||||
contentType = model.contentType.toContentType(),
|
||||
blockType = model.type.toBlockType(),
|
||||
children = model.children.map { modelTreeToDomainTree(it) }.toMutableList()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is BlockType.Divider -> {
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
parentId = model.parentId,
|
||||
contentType = ContentType.None,
|
||||
content = Content.Empty,
|
||||
blockType = BlockType.Divider,
|
||||
children = model.children.map { modelTreeToDomainTree(it) }.toMutableList()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is BlockType.Image -> {
|
||||
|
||||
val parsed = contentModelParser.parse(model.content, type) as ContentModel.Image
|
||||
val content = contentConverter.modelToDomain(parsed)
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
parentId = model.parentId,
|
||||
content = content,
|
||||
contentType = ContentType.None,
|
||||
blockType = BlockType.Image,
|
||||
children = model.children.map { modelTreeToDomainTree(it) }.toMutableList()
|
||||
)
|
||||
}
|
||||
|
||||
else -> TODO()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun modelToDomain(model: BlockModel): Block {
|
||||
|
||||
val type = model.type.toBlockType()
|
||||
|
||||
when (type) {
|
||||
is BlockType.Editable -> {
|
||||
|
||||
val parsed = contentModelParser.parse(model.content, type)
|
||||
|
||||
val content = contentConverter.modelToDomain(parsed as ContentModel.Text)
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
content = content,
|
||||
parentId = model.parentId,
|
||||
contentType = model.contentType.toContentType(),
|
||||
blockType = model.type.toBlockType()
|
||||
)
|
||||
|
||||
}
|
||||
is BlockType.Page -> {
|
||||
|
||||
val parsed = contentModelParser.parse(model.content, type)
|
||||
|
||||
val content = contentConverter.modelToDomain(parsed as ContentModel.Page)
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
content = content,
|
||||
parentId = model.parentId,
|
||||
contentType = model.contentType.toContentType(),
|
||||
blockType = model.type.toBlockType()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is BlockType.BookMark -> {
|
||||
|
||||
val parsed = contentModelParser.parse(model.content, type)
|
||||
|
||||
val content = contentConverter.modelToDomain(parsed as ContentModel.Bookmark)
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
content = content,
|
||||
parentId = model.parentId,
|
||||
contentType = model.contentType.toContentType(),
|
||||
blockType = model.type.toBlockType()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is BlockType.Divider -> {
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
parentId = model.parentId,
|
||||
contentType = ContentType.None,
|
||||
content = Content.Empty,
|
||||
blockType = BlockType.Divider
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is BlockType.Image -> {
|
||||
|
||||
val parsed = contentModelParser.parse(model.content, type) as ContentModel.Image
|
||||
val content = contentConverter.modelToDomain(parsed)
|
||||
|
||||
return Block(
|
||||
id = model.id,
|
||||
parentId = model.parentId,
|
||||
content = content,
|
||||
contentType = ContentType.None,
|
||||
blockType = BlockType.Image
|
||||
)
|
||||
}
|
||||
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 20.03.2019.
|
||||
*/
|
||||
data class BlockModel(
|
||||
val id: String,
|
||||
val parentId: String = "",
|
||||
val content: JsonElement,
|
||||
val contentType: Int,
|
||||
val type: Int,
|
||||
val children: List<BlockModel>
|
||||
)
|
|
@ -1,65 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data
|
||||
|
||||
import com.agileburo.anytype.feature_editor.domain.Content
|
||||
import com.agileburo.anytype.feature_editor.domain.ContentParam
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 25.03.2019.
|
||||
*/
|
||||
interface ContentConverter {
|
||||
fun modelToDomain(model: ContentModel.Text): Content.Text
|
||||
fun domainToModel(domain: Content.Text): ContentModel.Text
|
||||
fun modelToDomain(model : ContentModel.Page) : Content.Page
|
||||
fun modelToDomain(model : ContentModel.Bookmark) : Content.Bookmark
|
||||
fun modelToDomain(model : ContentModel.Image) : Content.Picture
|
||||
}
|
||||
|
||||
class ContentConverterImpl(private val markConverter: MarkConverter) : ContentConverter {
|
||||
|
||||
override fun modelToDomain(model: ContentModel.Text) =
|
||||
Content.Text(
|
||||
text = model.text,
|
||||
marks = model.marks.map { markConverter.modelToDomain(it) },
|
||||
param = ContentParam(
|
||||
mutableMapOf(
|
||||
"number" to (model.number ?: 0),
|
||||
"checked" to (model.checked ?: false)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
//TODO add marks convert!
|
||||
override fun domainToModel(domain: Content.Text) =
|
||||
ContentModel.Text(
|
||||
text = domain.text.toString(),
|
||||
marks = domain.marks.map { markConverter.domainToModel(it) },
|
||||
number = domain.param.number,
|
||||
checked = domain.param.checked
|
||||
)
|
||||
|
||||
override fun modelToDomain(model: ContentModel.Page): Content.Page {
|
||||
return Content.Page(model.id)
|
||||
}
|
||||
|
||||
override fun modelToDomain(model: ContentModel.Bookmark): Content.Bookmark {
|
||||
return Content.Bookmark(
|
||||
type = model.bookMark.type,
|
||||
description = model.bookMark.description,
|
||||
title = model.bookMark.title,
|
||||
url = model.bookMark.url,
|
||||
site = model.bookMark.site,
|
||||
icon = model.bookMark.icon,
|
||||
images = model.bookMark.images.map { image -> Content.Bookmark.Image(image.url) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun modelToDomain(model: ContentModel.Image): Content.Picture {
|
||||
// TODO remove hard-coded
|
||||
return Content.Picture(
|
||||
url = "https://c.wallhere.com/photos/6c/31/vintage_typewriters_books_glasses_wood_paper_pine_cones-727921.jpg!d",
|
||||
type = Content.Picture.Type.ORIGINAL
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 25.03.2019.
|
||||
*/
|
||||
sealed class ContentModel {
|
||||
|
||||
data class Text(
|
||||
val text: String = "",
|
||||
val marks: List<MarkModel> = emptyList(),
|
||||
val number : Int? = null,
|
||||
val checked : Boolean? = null
|
||||
) : ContentModel()
|
||||
|
||||
data class Page(
|
||||
val id : String
|
||||
) : ContentModel()
|
||||
|
||||
data class Bookmark(
|
||||
val bookMark : BookmarkModel
|
||||
) : ContentModel()
|
||||
|
||||
data class BookmarkModel(
|
||||
val type : String,
|
||||
val url : String,
|
||||
val title : String,
|
||||
val description : String,
|
||||
val site : String,
|
||||
val icon : String,
|
||||
val images : List<ImageModel>
|
||||
)
|
||||
|
||||
data class Image(
|
||||
val original : OriginalImageModel
|
||||
) : ContentModel()
|
||||
|
||||
data class OriginalImageModel(
|
||||
val key : String,
|
||||
val type : String,
|
||||
val name : String,
|
||||
val size : Int,
|
||||
val time : Long
|
||||
)
|
||||
|
||||
data class ImageModel(val url : String)
|
||||
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data
|
||||
|
||||
import com.agileburo.anytype.feature_editor.data.datasource.BlockDataSource
|
||||
import com.agileburo.anytype.feature_editor.domain.Block
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
interface EditorRepo {
|
||||
|
||||
fun getBlocks(): Single<List<Block>>
|
||||
fun saveState(list: List<Block>)
|
||||
}
|
||||
|
||||
class EditorRepoImpl @Inject constructor(
|
||||
private val dataSource: BlockDataSource,
|
||||
private val blockConverter: BlockConverter
|
||||
) : EditorRepo {
|
||||
|
||||
override fun getBlocks(): Single<List<Block>> {
|
||||
return dataSource.getBlocks().map { it.map(blockConverter::modelTreeToDomainTree) }
|
||||
}
|
||||
|
||||
override fun saveState(list: List<Block>) {
|
||||
wrap(list)
|
||||
}
|
||||
|
||||
// TODO перевести в отдельный маппер, пусть репозиторий отвечает толька за CRUD-операции
|
||||
private fun unwrap(blocks: List<BlockModel>): List<Block> {
|
||||
val result = mutableListOf<Block>()
|
||||
if (blocks.isEmpty()) return result
|
||||
blocks.forEach {
|
||||
result.add(blockConverter.modelToDomain(it))
|
||||
if (it.children.isNotEmpty()) {
|
||||
result.addAll(unwrap(it.children))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
//TODO предполагаем, что понадобится перевод листа блоков в дерево для отдачи беку
|
||||
private fun wrap(list: List<Block>): List<BlockModel> {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data
|
||||
|
||||
import android.net.MacAddress
|
||||
import com.agileburo.anytype.feature_editor.domain.Mark
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 03.04.2019.
|
||||
*/
|
||||
interface MarkConverter {
|
||||
|
||||
fun modelToDomain(model: MarkModel): Mark
|
||||
fun domainToModel(domain: Mark): MarkModel
|
||||
}
|
||||
|
||||
class MarkConverterImpl : MarkConverter {
|
||||
|
||||
override fun modelToDomain(model: MarkModel): Mark =
|
||||
Mark(type = getType(model.type),
|
||||
start = model.start,
|
||||
end = model.end,
|
||||
param = model.param)
|
||||
|
||||
override fun domainToModel(domain: Mark): MarkModel {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
private fun getType(type: String) =
|
||||
when (type) {
|
||||
"b" -> Mark.MarkType.BOLD
|
||||
"i" -> Mark.MarkType.ITALIC
|
||||
"s" -> Mark.MarkType.STRIKE_THROUGH
|
||||
"kbd" -> Mark.MarkType.CODE
|
||||
"a" -> Mark.MarkType.HYPERTEXT
|
||||
else -> Mark.MarkType.UNDEFINED
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 03.04.2019.
|
||||
*/
|
||||
data class MarkModel(val type: String = "",
|
||||
val start: Int = 0,
|
||||
val end: Int = 0,
|
||||
val param: String = "")
|
|
@ -1,33 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data.converter
|
||||
|
||||
import com.agileburo.anytype.feature_editor.domain.BlockType
|
||||
import com.agileburo.anytype.feature_editor.domain.BlockTypes
|
||||
|
||||
fun Int.toBlockType(): BlockType =
|
||||
when (this) {
|
||||
BlockTypes.HORIZONTAL_GRID -> BlockType.HrGrid
|
||||
BlockTypes.VERTICAL_GRID -> BlockType.VrGrid
|
||||
BlockTypes.EDITABLE -> BlockType.Editable
|
||||
BlockTypes.DIVIDER -> BlockType.Divider
|
||||
BlockTypes.VIDEO -> BlockType.Video
|
||||
BlockTypes.IMAGE -> BlockType.Image
|
||||
BlockTypes.PAGE -> BlockType.Page
|
||||
BlockTypes.NEW_PAGE -> BlockType.NewPage
|
||||
BlockTypes.BOOKMARK -> BlockType.BookMark
|
||||
BlockTypes.FILE -> BlockType.File
|
||||
else -> throw IllegalStateException("Unexpected block type code: $this")
|
||||
}
|
||||
|
||||
fun BlockType.toNumericalCode() : Int =
|
||||
when(this) {
|
||||
BlockType.HrGrid -> BlockTypes.HORIZONTAL_GRID
|
||||
BlockType.VrGrid -> BlockTypes.VERTICAL_GRID
|
||||
BlockType.Editable -> BlockTypes.EDITABLE
|
||||
BlockType.Divider -> BlockTypes.DIVIDER
|
||||
BlockType.Video -> BlockTypes.VIDEO
|
||||
BlockType.Image -> BlockTypes.IMAGE
|
||||
BlockType.Page -> BlockTypes.PAGE
|
||||
BlockType.NewPage -> BlockTypes.NEW_PAGE
|
||||
BlockType.BookMark -> BlockTypes.BOOKMARK
|
||||
BlockType.File -> BlockTypes.FILE
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data.converter
|
||||
|
||||
import com.agileburo.anytype.feature_editor.domain.ContentType
|
||||
import com.agileburo.anytype.feature_editor.domain.ContentTypes
|
||||
|
||||
fun Int.toContentType(): ContentType =
|
||||
when (this) {
|
||||
ContentTypes.UNDEFINED -> ContentType.None
|
||||
ContentTypes.PARAGRAPH -> ContentType.P
|
||||
ContentTypes.CODE_SNIPPET -> ContentType.Code
|
||||
ContentTypes.HEADER_ONE -> ContentType.H1
|
||||
ContentTypes.HEADER_TWO -> ContentType.H2
|
||||
ContentTypes.HEADER_THREE -> ContentType.H3
|
||||
ContentTypes.NUMBERED_LIST -> ContentType.NumberedList
|
||||
ContentTypes.BULLET_LIST_ITEM -> ContentType.UL
|
||||
ContentTypes.QUOTE -> ContentType.Quote
|
||||
ContentTypes.TOGGLE -> ContentType.Toggle
|
||||
ContentTypes.CHECKBOX -> ContentType.Check
|
||||
ContentTypes.HEADER_FOUR -> ContentType.H4
|
||||
else -> throw IllegalStateException("Unexpected content type code: $this")
|
||||
}
|
||||
|
||||
fun ContentType.toNumericalCode(): Int =
|
||||
when (this) {
|
||||
ContentType.None -> ContentTypes.UNDEFINED
|
||||
ContentType.P -> ContentTypes.PARAGRAPH
|
||||
ContentType.Code -> ContentTypes.CODE_SNIPPET
|
||||
ContentType.H1 -> ContentTypes.HEADER_ONE
|
||||
ContentType.H2 -> ContentTypes.HEADER_TWO
|
||||
ContentType.H3 -> ContentTypes.HEADER_THREE
|
||||
ContentType.NumberedList -> ContentTypes.NUMBERED_LIST
|
||||
ContentType.UL -> ContentTypes.BULLET_LIST_ITEM
|
||||
ContentType.Quote -> ContentTypes.QUOTE
|
||||
ContentType.Toggle -> ContentTypes.TOGGLE
|
||||
ContentType.Check -> ContentTypes.CHECKBOX
|
||||
ContentType.H4 -> ContentTypes.HEADER_FOUR
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data.datasource
|
||||
|
||||
import android.content.Context
|
||||
import com.agileburo.anytype.feature_editor.BuildConfig
|
||||
import com.agileburo.anytype.feature_editor.data.BlockModel
|
||||
import com.google.gson.Gson
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 20.03.2019.
|
||||
*/
|
||||
interface BlockDataSource {
|
||||
fun getBlocks(): Single<List<BlockModel>>
|
||||
}
|
||||
|
||||
class IPFSDataSourceImpl @Inject constructor(
|
||||
private val context: Context,
|
||||
private val gson: Gson
|
||||
) : BlockDataSource {
|
||||
|
||||
override fun getBlocks(): Single<List<BlockModel>> {
|
||||
return Single.create<List<BlockModel>> { emitter ->
|
||||
try {
|
||||
val json = context.assets.open(BuildConfig.TEST_JSON).bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
|
||||
val ipfsResponse = gson.fromJson<IpfsResponse>(json, IpfsResponse::class.java)
|
||||
|
||||
emitter.onSuccess(ipfsResponse.blocks)
|
||||
|
||||
} catch (e: Exception) {
|
||||
emitter.onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class IpfsResponse(val blocks: List<BlockModel> = emptyList())
|
|
@ -1,28 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.data.parser
|
||||
|
||||
import com.agileburo.anytype.feature_editor.data.ContentModel
|
||||
import com.agileburo.anytype.feature_editor.domain.BlockType
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonElement
|
||||
|
||||
class ContentModelParser(val gson : Gson) {
|
||||
|
||||
fun parse(json : JsonElement, blockType : BlockType) : ContentModel {
|
||||
return when(blockType) {
|
||||
BlockType.Editable -> {
|
||||
gson.fromJson(json, ContentModel.Text::class.java)
|
||||
}
|
||||
BlockType.Page -> {
|
||||
gson.fromJson(json, ContentModel.Page::class.java)
|
||||
}
|
||||
BlockType.BookMark -> {
|
||||
gson.fromJson(json, ContentModel.Bookmark::class.java)
|
||||
}
|
||||
BlockType.Image -> {
|
||||
gson.fromJson(json, ContentModel.Image::class.java)
|
||||
}
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,496 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.domain
|
||||
|
||||
import com.agileburo.anytype.feature_editor.presentation.mapper.BlockViewMapper
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 14.03.2019.
|
||||
*/
|
||||
sealed class BlockType {
|
||||
object HrGrid : BlockType()
|
||||
object VrGrid : BlockType()
|
||||
object Editable : BlockType()
|
||||
object Divider : BlockType()
|
||||
object Video : BlockType()
|
||||
object Image : BlockType()
|
||||
object Page : BlockType()
|
||||
object NewPage : BlockType()
|
||||
object BookMark : BlockType()
|
||||
object File : BlockType()
|
||||
}
|
||||
|
||||
sealed class ContentType {
|
||||
object None : ContentType()
|
||||
object P : ContentType()
|
||||
object Code : ContentType()
|
||||
object H1 : ContentType()
|
||||
object H2 : ContentType()
|
||||
object H3 : ContentType()
|
||||
object NumberedList : ContentType()
|
||||
object UL : ContentType()
|
||||
object Quote : ContentType()
|
||||
object Toggle : ContentType()
|
||||
object Check : ContentType()
|
||||
object H4 : ContentType()
|
||||
}
|
||||
|
||||
typealias Document = MutableList<Block>
|
||||
|
||||
data class Block(
|
||||
val id: String,
|
||||
val parentId: String,
|
||||
val contentType: ContentType,
|
||||
val blockType: BlockType,
|
||||
val content: Content,
|
||||
val children : MutableList<Block> = mutableListOf(),
|
||||
val state : State = State.expanded(false)
|
||||
) {
|
||||
|
||||
fun isNumberedList() = contentType == ContentType.NumberedList
|
||||
|
||||
fun setNumber(number: Int) {
|
||||
if (content is Content.Text) content.param.number = number
|
||||
}
|
||||
|
||||
fun isConsumer(): Boolean = when (contentType) {
|
||||
ContentType.Toggle, ContentType.Check, ContentType.NumberedList, ContentType.UL -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun hasParent() = parentId.isNotEmpty()
|
||||
|
||||
fun isList() : Boolean {
|
||||
return when(contentType) {
|
||||
ContentType.NumberedList, ContentType.UL, ContentType.Check -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun isCheckbox() = contentType == ContentType.Check
|
||||
|
||||
fun isToggle() = contentType == ContentType.Toggle
|
||||
|
||||
data class State(val map : MutableMap<String, Any> = mutableMapOf()) {
|
||||
var expanded : Boolean by map
|
||||
var focused : Boolean by map
|
||||
|
||||
companion object {
|
||||
fun expanded(expanded : Boolean = false) = State(mutableMapOf("expanded" to expanded, "focused" to false))
|
||||
fun focused(focused : Boolean = false) = State(mutableMapOf("focused" to focused))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun new(parentId: String, contentType : ContentType) : Block {
|
||||
return Block(
|
||||
id = UUID.randomUUID().toString(),
|
||||
parentId = parentId,
|
||||
content = Content.Text(
|
||||
marks = emptyList(),
|
||||
text = "",
|
||||
param = ContentParam.empty()
|
||||
),
|
||||
blockType = BlockType.Editable,
|
||||
contentType = contentType,
|
||||
state = State.focused(true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens document, so that as result we get a flattened list.
|
||||
* @return list, in which all blocks do not have children (parent-child relations are indicated only via parentId)
|
||||
*/
|
||||
fun Document.flat() : List<Block> {
|
||||
val result = mutableListOf<Block>()
|
||||
|
||||
forEach { block ->
|
||||
result.add(block.copy(children = mutableListOf()))
|
||||
if (block.children.isNotEmpty()) result.addAll(block.children.flat())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows to transform a flattened list of block into a graph-like structured document.
|
||||
*/
|
||||
fun Document.graph() : Document {
|
||||
val map = this.associateBy { block -> block.id }.toMutableMap()
|
||||
|
||||
forEach { block ->
|
||||
if (block.parentId.isNotEmpty()) {
|
||||
map[block.parentId]?.let { parent ->
|
||||
parent.children.add(block)
|
||||
map[block.parentId] = parent
|
||||
map[block.id] = block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map.values.filter { block -> block.parentId.isEmpty() }.toMutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Search inside a flattened block list.
|
||||
*/
|
||||
fun Document.flatSearch(id : String) : Block? {
|
||||
return flat().find { block -> block.id == id }?.copy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Search inside a graph-like structured document.
|
||||
* @param id id of the block to search
|
||||
* @return returns a copy instance of the block, or null if it is not present inside the document
|
||||
*/
|
||||
fun Document.search(id : String) : Block? {
|
||||
forEach { block ->
|
||||
if (block.id == id) return block.copy()
|
||||
|
||||
if (block.children.isNotEmpty()) {
|
||||
val result = block.children.search(id)
|
||||
if (result != null) return result.copy()
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param targetId id of the block to update.
|
||||
* @param targetType new content type for the block that is being updated
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
fun Document.changeContentType(targetId : String, targetType: ContentType) {
|
||||
|
||||
search(targetId)?.let { target ->
|
||||
|
||||
if (target.contentType == targetType) return
|
||||
|
||||
if (target.parentId.isNotEmpty()) {
|
||||
|
||||
// Changing content type at children level
|
||||
|
||||
search(target.parentId)?.let { parent ->
|
||||
|
||||
val index = parent.children.indexOf(target)
|
||||
|
||||
parent.children[index] = target.copy(contentType = targetType)
|
||||
|
||||
// treat toggle block children move.
|
||||
|
||||
if (target.contentType == ContentType.Toggle) {
|
||||
|
||||
// move toggle block children to an upper level
|
||||
|
||||
if (target.children.isNotEmpty()) {
|
||||
|
||||
val children = target.children.map { it.copy(parentId = parent.id) }
|
||||
|
||||
// remove children from toggle block
|
||||
|
||||
target.children.clear()
|
||||
|
||||
// add children to their new parent (i.e. parent of the target block)
|
||||
|
||||
if (index < parent.children.size - 1) {
|
||||
val slice = slice(index + 1 until parent.children.size)
|
||||
parent.children.removeAll { child -> slice.contains(child) }
|
||||
parent.children.addAll(children + slice)
|
||||
} else {
|
||||
parent.children.addAll(children)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: throw IllegalStateException("Could not found parent by id : ${target.parentId}")
|
||||
|
||||
} else {
|
||||
|
||||
// Changing content type at root level
|
||||
|
||||
val index = indexOf(target)
|
||||
|
||||
set(index, target.copy(contentType = targetType))
|
||||
|
||||
// treat toggle block children move.
|
||||
|
||||
if (target.contentType == ContentType.Toggle) {
|
||||
|
||||
// move toggle block children to root level
|
||||
|
||||
if (target.children.isNotEmpty()) {
|
||||
|
||||
val children = target.children.map { it.copy(parentId = "") }
|
||||
|
||||
target.children.clear()
|
||||
|
||||
// add children to root revel
|
||||
|
||||
if (index < size - 1) {
|
||||
val slice = slice(index + 1 until size)
|
||||
removeAll { slice.contains(it) }
|
||||
addAll(children + slice)
|
||||
} else {
|
||||
addAll(children)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} ?: throw IllegalStateException("Could not find target by id: $targetId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes block by id.
|
||||
* @param targetId id of the block to delete
|
||||
*/
|
||||
fun Document.delete(targetId : String) {
|
||||
val block = search(targetId)
|
||||
|
||||
check(block != null) { "Could not found block with id : $targetId" }
|
||||
|
||||
if (block.parentId.isNotEmpty()) {
|
||||
val parent = search(block.parentId)
|
||||
check(parent != null) { "Could not found parent by id: ${block.parentId}" }
|
||||
parent.children.removeIf { it.id == block.id }
|
||||
} else {
|
||||
removeIf { it.id == block.id }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param targetId id of the block to update.
|
||||
* @param targetContentUpdate new content for the block that is being updated.
|
||||
*/
|
||||
fun Document.updateContent(targetId : String, targetContentUpdate : Content) {
|
||||
val current = search(targetId)
|
||||
if (current != null) {
|
||||
if (current.parentId.isNotEmpty()) {
|
||||
val parent = search(current.parentId)
|
||||
if (parent != null) {
|
||||
val index = parent.children.indexOf(current)
|
||||
parent.children[index] = current.copy(content = targetContentUpdate)
|
||||
}
|
||||
} else {
|
||||
val index = indexOfFirst { block -> block.id == targetId }
|
||||
set(index, current.copy(content = targetContentUpdate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes number order inside document.
|
||||
*/
|
||||
fun Document.fixNumberOrder() {
|
||||
var number = 0
|
||||
|
||||
forEach { block ->
|
||||
if (block.isNumberedList()) {
|
||||
number++
|
||||
block.setNumber(number)
|
||||
} else {
|
||||
number = 0
|
||||
}
|
||||
if (block.children.isNotEmpty()) {
|
||||
block.children.fixNumberOrder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param indent current indent (starting with 0)
|
||||
* @return a document representation adapted for rendering.
|
||||
*
|
||||
*/
|
||||
fun Document.toView(indent : Int = 0) : List<BlockView> {
|
||||
|
||||
val mapper = BlockViewMapper()
|
||||
|
||||
val result = mutableListOf<BlockView>()
|
||||
|
||||
forEach { block ->
|
||||
|
||||
result.add(mapper.mapToView(model = block, indent = indent))
|
||||
|
||||
if (block.isToggle()) {
|
||||
if (block.children.isNotEmpty() && block.state.expanded) {
|
||||
result.addAll(block.children.toView(indent.inc()))
|
||||
}
|
||||
} else if (block.isList()) {
|
||||
if (block.children.isNotEmpty()) {
|
||||
result.addAll(block.children.toView(indent.inc()))
|
||||
}
|
||||
} else {
|
||||
if (block.children.isNotEmpty()) {
|
||||
result.addAll(block.children.toView(indent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
fun Document.consume(consumerId : String, consumableId : String) {
|
||||
search(consumerId)?.let { consumer ->
|
||||
if (consumer.isConsumer())
|
||||
search(consumableId)?.let { consumable ->
|
||||
val parent = search(consumable.parentId)
|
||||
if (parent != null) {
|
||||
val index = parent.children.indexOf(consumable)
|
||||
parent.children.removeAt(index)
|
||||
consumer.children.add(consumable.copy(parentId = consumer.id))
|
||||
} else {
|
||||
val index = indexOf(consumable)
|
||||
removeAt(index)
|
||||
consumer.children.add(consumable.copy(parentId = consumer.id))
|
||||
}
|
||||
} ?: throw IllegalStateException("Could not find consumable with id: $consumableId")
|
||||
|
||||
} ?: throw IllegalStateException("Could not find consumer with id: $consumerId")
|
||||
|
||||
}
|
||||
|
||||
fun Document.moveAfter(previousId : String, targetId : String) {
|
||||
|
||||
search(previousId)?.let { previous ->
|
||||
|
||||
search(targetId)?.let { target ->
|
||||
|
||||
if (previous.hasParent()) {
|
||||
|
||||
search(previous.parentId)?.let { parent ->
|
||||
|
||||
if (target.hasParent() && target.parentId != parent.id) {
|
||||
search(target.parentId)?.let { targetParent ->
|
||||
targetParent.children.removeIf { child -> child.id == targetId }
|
||||
} ?: throw IllegalStateException("Could not found parent for target id: $targetId")
|
||||
} else {
|
||||
removeIf { block -> block.id == target.id }
|
||||
}
|
||||
|
||||
val result = mutableListOf<Block>()
|
||||
|
||||
parent.children.forEach { child ->
|
||||
if (child.id != targetId)
|
||||
result.add(child)
|
||||
if (child.id == previousId)
|
||||
result.add(target.copy(parentId = parent.id))
|
||||
}
|
||||
|
||||
parent.children.apply {
|
||||
clear()
|
||||
addAll(result)
|
||||
}
|
||||
|
||||
} ?: throw IllegalStateException("Could not find parent for previous block by parent id: ${previous.parentId}")
|
||||
|
||||
} else {
|
||||
|
||||
if (target.hasParent()) {
|
||||
|
||||
if (target.parentId != previous.parentId) {
|
||||
search(target.parentId)?.let { targetParent ->
|
||||
targetParent.children.removeIf { child -> child.id == targetId }
|
||||
} ?: throw IllegalStateException("Could not found parent for target id: $targetId")
|
||||
}
|
||||
|
||||
val result = mutableListOf<Block>()
|
||||
|
||||
forEach { child ->
|
||||
result.add(child)
|
||||
if (child.id == previousId)
|
||||
result.add(target.copy(parentId = previous.parentId))
|
||||
}
|
||||
|
||||
clear()
|
||||
addAll(result)
|
||||
|
||||
} else {
|
||||
|
||||
val result = mutableListOf<Block>()
|
||||
|
||||
forEach { child ->
|
||||
if (child.id != targetId)
|
||||
result.add(child)
|
||||
if (child.id == previousId)
|
||||
result.add(target.copy(parentId = previous.parentId))
|
||||
}
|
||||
|
||||
clear()
|
||||
addAll(result)
|
||||
}
|
||||
}
|
||||
|
||||
} ?: throw IllegalStateException("Could not find target block with id: $targetId")
|
||||
|
||||
} ?: throw IllegalStateException("Could not find previous block with id: $previousId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new block after given block according to block hierarchy.
|
||||
* @param previousBlockId id of the previous block (new block is inserted after this block)
|
||||
*/
|
||||
fun Document.insertNewBlockAfter(previousBlockId : String) {
|
||||
|
||||
search(previousBlockId)?.let { previous ->
|
||||
|
||||
val newContentType = if (previous.isCheckbox() || previous.isList()) previous.contentType else ContentType.P
|
||||
|
||||
val newBlock = Block.new(
|
||||
parentId = previous.parentId,
|
||||
contentType = newContentType
|
||||
)
|
||||
|
||||
if (previous.parentId.isNotEmpty()) {
|
||||
|
||||
search(previous.parentId)?.let { parent ->
|
||||
|
||||
val result = mutableListOf<Block>()
|
||||
|
||||
parent.children.forEach { block ->
|
||||
result.add(block)
|
||||
if (block.id == previousBlockId) result.add(newBlock)
|
||||
}
|
||||
|
||||
parent.children.apply {
|
||||
clear()
|
||||
addAll(result)
|
||||
}
|
||||
|
||||
} ?: throw IllegalStateException("Could not found parent for previous item with parent id: ${previous.parentId}")
|
||||
|
||||
} else {
|
||||
|
||||
val result = mutableListOf<Block>()
|
||||
|
||||
forEach { block ->
|
||||
result.add(block)
|
||||
if (block.id == previousBlockId) result.add(newBlock)
|
||||
}
|
||||
|
||||
this.clear()
|
||||
this.addAll(result)
|
||||
}
|
||||
|
||||
|
||||
} ?: throw IllegalStateException("Could not found previous block with id: $previousBlockId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies given action on every block of this document.
|
||||
* Only a variable field of a block can be modified by this method.
|
||||
* @param action action on block instance.
|
||||
*/
|
||||
fun Document.applyToAll(action : (Block) -> Unit) {
|
||||
forEach { block ->
|
||||
action(block)
|
||||
block.children.applyToAll(action)
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.domain
|
||||
|
||||
object BlockTypes {
|
||||
const val HORIZONTAL_GRID = 1
|
||||
const val VERTICAL_GRID = 2
|
||||
const val EDITABLE = 3
|
||||
const val DIVIDER = 4
|
||||
const val VIDEO = 5
|
||||
const val IMAGE = 6
|
||||
const val PAGE = 7
|
||||
const val NEW_PAGE = 8
|
||||
const val BOOKMARK = 9
|
||||
const val FILE = 10
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.domain
|
||||
|
||||
sealed class Content {
|
||||
|
||||
data class Text(
|
||||
val text : String,
|
||||
val param : ContentParam,
|
||||
val marks : List<Mark>
|
||||
) : Content()
|
||||
|
||||
data class Page(
|
||||
val id : String
|
||||
) : Content()
|
||||
|
||||
data class Bookmark(
|
||||
val type : String,
|
||||
val url : String,
|
||||
val title : String,
|
||||
val description : String,
|
||||
val site : String,
|
||||
val icon : String,
|
||||
val images : List<Image>
|
||||
) : Content() {
|
||||
|
||||
data class Image(val url : String)
|
||||
|
||||
}
|
||||
|
||||
@Deprecated("Picture will be downloaded from device or in some other way")
|
||||
data class Picture(
|
||||
val url : String,
|
||||
val type : Type
|
||||
) : Content() {
|
||||
enum class Type { ORIGINAL, THUMBNAIL }
|
||||
}
|
||||
|
||||
object Empty : Content()
|
||||
|
||||
|
||||
}
|
||||
|
||||
data class ContentParam(val map : MutableMap<String, Any?>) {
|
||||
var number : Int by map
|
||||
var checked : Boolean by map
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
fun checkbox(checked : Boolean) : ContentParam {
|
||||
return ContentParam(
|
||||
mutableMapOf(
|
||||
"number" to 0,
|
||||
"checked" to checked
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun empty() : ContentParam {
|
||||
return ContentParam(
|
||||
mutableMapOf(
|
||||
"number" to 0,
|
||||
"checked" to false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun numberedList(number : Int = 1): ContentParam {
|
||||
return ContentParam(
|
||||
mutableMapOf(
|
||||
"number" to number,
|
||||
"checked" to false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Mark(
|
||||
val start : Int,
|
||||
val end : Int,
|
||||
val type : MarkType,
|
||||
val param: String
|
||||
) {
|
||||
enum class MarkType {
|
||||
BOLD, ITALIC, UNDERLINE, STRIKE_THROUGH, HYPERTEXT, CODE, UNDEFINED
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.domain
|
||||
|
||||
object ContentTypes {
|
||||
const val UNDEFINED = 0
|
||||
const val PARAGRAPH = 1
|
||||
const val CODE_SNIPPET = 2
|
||||
const val HEADER_ONE = 3
|
||||
const val HEADER_TWO = 4
|
||||
const val HEADER_THREE = 5
|
||||
const val NUMBERED_LIST = 6
|
||||
const val BULLET_LIST_ITEM = 7
|
||||
const val QUOTE = 8
|
||||
const val TOGGLE = 9
|
||||
const val CHECKBOX = 10
|
||||
const val HEADER_FOUR = 11
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.domain
|
||||
|
||||
import com.agileburo.anytype.feature_editor.data.EditorRepo
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
interface EditorInteractor {
|
||||
|
||||
fun getBlocks(): Single<List<Block>>
|
||||
fun saveState(list: MutableList<Block>)
|
||||
}
|
||||
|
||||
class EditorInteractorImpl @Inject constructor(private val repo: EditorRepo) : EditorInteractor {
|
||||
|
||||
override fun getBlocks(): Single<List<Block>> =
|
||||
repo.getBlocks()
|
||||
.flattenAsObservable { blocks -> blocks }
|
||||
.toList()
|
||||
|
||||
override fun saveState(list: MutableList<Block>) {
|
||||
repo.saveState(list)
|
||||
}
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.presentation.converter
|
||||
|
||||
import com.agileburo.anytype.feature_editor.domain.Block
|
||||
import com.agileburo.anytype.feature_editor.domain.Content
|
||||
import com.agileburo.anytype.feature_editor.domain.ContentParam
|
||||
import com.agileburo.anytype.feature_editor.domain.ContentType
|
||||
|
||||
interface BlockContentTypeConverter {
|
||||
|
||||
/**
|
||||
* @param block block to convert
|
||||
* @param type content type for new block
|
||||
*/
|
||||
fun convert(block : Block, type : ContentType) : Block
|
||||
|
||||
/**
|
||||
* @param blocks list of blocks
|
||||
* @param target block that we need to convert
|
||||
* @param targetType type for a new block
|
||||
*/
|
||||
fun convert(blocks : List<Block>, target : Block, targetType : ContentType) : List<Block>
|
||||
|
||||
fun normalizeNumbers(blocks : List<Block>) : List<Block>
|
||||
|
||||
fun getPermittedTypes(typeInitial: ContentType): Set<ContentType>
|
||||
fun getForbiddenTypes(typeInitial: ContentType): Set<ContentType>
|
||||
}
|
||||
|
||||
class BlockContentTypeConverterImpl :
|
||||
BlockContentTypeConverter {
|
||||
|
||||
override
|
||||
fun getPermittedTypes(typeInitial: ContentType): Set<ContentType> =
|
||||
setOf(
|
||||
ContentType.P, ContentType.Code, ContentType.H1, ContentType.H2,
|
||||
ContentType.H3, ContentType.NumberedList, ContentType.UL, ContentType.Quote,
|
||||
ContentType.Toggle, ContentType.Check, ContentType.H4, ContentType.None
|
||||
)
|
||||
|
||||
//Если вдруг появятся недопустимые варианты для конвертации, добавлять можно здесь
|
||||
override fun getForbiddenTypes(typeInitial: ContentType): Set<ContentType> =
|
||||
when (typeInitial) {
|
||||
//Выключаем H1 при работе с блоком, используем H2, H2, H4
|
||||
ContentType.P -> setOf(ContentType.H1)
|
||||
else -> setOf(ContentType.H1)
|
||||
}
|
||||
|
||||
override fun convert(block: Block, type: ContentType): Block {
|
||||
return when (type) {
|
||||
ContentType.NumberedList -> {
|
||||
block.copy(
|
||||
contentType = type,
|
||||
content = (block.content as Content.Text).copy(param = ContentParam.numberedList())
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
block.copy(contentType = type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun convert(blocks: List<Block>, target: Block, targetType: ContentType): List<Block> {
|
||||
|
||||
if (target.contentType == targetType)
|
||||
return blocks
|
||||
else
|
||||
when (targetType) {
|
||||
|
||||
ContentType.NumberedList -> {
|
||||
|
||||
val result = mutableListOf<Block>()
|
||||
|
||||
blocks.forEach { block ->
|
||||
if (block.id == target.id) {
|
||||
val item = block.copy(
|
||||
contentType = targetType,
|
||||
content = (block.content as Content.Text).copy(
|
||||
param = ContentParam.numberedList()
|
||||
)
|
||||
)
|
||||
result.add(item)
|
||||
} else {
|
||||
result.add(block)
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeNumbers(result)
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
val result = blocks.toMutableList().also { result ->
|
||||
val index = blocks.indexOf(target)
|
||||
val converted = target.copy(contentType = targetType)
|
||||
result[index] = converted
|
||||
}
|
||||
|
||||
return normalizeNumbers(result)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun normalizeNumbers(blocks: List<Block>): List<Block> {
|
||||
|
||||
if (blocks.isEmpty())
|
||||
return emptyList()
|
||||
|
||||
val result = mutableListOf<Block>()
|
||||
|
||||
var number = 0
|
||||
var isPreviousNumbered = false
|
||||
|
||||
blocks.forEach { block ->
|
||||
if (block.contentType == ContentType.NumberedList) {
|
||||
|
||||
if (isPreviousNumbered) {
|
||||
number++
|
||||
block.setNumber(number)
|
||||
} else {
|
||||
number = 1
|
||||
block.setNumber(number)
|
||||
}
|
||||
|
||||
isPreviousNumbered = true
|
||||
|
||||
} else {
|
||||
block.setNumber(0)
|
||||
isPreviousNumbered = false
|
||||
}
|
||||
|
||||
result.add(block)
|
||||
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.presentation.mapper
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableString
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.URLSpan
|
||||
import com.agileburo.anytype.feature_editor.domain.*
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView.HeaderView.*
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 2019-05-15.
|
||||
*/
|
||||
|
||||
interface ModelMapper<in V, out D> {
|
||||
fun mapToModel(view: V): D
|
||||
}
|
||||
|
||||
class BlockModelMapper : ModelMapper<BlockView, Block> {
|
||||
|
||||
override fun mapToModel(view: BlockView): Block {
|
||||
//todo Разобраться с parentId
|
||||
|
||||
return when (view) {
|
||||
is BlockView.ParagraphView -> {
|
||||
Block(
|
||||
id = view.id,
|
||||
parentId = "test",
|
||||
contentType = ContentType.P,
|
||||
content = Content.Text(
|
||||
text = view.text.toString(),
|
||||
marks = fromSpannableToMarks(view.text),
|
||||
param = ContentParam.empty()
|
||||
),
|
||||
blockType = BlockType.Editable
|
||||
)
|
||||
}
|
||||
is BlockView.QuoteView -> {
|
||||
Block(
|
||||
id = view.id,
|
||||
parentId = "",
|
||||
contentType = ContentType.Quote,
|
||||
content = Content.Text(
|
||||
text = view.text.toString(),
|
||||
marks = fromSpannableToMarks(view.text),
|
||||
param = ContentParam.empty()
|
||||
),
|
||||
blockType = BlockType.Editable
|
||||
)
|
||||
}
|
||||
is BlockView.CodeSnippetView -> {
|
||||
Block(
|
||||
id = view.id,
|
||||
parentId = "",
|
||||
contentType = ContentType.Code,
|
||||
content = Content.Text(
|
||||
text = view.text.toString(),
|
||||
marks = fromSpannableToMarks(view.text),
|
||||
param = ContentParam.empty()
|
||||
),
|
||||
blockType = BlockType.Editable
|
||||
)
|
||||
}
|
||||
is BlockView.CheckboxView -> {
|
||||
Block(
|
||||
id = view.id,
|
||||
parentId = "",
|
||||
contentType = ContentType.Check,
|
||||
content = Content.Text(
|
||||
text = view.text.toString(),
|
||||
marks = fromSpannableToMarks(view.text),
|
||||
param = ContentParam.checkbox(view.isChecked)
|
||||
),
|
||||
blockType = BlockType.Editable
|
||||
)
|
||||
}
|
||||
is BlockView.HeaderView -> {
|
||||
|
||||
val contentType = when(view.type) {
|
||||
HeaderType.ONE -> ContentType.H1
|
||||
HeaderType.TWO -> ContentType.H2
|
||||
HeaderType.THREE -> ContentType.H3
|
||||
HeaderType.FOUR -> ContentType.H4
|
||||
}
|
||||
|
||||
Block(
|
||||
id = view.id,
|
||||
parentId = "",
|
||||
contentType = contentType,
|
||||
content = Content.Text(
|
||||
text = view.text.toString(),
|
||||
marks = fromSpannableToMarks(view.text),
|
||||
param = ContentParam.empty()
|
||||
),
|
||||
blockType = BlockType.Editable
|
||||
)
|
||||
}
|
||||
|
||||
is BlockView.BulletView -> {
|
||||
Block(
|
||||
id = view.id,
|
||||
parentId = "",
|
||||
contentType = ContentType.UL,
|
||||
content = Content.Text(
|
||||
text = view.text.toString(),
|
||||
marks = fromSpannableToMarks(view.text),
|
||||
param = ContentParam.empty()
|
||||
),
|
||||
blockType = BlockType.Editable
|
||||
)
|
||||
}
|
||||
is BlockView.NumberListItemView -> {
|
||||
Block(
|
||||
id = view.id,
|
||||
parentId = "",
|
||||
contentType = ContentType.NumberedList,
|
||||
content = Content.Text(
|
||||
text = view.text.toString(),
|
||||
marks = fromSpannableToMarks(view.text),
|
||||
param = ContentParam.numberedList(view.number)
|
||||
),
|
||||
blockType = BlockType.Editable
|
||||
)
|
||||
}
|
||||
|
||||
is BlockView.ToggleView -> {
|
||||
Block(
|
||||
id = view.id,
|
||||
parentId = "lost",
|
||||
contentType = ContentType.Toggle,
|
||||
content = Content.Text(
|
||||
text = view.text.toString(),
|
||||
marks = fromSpannableToMarks(view.text),
|
||||
param = ContentParam.empty()
|
||||
),
|
||||
blockType = BlockType.Editable
|
||||
)
|
||||
}
|
||||
|
||||
else -> TODO()
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun fromSpannableToMarks(content: CharSequence): List<Mark> {
|
||||
val text = SpannableString(content)
|
||||
val marks = ArrayList<Mark>()
|
||||
text.getSpans(0, text.length, StyleSpan::class.java).forEach {
|
||||
val start = text.getSpanStart(it)
|
||||
val end = text.getSpanEnd(it)
|
||||
if (start <= end) {
|
||||
marks.add(Mark(start = start, end = end, param = "", type = getStyleMarkType(it.style)))
|
||||
}
|
||||
}
|
||||
text.getSpans(0, text.length, StrikethroughSpan::class.java).forEach {
|
||||
val start = text.getSpanStart(it)
|
||||
val end = text.getSpanEnd(it)
|
||||
if (start <= end) {
|
||||
marks.add(Mark(start = start, end = end, param = "", type = Mark.MarkType.STRIKE_THROUGH))
|
||||
}
|
||||
}
|
||||
text.getSpans(0, text.length, URLSpan::class.java).forEach {
|
||||
val start = text.getSpanStart(it)
|
||||
val end = text.getSpanEnd(it)
|
||||
if (start <= end) {
|
||||
marks.add(Mark(start = start, end = end, param = it.url, type = Mark.MarkType.HYPERTEXT))
|
||||
}
|
||||
}
|
||||
return marks
|
||||
}
|
||||
|
||||
private fun getStyleMarkType(style: Int) =
|
||||
when (style) {
|
||||
Typeface.BOLD -> Mark.MarkType.BOLD
|
||||
Typeface.ITALIC -> Mark.MarkType.ITALIC
|
||||
else -> Mark.MarkType.UNDEFINED
|
||||
}
|
||||
}
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.presentation.mapper
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.view.View
|
||||
import com.agileburo.anytype.feature_editor.domain.*
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView
|
||||
import com.agileburo.anytype.feature_editor.ui.CodeBlockSpan
|
||||
|
||||
interface ViewMapper<in D, out V> {
|
||||
fun mapToView(model: D, indent : Int = 0): V
|
||||
}
|
||||
|
||||
class BlockViewMapper : ViewMapper<Block, BlockView> {
|
||||
|
||||
override fun mapToView(model: Block, indent : Int): BlockView {
|
||||
return when (model.blockType) {
|
||||
is BlockType.Editable -> {
|
||||
when (model.contentType) {
|
||||
is ContentType.P -> {
|
||||
BlockView.ParagraphView(
|
||||
indent = indent,
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.H1 -> {
|
||||
BlockView.HeaderView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
type = BlockView.HeaderView.HeaderType.ONE,
|
||||
indent = indent,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.H2 -> {
|
||||
BlockView.HeaderView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
type = BlockView.HeaderView.HeaderType.TWO,
|
||||
indent = indent,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.H3 -> {
|
||||
BlockView.HeaderView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
type = BlockView.HeaderView.HeaderType.THREE,
|
||||
indent = indent,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.H4 -> {
|
||||
BlockView.HeaderView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
type = BlockView.HeaderView.HeaderType.FOUR,
|
||||
indent = indent,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.Quote -> {
|
||||
BlockView.QuoteView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
indent = indent,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.Code -> {
|
||||
BlockView.CodeSnippetView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
indent = indent,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.Check -> {
|
||||
BlockView.CheckboxView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
isChecked = model.content.param.checked,
|
||||
indent = indent,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.NumberedList -> {
|
||||
BlockView.NumberListItemView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
number = model.content.param.number,
|
||||
indent = indent,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.UL -> {
|
||||
BlockView.BulletView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
indent = indent,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
is ContentType.Toggle -> {
|
||||
BlockView.ToggleView(
|
||||
id = model.id,
|
||||
text = fromMarksToSpannable(
|
||||
marks = (model.content as Content.Text).marks,
|
||||
text = model.content.text
|
||||
),
|
||||
indent = indent,
|
||||
expanded = model.state.expanded,
|
||||
focused = model.state.focused
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
throw NotImplementedError("${model.contentType} is not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
is BlockType.Page -> {
|
||||
BlockView.LinkToPageView(
|
||||
id = model.id,
|
||||
title = (model.content as Content.Page).id
|
||||
)
|
||||
}
|
||||
is BlockType.BookMark -> {
|
||||
BlockView.BookmarkView(
|
||||
id = model.id,
|
||||
title = (model.content as Content.Bookmark).title,
|
||||
description = model.content.description,
|
||||
url = model.content.url,
|
||||
image = model.content.images.first().url,
|
||||
indent = indent
|
||||
)
|
||||
}
|
||||
is BlockType.Divider -> {
|
||||
BlockView.DividerView(
|
||||
id = model.id,
|
||||
indent = indent
|
||||
)
|
||||
}
|
||||
is BlockType.Image -> {
|
||||
BlockView.PictureView(
|
||||
id = model.id,
|
||||
url = (model.content as Content.Picture).url,
|
||||
indent = indent
|
||||
)
|
||||
}
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fromMarksToSpannable(text: String, marks: List<Mark>) =
|
||||
SpannableString(text).apply {
|
||||
marks.forEach {
|
||||
when (it.type) {
|
||||
Mark.MarkType.BOLD -> setSpan(
|
||||
StyleSpan(Typeface.BOLD),
|
||||
it.start,
|
||||
it.end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
Mark.MarkType.ITALIC -> setSpan(
|
||||
StyleSpan(Typeface.ITALIC),
|
||||
it.start,
|
||||
it.end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
Mark.MarkType.STRIKE_THROUGH -> setSpan(
|
||||
StrikethroughSpan(),
|
||||
it.start,
|
||||
it.end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
Mark.MarkType.CODE -> setSpan(
|
||||
CodeBlockSpan(Typeface.DEFAULT),
|
||||
it.start,
|
||||
it.end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
Mark.MarkType.HYPERTEXT -> {
|
||||
setSpan(URLSpan(it.param), it.start, it.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
//TODO решить задачу EditText + Clickable UrlSpan
|
||||
// val clickableSpan = object : ClickableSpan() {
|
||||
// override fun onClick(widget: View?) {
|
||||
// //onClickListener.invoke()
|
||||
// Timber.d("On link clicked !!!")
|
||||
// }
|
||||
// }
|
||||
// setSpan(clickableSpan, it.start, it.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
// val method = BetterLinkMovementMethod.getInstance()
|
||||
// textView.movementMethod = method
|
||||
// textView.setOnTouchListener { _, event ->
|
||||
// method.onTouchEvent(textView, textView.text as Spannable, event)
|
||||
// || itemView.onTouchEvent(event)
|
||||
// }
|
||||
// withClickableSpan(it.param, it.start.toInt(), it.end.toInt(), click)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Not supported type of marks : ${it.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SpannableString.withClickableSpan(clickablePart: String, onClickListener: () -> Unit): SpannableString {
|
||||
val clickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) = onClickListener.invoke()
|
||||
}
|
||||
val clickablePartStart = indexOf(clickablePart)
|
||||
setSpan(
|
||||
clickableSpan,
|
||||
clickablePartStart,
|
||||
clickablePartStart + clickablePart.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
return this
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.presentation.model
|
||||
|
||||
import android.text.SpannableString
|
||||
|
||||
sealed class BlockView {
|
||||
|
||||
abstract val id : String
|
||||
var isSelected: Boolean = false
|
||||
|
||||
data class ParagraphView(
|
||||
override val id : String,
|
||||
override val indent : Int = 0,
|
||||
override var text : SpannableString,
|
||||
override val focused: Boolean
|
||||
) : BlockView(), Editable, Indentable, Focusable
|
||||
|
||||
data class HeaderView(
|
||||
override val id : String,
|
||||
override var text : SpannableString,
|
||||
val type : HeaderType,
|
||||
override val indent: Int = 0,
|
||||
override val focused: Boolean
|
||||
) : BlockView(), Editable, Indentable, Focusable {
|
||||
enum class HeaderType { ONE, TWO, THREE, FOUR }
|
||||
}
|
||||
|
||||
data class QuoteView(
|
||||
override val id : String,
|
||||
override var text : SpannableString,
|
||||
override val indent : Int = 0,
|
||||
override val focused: Boolean
|
||||
) : BlockView(), Editable, Indentable, Focusable
|
||||
|
||||
data class CheckboxView(
|
||||
override val id : String,
|
||||
override var text : SpannableString,
|
||||
override val indent: Int = 0,
|
||||
override val focused: Boolean,
|
||||
val isChecked : Boolean
|
||||
) : BlockView(), Editable, Indentable, Focusable
|
||||
|
||||
data class CodeSnippetView(
|
||||
override val id : String,
|
||||
override var text : SpannableString,
|
||||
override val indent : Int = 0,
|
||||
override val focused: Boolean
|
||||
) : BlockView(), Editable, Indentable, Focusable
|
||||
|
||||
data class NumberListItemView(
|
||||
override val id : String,
|
||||
override var text: SpannableString,
|
||||
override val indent : Int = 0,
|
||||
override val focused: Boolean,
|
||||
val number : Int
|
||||
) : BlockView(), Editable, Indentable, Focusable
|
||||
|
||||
data class BulletView(
|
||||
override val id : String,
|
||||
override var text: SpannableString,
|
||||
override val indent : Int = 0,
|
||||
override val focused: Boolean
|
||||
) : BlockView(), Editable, Indentable, Focusable
|
||||
|
||||
data class LinkToPageView(
|
||||
override val id : String,
|
||||
val title : String,
|
||||
override val indent : Int = 0
|
||||
) : BlockView(), Indentable
|
||||
|
||||
data class BookmarkView(
|
||||
override val id : String,
|
||||
val title : String,
|
||||
val description : String,
|
||||
val url : String,
|
||||
val image : String,
|
||||
override val indent : Int = 0
|
||||
) : BlockView(), Indentable
|
||||
|
||||
data class DividerView(
|
||||
override val id : String,
|
||||
override val indent : Int = 0
|
||||
) : BlockView(), Indentable
|
||||
|
||||
data class PictureView (
|
||||
override val id : String,
|
||||
val url : String,
|
||||
override val indent : Int = 0
|
||||
) : BlockView(), Indentable
|
||||
|
||||
data class ToggleView(
|
||||
override val id : String,
|
||||
override val indent : Int = 0,
|
||||
override var text : SpannableString,
|
||||
val expanded : Boolean = false,
|
||||
override val focused: Boolean
|
||||
) : BlockView(), Editable, Indentable, Focusable
|
||||
|
||||
interface Editable {
|
||||
var text : SpannableString
|
||||
}
|
||||
|
||||
interface Indentable {
|
||||
val indent : Int
|
||||
}
|
||||
|
||||
interface Focusable {
|
||||
val focused : Boolean
|
||||
}
|
||||
|
||||
interface Consumer
|
||||
}
|
|
@ -1,172 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.presentation.mvvm
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.agileburo.anytype.core_utils.ext.BaseSchedulerProvider
|
||||
import com.agileburo.anytype.core_utils.ext.shift
|
||||
import com.agileburo.anytype.feature_editor.disposedBy
|
||||
import com.agileburo.anytype.feature_editor.domain.*
|
||||
import com.agileburo.anytype.feature_editor.presentation.converter.BlockContentTypeConverter
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView
|
||||
import com.agileburo.anytype.feature_editor.presentation.util.DragDropAction
|
||||
import com.agileburo.anytype.feature_editor.ui.BlockMenuAction
|
||||
import com.agileburo.anytype.feature_editor.ui.EditorState
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import timber.log.Timber
|
||||
|
||||
class EditorViewModel(
|
||||
private val interactor: EditorInteractor,
|
||||
private val contentTypeConverter: BlockContentTypeConverter,
|
||||
private val schedulerProvider: BaseSchedulerProvider
|
||||
) : ViewModel() {
|
||||
|
||||
private val subscriptions by lazy { CompositeDisposable() }
|
||||
private val document : Document by lazy { mutableListOf<Block>() }
|
||||
private val progress by lazy { BehaviorRelay.create<EditorState>() }
|
||||
|
||||
private var positionInFocus: Int = -1
|
||||
|
||||
init {
|
||||
fetchBlocks()
|
||||
}
|
||||
|
||||
fun observeState() = progress
|
||||
|
||||
fun onBlockContentChanged(block: Block) {
|
||||
document.updateContent(
|
||||
targetId = block.id,
|
||||
targetContentUpdate = block.content
|
||||
)
|
||||
}
|
||||
|
||||
fun onExpandClicked(view : BlockView) {
|
||||
check(view is BlockView.ToggleView)
|
||||
|
||||
document.flatSearch(view.id)?.let { block ->
|
||||
block.state.expanded = !view.expanded
|
||||
dispatchBlocksToView()
|
||||
}
|
||||
}
|
||||
|
||||
fun onBlockMenuAction(action: BlockMenuAction) {
|
||||
when (action) {
|
||||
is BlockMenuAction.ContentTypeAction -> {
|
||||
document.changeContentType(targetId = action.id, targetType = action.newType)
|
||||
document.fixNumberOrder()
|
||||
dispatchBlocksToView()
|
||||
}
|
||||
is BlockMenuAction.ArchiveAction -> {
|
||||
removeBlock(action.id)
|
||||
}
|
||||
is BlockMenuAction.DuplicateAction -> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBlockDragAndDropAction(action: DragDropAction) = when (action) {
|
||||
is DragDropAction.Shift -> onShiftAction(from = action.from, to = action.to)
|
||||
is DragDropAction.Consume -> onConsumeAction(target = action.target, consumer = action.consumer)
|
||||
}
|
||||
|
||||
private fun onShiftAction(from: Int, to: Int) {
|
||||
val newBlocks = document.shift(from, to)
|
||||
document.clear()
|
||||
document.addAll(newBlocks)
|
||||
dispatchBlocksToView()
|
||||
normalizeBlocks()
|
||||
}
|
||||
|
||||
//Todo Update with proper consume action
|
||||
private fun onConsumeAction(target: Int, consumer: Int) {
|
||||
document.removeAt(target)
|
||||
progress.accept(EditorState.Remove(target))
|
||||
normalizeBlocks()
|
||||
}
|
||||
|
||||
fun onConsumeRequested(consumerId : String, consumableId : String) {
|
||||
document.apply {
|
||||
consume(consumerId = consumerId, consumableId = consumableId)
|
||||
fixNumberOrder()
|
||||
}
|
||||
dispatchBlocksToView()
|
||||
}
|
||||
|
||||
fun onMoveAfter(previousId : String, targetId : String) {
|
||||
document.apply {
|
||||
moveAfter(previousId, targetId)
|
||||
fixNumberOrder()
|
||||
}
|
||||
dispatchBlocksToView()
|
||||
}
|
||||
|
||||
fun onBlockFocus(position: Int) {
|
||||
if (position == -1) {
|
||||
positionInFocus = position
|
||||
clearBlockFocus()
|
||||
return
|
||||
}
|
||||
progress.accept(EditorState.DragDropOff)
|
||||
positionInFocus = position
|
||||
}
|
||||
|
||||
fun onEnterClicked(id : String) {
|
||||
|
||||
Timber.d("On enter clicked with id: $id")
|
||||
|
||||
document.apply {
|
||||
applyToAll { it.state.focused = false }
|
||||
insertNewBlockAfter(previousBlockId = id)
|
||||
fixNumberOrder()
|
||||
}
|
||||
dispatchBlocksToView()
|
||||
}
|
||||
|
||||
|
||||
private fun normalizeBlocks() {
|
||||
val normalized = contentTypeConverter.normalizeNumbers(document)
|
||||
document.clear()
|
||||
document.addAll(normalized)
|
||||
dispatchBlocksToView()
|
||||
}
|
||||
|
||||
private fun clearBlockFocus() =
|
||||
document.getOrNull(positionInFocus)?.let {
|
||||
progress.accept(EditorState.DragDropOn)
|
||||
progress.accept(
|
||||
EditorState.ClearBlockFocus(positionInFocus, it.contentType)
|
||||
)
|
||||
}
|
||||
|
||||
private fun fetchBlocks() {
|
||||
interactor.getBlocks()
|
||||
.observeOn(schedulerProvider.ui())
|
||||
.subscribeOn(schedulerProvider.io())
|
||||
.subscribe(
|
||||
{ data -> onBlockReceived(data) },
|
||||
{ error -> Timber.e(error, "Error while fetching document") }
|
||||
).disposedBy(subscriptions)
|
||||
}
|
||||
|
||||
private fun onBlockReceived(items: List<Block>) {
|
||||
document.addAll(items)
|
||||
progress.accept(EditorState.Result(document))
|
||||
}
|
||||
|
||||
private fun removeBlock(id: String) {
|
||||
document.apply {
|
||||
delete(id)
|
||||
fixNumberOrder()
|
||||
}
|
||||
dispatchBlocksToView()
|
||||
}
|
||||
|
||||
private fun dispatchBlocksToView() {
|
||||
progress.accept(EditorState.Updates(document))
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
subscriptions.clear()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.presentation.mvvm
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.agileburo.anytype.core_utils.ext.BaseSchedulerProvider
|
||||
import com.agileburo.anytype.feature_editor.domain.EditorInteractor
|
||||
import com.agileburo.anytype.feature_editor.presentation.converter.BlockContentTypeConverter
|
||||
|
||||
class EditorViewModelFactory(
|
||||
private val editorInteractor: EditorInteractor,
|
||||
private val contentTypeConverter: BlockContentTypeConverter,
|
||||
private val schedulerProvider: BaseSchedulerProvider
|
||||
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
|
||||
EditorViewModel(
|
||||
interactor = editorInteractor,
|
||||
contentTypeConverter = contentTypeConverter,
|
||||
schedulerProvider = schedulerProvider
|
||||
) as T
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.presentation.util
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView
|
||||
|
||||
class BlockViewDiffUtil(
|
||||
private val old : List<BlockView>,
|
||||
private val new : List<BlockView>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return old[oldItemPosition].id == new[newItemPosition].id
|
||||
}
|
||||
|
||||
override fun getOldListSize(): Int = old.size
|
||||
override fun getNewListSize(): Int = new.size
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return new[newItemPosition] == old[oldItemPosition]
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.presentation.util
|
||||
|
||||
sealed class DragDropAction {
|
||||
data class Consume(val target: Int, val consumer: Int) : DragDropAction()
|
||||
data class Shift(val from: Int, val to: Int) : DragDropAction()
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.presentation.util
|
||||
|
||||
data class SwapRequest(
|
||||
val from : Int,
|
||||
val to : Int
|
||||
)
|
|
@ -1,127 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupWindow
|
||||
import com.agileburo.anytype.feature_editor.R
|
||||
import com.agileburo.anytype.feature_editor.domain.ContentType
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 2019-05-20.
|
||||
*/
|
||||
|
||||
|
||||
sealed class BlockMenuAction{
|
||||
data class ContentTypeAction(val id : String, val newType: ContentType): BlockMenuAction()
|
||||
data class ArchiveAction(val id : String): BlockMenuAction()
|
||||
data class DuplicateAction(val id: String): BlockMenuAction()
|
||||
}
|
||||
|
||||
const val UNDEFINED_BUTTON = -1
|
||||
|
||||
class BlockMenu(
|
||||
private val context: Context,
|
||||
private val block: BlockView,
|
||||
private val menuItemClick: (BlockMenuAction) -> Unit
|
||||
) : PopupWindow(context) {
|
||||
|
||||
init {
|
||||
setupView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
val view = LayoutInflater.from(context).inflate(getLayout(block), null)
|
||||
view.findViewById<ImageView>(getButton(block))?.isSelected = true
|
||||
isOutsideTouchable = true
|
||||
isFocusable = true
|
||||
contentView = view
|
||||
setClicks()
|
||||
}
|
||||
|
||||
private fun setClicks() {
|
||||
contentView.findViewById<View>(R.id.btn_menu_p)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.P))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_h2)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.H2))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_h3)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.H3))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_h4)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.H4))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_bullet)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.UL))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_quote)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.Quote))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_numbered)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.NumberedList))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_checkbox)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.Check))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_code)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.Code))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_archive)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ArchiveAction(block.id))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_duplicate)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.DuplicateAction(block.id))
|
||||
dismiss()
|
||||
}
|
||||
contentView.findViewById<View>(R.id.btn_menu_toggle)?.setOnClickListener {
|
||||
menuItemClick.invoke(BlockMenuAction.ContentTypeAction(block.id, ContentType.Toggle))
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLayout(block: BlockView): Int =
|
||||
when (block) {
|
||||
is BlockView.Editable -> R.layout.popup_edit_block
|
||||
else -> R.layout.popup_non_edit_block
|
||||
}
|
||||
|
||||
private fun getButton(block: BlockView) =
|
||||
when (block) {
|
||||
is BlockView.ParagraphView -> R.id.btn_menu_p
|
||||
is BlockView.HeaderView -> {
|
||||
when (block.type) {
|
||||
BlockView.HeaderView.HeaderType.ONE -> UNDEFINED_BUTTON
|
||||
BlockView.HeaderView.HeaderType.TWO -> R.id.btn_menu_h2
|
||||
BlockView.HeaderView.HeaderType.THREE -> R.id.btn_menu_h3
|
||||
BlockView.HeaderView.HeaderType.FOUR -> R.id.btn_menu_h4
|
||||
}
|
||||
}
|
||||
is BlockView.BulletView -> R.id.btn_menu_bullet
|
||||
is BlockView.QuoteView -> R.id.btn_menu_quote
|
||||
is BlockView.NumberListItemView -> R.id.btn_menu_numbered
|
||||
is BlockView.CheckboxView -> R.id.btn_menu_checkbox
|
||||
is BlockView.CodeSnippetView -> R.id.btn_menu_code
|
||||
is BlockView.ToggleView -> R.id.btn_menu_toggle
|
||||
|
||||
//Todo implement!
|
||||
is BlockView.LinkToPageView -> throw NotImplementedError()
|
||||
is BlockView.BookmarkView -> throw NotImplementedError()
|
||||
is BlockView.DividerView -> throw NotImplementedError()
|
||||
is BlockView.PictureView -> throw NotImplementedError()
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
|
||||
class ClearFocusEditText : AppCompatEditText {
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyle
|
||||
)
|
||||
|
||||
override fun onKeyPreIme(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
clearFocus()
|
||||
}
|
||||
return super.onKeyPreIme(keyCode, event)
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import androidx.annotation.ColorInt
|
||||
import android.text.TextPaint
|
||||
import com.agileburo.anytype.feature_editor.ui.FontSpan
|
||||
|
||||
class CodeBlockSpan(
|
||||
font: Typeface?,
|
||||
@param:ColorInt private val backgroundColor: Int = Color.LTGRAY
|
||||
) : FontSpan(font) {
|
||||
|
||||
// Since we're only changing the background color, it will not affect the measure state, so
|
||||
// just override the updateContent draw state.
|
||||
override fun updateDrawState(textPaint: TextPaint) {
|
||||
super.updateDrawState(textPaint)
|
||||
textPaint.bgColor = backgroundColor
|
||||
}
|
||||
}
|
|
@ -1,349 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.*
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.agileburo.anytype.feature_editor.presentation.model.BlockView
|
||||
import com.agileburo.anytype.feature_editor.presentation.util.DragDropAction
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 2019-08-01.
|
||||
*/
|
||||
|
||||
interface ItemTouchHelperViewHolder {
|
||||
|
||||
fun targetView()
|
||||
fun targetViewBottom()
|
||||
fun clearTargetView()
|
||||
}
|
||||
|
||||
sealed class DragState {
|
||||
|
||||
/** Send Shift action. */
|
||||
data class Shift(val from: RecyclerView.ViewHolder) : DragState()
|
||||
|
||||
/** The opportunity to produce Shift action. Remember shiftView for the future. Render view. */
|
||||
data class ShiftRequest(val from: RecyclerView.ViewHolder, val to: RecyclerView.ViewHolder) : DragState()
|
||||
|
||||
/** Delete shiftView */
|
||||
object DeleteShiftRequest : DragState()
|
||||
|
||||
/** Send Consume action. */
|
||||
data class Consume(val target: RecyclerView.ViewHolder) : DragState()
|
||||
|
||||
/** The opportunity to produce Consume action. Remember consumerView for the future. */
|
||||
data class ConsumeRequest(val target: Int, val consumerViewHolder: RecyclerView.ViewHolder?) : DragState()
|
||||
|
||||
/** The user drag element. */
|
||||
data class Dragging(val target: Int) : DragState()
|
||||
|
||||
/** The element is not controlled by user and simply animating back to its original state. */
|
||||
data class DraggingNotActive(val view: View, val alpha: Float) : DragState()
|
||||
|
||||
/** The user interaction with an element is over and it also completed its animation. */
|
||||
object Idle : DragState()
|
||||
}
|
||||
|
||||
const val CONSUME_INTERVAL = 7
|
||||
const val DRAG_STATE_ALPHA = 0.3f
|
||||
const val CONSUME_STATE_ALPHA = 0.0f
|
||||
const val IDLE_STATE_ALPHA = 1.0f
|
||||
const val POSITION_NONE = -1
|
||||
|
||||
class DragAndDropBehavior(
|
||||
private val onDragDropAction: (DragDropAction) -> Unit
|
||||
) : ItemTouchHelper.Callback() {
|
||||
|
||||
private val disposable = CompositeDisposable()
|
||||
private val subject: BehaviorRelay<DragState> = BehaviorRelay.createDefault(DragState.Idle)
|
||||
private var shiftView: RecyclerView.ViewHolder? = null
|
||||
private var consumerPosition: Int = POSITION_NONE
|
||||
|
||||
fun init() = disposable.addAll(subject.subscribe { handleState(it) })
|
||||
fun destroy() = disposable.clear()
|
||||
|
||||
override fun onSwiped(p0: RecyclerView.ViewHolder, p1: Int) {}
|
||||
override fun getMovementFlags(p0: RecyclerView, p1: RecyclerView.ViewHolder): Int =
|
||||
makeMovementFlags(UP or DOWN, 0)
|
||||
|
||||
/**
|
||||
* The user has removed the finger from the screen.
|
||||
* Check for null viewHolders for Shift or Consume behavior.
|
||||
* First check for consume action
|
||||
*/
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
removeDraggableViewProperties(viewHolder.itemView)
|
||||
if (consumerPosition > POSITION_NONE) {
|
||||
subject.accept(DragState.Consume(target = viewHolder))
|
||||
return
|
||||
}
|
||||
if (shiftView != null) {
|
||||
subject.accept(DragState.Shift(from = viewHolder))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fraction that the user should move the View to be considered as it is
|
||||
* dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views
|
||||
* below it for a possible drop.
|
||||
*
|
||||
* @param viewHolder The ViewHolder that is being dragged.
|
||||
* @return A float value that denotes the fraction of the View size. Dragging value is
|
||||
* .5f .
|
||||
*/
|
||||
override fun getMoveThreshold(viewHolder: RecyclerView.ViewHolder): Float = 0.1f
|
||||
|
||||
override fun getAnimationDuration(
|
||||
recyclerView: RecyclerView,
|
||||
animationType: Int,
|
||||
animateDx: Float,
|
||||
animateDy: Float
|
||||
): Long = 0
|
||||
|
||||
override fun isLongPressDragEnabled(): Boolean = true
|
||||
override fun isItemViewSwipeEnabled(): Boolean = false
|
||||
|
||||
override fun canDropOver(
|
||||
recyclerView: RecyclerView,
|
||||
current: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean = target.adapterPosition != 0
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
subject.accept(DragState.ShiftRequest(viewHolder, target))
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onChildDraw(
|
||||
c: Canvas,
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
dX: Float,
|
||||
dY: Float,
|
||||
actionState: Int,
|
||||
isCurrentlyActive: Boolean
|
||||
) {
|
||||
when (actionState) {
|
||||
ACTION_STATE_DRAG -> isCurrentlyActive(
|
||||
recyclerView = recyclerView, dY = dY,
|
||||
viewHolder = viewHolder, isCurrentlyActive = isCurrentlyActive
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
}
|
||||
|
||||
override fun chooseDropTarget(
|
||||
selected: RecyclerView.ViewHolder,
|
||||
dropTargets: MutableList<RecyclerView.ViewHolder>,
|
||||
curX: Int,
|
||||
curY: Int
|
||||
): RecyclerView.ViewHolder? {
|
||||
var winner: RecyclerView.ViewHolder? = null
|
||||
var winnerScore = Int.MAX_VALUE
|
||||
val dy = curY - selected.itemView.top
|
||||
dropTargets.forEach { dropTarget ->
|
||||
with(dropTarget) {
|
||||
if (this is BlockView.Consumer) {
|
||||
val selectedTargetCenter = getSelectedTargetCenter(curY, selected.itemView.height)
|
||||
when {
|
||||
dy < 0 -> {
|
||||
val diff = getConsumeTopBorder(itemView.top, itemView.bottom) - selectedTargetCenter
|
||||
if (diff > 0 && abs(diff) < winnerScore) {
|
||||
winnerScore = abs(diff)
|
||||
winner = this
|
||||
}
|
||||
}
|
||||
dy > 0 -> {
|
||||
val diff = getConsumeBottomBorder(itemView.top, itemView.bottom) - selectedTargetCenter
|
||||
if (diff < 0 && abs(diff) < winnerScore) {
|
||||
winnerScore = abs(diff)
|
||||
winner = this
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
} else {
|
||||
val diff = getDropTargetCenter(itemView.top, itemView.bottom) -
|
||||
getSelectedTargetCenter(curY, selected.itemView.height)
|
||||
when {
|
||||
dy < 0 ->
|
||||
if (diff > 0 && abs(diff) < winnerScore) {
|
||||
winnerScore = abs(diff)
|
||||
winner = this
|
||||
} else Unit
|
||||
dy > 0 ->
|
||||
if (diff < 0 && abs(diff) < winnerScore) {
|
||||
winnerScore = abs(diff)
|
||||
winner = this
|
||||
} else Unit
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (winner == null) {
|
||||
subject.accept(DragState.DeleteShiftRequest)
|
||||
}
|
||||
return winner
|
||||
}
|
||||
|
||||
private fun setDraggableViewProperties(itemView: View) = with(itemView) {
|
||||
setBackgroundColor(Color.LTGRAY)
|
||||
alpha = DRAG_STATE_ALPHA
|
||||
}
|
||||
|
||||
private fun removeDraggableViewProperties(itemView: View) = with(itemView) {
|
||||
setBackgroundColor(0)
|
||||
alpha = IDLE_STATE_ALPHA
|
||||
}
|
||||
|
||||
private fun isCurrentlyActive(
|
||||
isCurrentlyActive: Boolean,
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
dY: Float
|
||||
) = when (isCurrentlyActive) {
|
||||
true -> itemDraggedActive(recyclerView, viewHolder, dY)
|
||||
false -> subject.accept(DragState.DraggingNotActive(viewHolder.itemView, IDLE_STATE_ALPHA))
|
||||
}
|
||||
|
||||
private fun itemDraggedActive(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
dY: Float
|
||||
) = with(viewHolder.itemView) {
|
||||
if (alpha != DRAG_STATE_ALPHA) setDraggableViewProperties(this)
|
||||
for (x in 0 until recyclerView.childCount) {
|
||||
val child = recyclerView.getChildAt(x)
|
||||
if (child != this) {
|
||||
val consumeCenter = (child.top + child.bottom).div(2)
|
||||
val interval = child.height.div(CONSUME_INTERVAL)
|
||||
if (isDraggedItemCanBeConsumeByCenterOfDragged(
|
||||
consumerCenter = consumeCenter,
|
||||
interval = interval,
|
||||
baselineY = getDraggedItemBaseline(this, dY).toInt()
|
||||
)
|
||||
) {
|
||||
subject.accept(DragState.DeleteShiftRequest)
|
||||
val consume = recyclerView.findContainingViewHolder(child)
|
||||
subject.accept(DragState.ConsumeRequest(viewHolder.adapterPosition, consume))
|
||||
break
|
||||
} else {
|
||||
if (consumerPosition > POSITION_NONE) {
|
||||
recyclerView.findViewHolderForAdapterPosition(consumerPosition)?.itemView?.setBackgroundColor(0)
|
||||
}
|
||||
subject.accept(DragState.Dragging(viewHolder.adapterPosition))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleState(state: DragState) = when (state) {
|
||||
|
||||
is DragState.Shift -> {
|
||||
shiftView?.let {
|
||||
consumerPosition = POSITION_NONE
|
||||
(it as? ItemTouchHelperViewHolder)?.clearTargetView()
|
||||
onDragDropAction.invoke(
|
||||
DragDropAction.Shift(from = state.from.adapterPosition, to = it.adapterPosition)
|
||||
)
|
||||
}
|
||||
shiftView = null
|
||||
}
|
||||
|
||||
is DragState.ShiftRequest -> {
|
||||
(shiftView as? ItemTouchHelperViewHolder)?.clearTargetView()
|
||||
shiftView = null
|
||||
|
||||
if (state.from.adapterPosition > state.to.adapterPosition) {
|
||||
(state.to as? ItemTouchHelperViewHolder)?.targetView()
|
||||
} else {
|
||||
(state.to as? ItemTouchHelperViewHolder)?.targetViewBottom()
|
||||
}
|
||||
shiftView = state.to
|
||||
}
|
||||
|
||||
is DragState.DeleteShiftRequest -> {
|
||||
(shiftView as? ItemTouchHelperViewHolder)?.clearTargetView()
|
||||
shiftView = null
|
||||
}
|
||||
|
||||
is DragState.ConsumeRequest -> {
|
||||
subject.accept(DragState.DeleteShiftRequest)
|
||||
state.consumerViewHolder?.let { holder ->
|
||||
if (holder is BlockView.Consumer) {
|
||||
holder.itemView.setBackgroundColor(Color.GRAY)
|
||||
consumerPosition = holder.adapterPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DragState.Consume -> {
|
||||
shiftView = null
|
||||
subject.accept(
|
||||
DragState.DraggingNotActive(view = state.target.itemView, alpha = CONSUME_STATE_ALPHA)
|
||||
)
|
||||
onDragDropAction.invoke(
|
||||
DragDropAction.Consume(target = state.target.adapterPosition, consumer = consumerPosition)
|
||||
)
|
||||
}
|
||||
|
||||
is DragState.Dragging -> {
|
||||
/** shiftView не нужно обнулять, так как после DragState.ShiftRequest может
|
||||
быть DragState.Dragging и тогда в clearView не случится SwapRequest
|
||||
*/
|
||||
consumerPosition = POSITION_NONE
|
||||
}
|
||||
|
||||
is DragState.DraggingNotActive -> {
|
||||
if (state.alpha < IDLE_STATE_ALPHA) {
|
||||
state.view.alpha = state.alpha
|
||||
} else Unit
|
||||
}
|
||||
|
||||
is DragState.Idle -> {
|
||||
subject.accept(DragState.DeleteShiftRequest)
|
||||
consumerPosition = POSITION_NONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDraggedItemBaseline(draggedView: View, dY: Float): Float =
|
||||
(draggedView.top + dY + (draggedView.height / 2))
|
||||
|
||||
private fun isDraggedItemCanBeConsumeByCenterOfDragged(
|
||||
consumerCenter: Int,
|
||||
baselineY: Int,
|
||||
interval: Int
|
||||
): Boolean = baselineY in consumerCenter.minus(interval)..consumerCenter.plus(interval)
|
||||
}
|
||||
|
||||
fun getDropTargetCenter(top: Int, bottom: Int) = (top + bottom) / 2
|
||||
|
||||
fun getConsumeTopBorder(top: Int, bottom: Int): Int {
|
||||
val centerY = getDropTargetCenter(top = top, bottom = bottom)
|
||||
val height = bottom - top
|
||||
return centerY - height.div(CONSUME_INTERVAL)
|
||||
}
|
||||
|
||||
fun getConsumeBottomBorder(top: Int, bottom: Int): Int {
|
||||
val centerY = getDropTargetCenter(top = top, bottom = bottom)
|
||||
val height = bottom - top
|
||||
return centerY + height.div(CONSUME_INTERVAL)
|
||||
}
|
||||
|
||||
fun getSelectedTargetCenter(curY: Int, height: Int): Int = curY + height.div(2)
|
File diff suppressed because it is too large
Load diff
|
@ -1,45 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import com.agileburo.anytype.feature_editor.domain.Block
|
||||
import com.agileburo.anytype.feature_editor.domain.ContentType
|
||||
import com.agileburo.anytype.feature_editor.presentation.util.SwapRequest
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 21.03.2019.
|
||||
*/
|
||||
sealed class EditorAction {
|
||||
object PressButton : EditorAction()
|
||||
data class PressBlock(val id: String) : EditorAction()
|
||||
data class OnBlockFocus(val position: Int) : EditorAction()
|
||||
}
|
||||
|
||||
sealed class EditBlockAction {
|
||||
data class TextClick(val block: Block) : EditBlockAction()
|
||||
data class Header1Click(val block: Block) : EditBlockAction()
|
||||
data class Header2Click(val block: Block) : EditBlockAction()
|
||||
data class Header3Click(val block: Block) : EditBlockAction()
|
||||
data class Header4Click(val block: Block) : EditBlockAction()
|
||||
data class HighLightClick(val block: Block) : EditBlockAction()
|
||||
data class BulletClick(val block: Block) : EditBlockAction()
|
||||
data class NumberedClick(val block: Block) : EditBlockAction()
|
||||
data class CheckBoxClick(val block: Block) : EditBlockAction()
|
||||
data class CodeClick(val block: Block) : EditBlockAction()
|
||||
data class ArchiveBlock(val id: String) : EditBlockAction()
|
||||
}
|
||||
|
||||
sealed class EditorState {
|
||||
data class ClearBlockFocus(val position: Int, val contentType: ContentType) : EditorState()
|
||||
object HideKeyboard : EditorState()
|
||||
data class Result(val blocks: List<Block>) : EditorState()
|
||||
data class Swap(val request: SwapRequest) : EditorState()
|
||||
data class Remove(val position: Int) : EditorState()
|
||||
data class Updates(val blocks: List<Block>) : EditorState()
|
||||
data class Update(val block: Block) : EditorState()
|
||||
data class Archive(val id: String) : EditorState()
|
||||
data class Error(val msg: String) : EditorState()
|
||||
object DragDropOn : EditorState()
|
||||
object DragDropOff : EditorState()
|
||||
object Loading : EditorState()
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.agileburo.anytype.core_utils.ext.UIExtensions
|
||||
import com.agileburo.anytype.core_utils.ext.toast
|
||||
import com.agileburo.anytype.feature_editor.R
|
||||
import com.agileburo.anytype.feature_editor.disposedBy
|
||||
import com.agileburo.anytype.feature_editor.domain.Block
|
||||
import com.agileburo.anytype.feature_editor.domain.ContentType
|
||||
import com.agileburo.anytype.feature_editor.domain.toView
|
||||
import com.agileburo.anytype.feature_editor.presentation.mapper.BlockModelMapper
|
||||
import com.agileburo.anytype.feature_editor.presentation.mapper.BlockViewMapper
|
||||
import com.agileburo.anytype.feature_editor.presentation.mvvm.EditorViewModel
|
||||
import com.agileburo.anytype.feature_editor.presentation.mvvm.EditorViewModelFactory
|
||||
import com.agileburo.anytype.feature_editor.presentation.util.DragDropAction
|
||||
import com.agileburo.anytype.feature_editor.presentation.util.SwapRequest
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.fragment_editor.*
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class EditorFragment : Fragment() {
|
||||
|
||||
private val mapper by lazy { BlockViewMapper() }
|
||||
private val viewToModelMapper by lazy { BlockModelMapper() }
|
||||
|
||||
@Inject
|
||||
lateinit var factory: EditorViewModelFactory
|
||||
|
||||
private val viewModel by lazy {
|
||||
ViewModelProviders.of(this, factory).get(EditorViewModel::class.java)
|
||||
}
|
||||
|
||||
private val disposable = CompositeDisposable()
|
||||
|
||||
private val helper : ItemTouchHelper by lazy {
|
||||
ItemTouchHelper(dragAndDropBehavior)
|
||||
}
|
||||
|
||||
private val dragAndDropBehavior by lazy {
|
||||
DragAndDropBehavior(
|
||||
onDragDropAction = { action ->
|
||||
if (action is DragDropAction.Consume)
|
||||
viewModel.onConsumeRequested(
|
||||
consumerId = blockAdapter.blocks[action.consumer].id,
|
||||
consumableId = blockAdapter.blocks[action.target].id
|
||||
)
|
||||
else if (action is DragDropAction.Shift) {
|
||||
if (action.from < action.to)
|
||||
viewModel.onMoveAfter(
|
||||
previousId = blockAdapter.blocks[action.to].id,
|
||||
targetId = blockAdapter.blocks[action.from].id
|
||||
) else
|
||||
viewModel.onMoveAfter(
|
||||
previousId = blockAdapter.blocks[action.to - 1].id,
|
||||
targetId = blockAdapter.blocks[action.from].id
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val blockAdapter by lazy {
|
||||
EditorAdapter(
|
||||
blocks = mutableListOf(),
|
||||
blockContentListener = { viewModel.onBlockContentChanged(viewToModelMapper.mapToModel(it)) },
|
||||
menuListener = viewModel::onBlockMenuAction,
|
||||
focusListener = viewModel::onBlockFocus,
|
||||
onExpandClick = viewModel::onExpandClicked,
|
||||
onEnterPressed = viewModel::onEnterClicked
|
||||
).also {
|
||||
helper.attachToRecyclerView(blockList)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun inject()
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
inject()
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_editor, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
viewModel.observeState()
|
||||
.subscribe(this::handleState)
|
||||
.disposedBy(disposable)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initializeView()
|
||||
dragAndDropBehavior.init()
|
||||
}
|
||||
|
||||
private fun initializeView() = with(blockList) {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
addItemDecoration(SpaceItemDecoration(space = 48, addSpaceBelowLastItem = true))
|
||||
adapter = blockAdapter
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
dragAndDropBehavior.destroy()
|
||||
disposable.clear()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun handleState(state: EditorState) = when (state) {
|
||||
is EditorState.Loading -> {
|
||||
}
|
||||
is EditorState.Result -> setBlocks(state.blocks)
|
||||
is EditorState.Update -> updateBlock(state.block)
|
||||
|
||||
is EditorState.Updates -> render(state.blocks)
|
||||
|
||||
is EditorState.Swap -> swap(state.request)
|
||||
|
||||
is EditorState.Remove -> remove(state.position)
|
||||
|
||||
is EditorState.Archive -> {
|
||||
}
|
||||
is EditorState.Error -> onError(state.msg)
|
||||
is EditorState.ClearBlockFocus -> clearBlockFocus(state.position, state.contentType)
|
||||
is EditorState.HideKeyboard -> UIExtensions.hideSoftKeyBoard(requireActivity(), blockList)
|
||||
is EditorState.DragDropOn -> helper.attachToRecyclerView(blockList)
|
||||
is EditorState.DragDropOff -> {
|
||||
//Todo Потенциально опасное место, см. https://issuetracker.google.com/issues/140447176
|
||||
helper.attachToRecyclerView(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearBlockFocus(position: Int, contentType: ContentType) {
|
||||
blockList.layoutManager?.findViewByPosition(position)?.let {
|
||||
it.findViewById<View>(getEditTextId(contentType))?.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEditTextId(contentType: ContentType) =
|
||||
when (contentType) {
|
||||
is ContentType.P -> R.id.textEditable
|
||||
is ContentType.H1 -> R.id.textHeaderOne
|
||||
is ContentType.H2 -> R.id.textHeaderTwo
|
||||
is ContentType.H3 -> R.id.textHeaderThree
|
||||
is ContentType.H4 -> R.id.textHeaderFour
|
||||
is ContentType.Quote -> R.id.textQuote
|
||||
is ContentType.Check -> R.id.textCheckBox
|
||||
is ContentType.Code -> R.id.textCode
|
||||
is ContentType.UL -> R.id.textBullet
|
||||
is ContentType.NumberedList -> R.id.contentText
|
||||
is ContentType.None -> throw IllegalStateException()
|
||||
is ContentType.Toggle -> throw UnsupportedOperationException("need implement Toggle")
|
||||
}
|
||||
|
||||
private fun setBlocks(blocks: List<Block>) =
|
||||
blockAdapter.setBlocks(blocks.toMutableList().toView())
|
||||
|
||||
private fun updateBlock(block: Block) = blockAdapter.updateBlock(mapper.mapToView(block))
|
||||
|
||||
private fun render(blocks: List<Block>) = blockAdapter.update(blocks.toMutableList().toView())
|
||||
|
||||
private fun swap(request: SwapRequest) = blockAdapter.swap(request)
|
||||
|
||||
private fun remove(position: Int) = blockAdapter.remove(position)
|
||||
|
||||
private fun onError(msg: CharSequence) = requireContext().toast(msg)
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextPaint
|
||||
import android.text.style.MetricAffectingSpan
|
||||
|
||||
open class FontSpan(private val font: Typeface?) : MetricAffectingSpan() {
|
||||
|
||||
override fun updateMeasureState(textPaint: TextPaint) = update(textPaint)
|
||||
|
||||
override fun updateDrawState(textPaint: TextPaint) = update(textPaint)
|
||||
|
||||
private fun update(textPaint: TextPaint) {
|
||||
textPaint.apply {
|
||||
val old = typeface
|
||||
val oldStyle = old?.style ?: 0
|
||||
|
||||
// keep the style set before
|
||||
val font = Typeface.create(font, oldStyle)
|
||||
typeface = font
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.URLSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.PopupWindow
|
||||
import com.agileburo.anytype.feature_editor.R
|
||||
|
||||
class HyperLinkMenu(
|
||||
private val context: Context,
|
||||
private val editText: EditText,
|
||||
private val start: Int, private val end: Int
|
||||
) : PopupWindow(context) {
|
||||
|
||||
init {
|
||||
setupView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.popup_hyperlink, null)
|
||||
isFocusable = true
|
||||
isOutsideTouchable = true
|
||||
contentView = view
|
||||
setClicks()
|
||||
}
|
||||
|
||||
private fun setClicks() {
|
||||
val edtLink = contentView.findViewById<EditText>(R.id.edtLink)
|
||||
contentView.findViewById<Button>(R.id.btnLink).setOnClickListener {
|
||||
editText.text = SpannableStringBuilder(editText.text).apply {
|
||||
setSpan(URLSpan(edtLink.text.toString()), start, end, 1)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* Created by Konstantin Ivanov
|
||||
* email : ki@agileburo.com
|
||||
* on 2019-06-25.
|
||||
*/
|
||||
class SpaceItemDecoration(private val space: Int, private val addSpaceBelowLastItem : Boolean) : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect, view: View,
|
||||
parent: RecyclerView, state: RecyclerView.State
|
||||
) {
|
||||
outRect.bottom = space
|
||||
if (parent.getChildAdapterPosition(view) == 0) {
|
||||
outRect.top = space
|
||||
}
|
||||
|
||||
if (addSpaceBelowLastItem && parent.getChildAdapterPosition(view) == parent.adapter?.itemCount?.dec()) {
|
||||
outRect.bottom = space * 10
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.BulletSpan
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import com.agileburo.anytype.feature_editor.domain.Mark
|
||||
import me.saket.bettermovementmethod.BetterLinkMovementMethod
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun SpannableString.addMarks(
|
||||
marks: List<Mark>, textView: TextView,
|
||||
click: (String) -> Unit, itemView: View
|
||||
) =
|
||||
apply {
|
||||
marks.forEach {
|
||||
when (it.type) {
|
||||
Mark.MarkType.BOLD -> setSpan(
|
||||
StyleSpan(Typeface.BOLD),
|
||||
it.start.toInt(),
|
||||
it.end.toInt(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
Mark.MarkType.ITALIC -> setSpan(
|
||||
StyleSpan(Typeface.ITALIC),
|
||||
it.start.toInt(),
|
||||
it.end.toInt(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
Mark.MarkType.STRIKE_THROUGH -> setSpan(
|
||||
StrikethroughSpan(),
|
||||
it.start.toInt(),
|
||||
it.end.toInt(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
Mark.MarkType.CODE -> setSpan(
|
||||
CodeBlockSpan(Typeface.DEFAULT),
|
||||
it.start.toInt(),
|
||||
it.end.toInt(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
Mark.MarkType.HYPERTEXT -> {
|
||||
// val method = BetterLinkMovementMethod.getInstance()
|
||||
// textView.movementMethod = method
|
||||
// textView.setOnTouchListener { _, event ->
|
||||
// method.onTouchEvent(textView, textView.text as Spannable, event)
|
||||
// || itemView.onTouchEvent(event)
|
||||
// }
|
||||
|
||||
withClickableSpan(it.param, it.start.toInt(), it.end.toInt(), click)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Not supported type of marks : ${it.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SpannableString.withClickableSpan(
|
||||
param: String,
|
||||
start: Int,
|
||||
end: Int,
|
||||
onClickListener: (String) -> Unit
|
||||
): SpannableString {
|
||||
|
||||
val clickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) = onClickListener(param)
|
||||
}
|
||||
require(start <= end) { "Marks start should be <= end!" }
|
||||
setSpan(
|
||||
clickableSpan,
|
||||
start,
|
||||
end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
fun SpannableString.withBulletSpan(gapWidth: Int, start: Int): SpannableString {
|
||||
setSpan(BulletSpan(gapWidth), start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
return this
|
||||
}
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
package com.agileburo.anytype.feature_editor.ui
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.EditText
|
||||
import com.agileburo.anytype.feature_editor.R
|
||||
|
||||
class TextStyleCallback(
|
||||
private val editText: ClearFocusEditText,
|
||||
private val linkClick: (EditText, Int, Int) -> Unit
|
||||
) : ActionMode.Callback {
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
val start = editText.selectionStart
|
||||
val end = editText.selectionEnd
|
||||
val ssb = SpannableStringBuilder(editText.text)
|
||||
when (item?.itemId) {
|
||||
R.id.boldInactive -> {
|
||||
ssb.setSpan(StyleSpan(Typeface.BOLD), start, end, 1)
|
||||
editText.text = ssb
|
||||
return true
|
||||
}
|
||||
R.id.boldActive -> {
|
||||
removeStyleSpanFromSubstring(
|
||||
style = Typeface.BOLD, start = start,
|
||||
end = end, spannable = ssb
|
||||
)?.let {
|
||||
editText.text = it
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.italicInactive -> {
|
||||
ssb.setSpan(StyleSpan(Typeface.ITALIC), start, end, 1)
|
||||
editText.text = ssb
|
||||
return true
|
||||
}
|
||||
R.id.italicActive -> {
|
||||
removeStyleSpanFromSubstring(
|
||||
style = Typeface.ITALIC, start = start,
|
||||
end = end, spannable = ssb
|
||||
)?.let {
|
||||
editText.text = it
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.strikeInactive -> {
|
||||
ssb.setSpan(StrikethroughSpan(), start, end, 1)
|
||||
editText.text = ssb
|
||||
return true
|
||||
}
|
||||
R.id.strikeActive -> {
|
||||
removeStrikeSpanFromSubstring(
|
||||
start = start, end = end, spannable = ssb
|
||||
)?.let {
|
||||
editText.text = it
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.linkInactive -> {
|
||||
linkClick.invoke(editText, start, end)
|
||||
return true
|
||||
}
|
||||
R.id.linkActive -> {
|
||||
removeUrlSpanFromSubstring(
|
||||
start = start, end = end, spannable = ssb
|
||||
)?.let {
|
||||
editText.text = it
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
mode?.menuInflater?.let {
|
||||
it.inflate(R.menu.style_toolbar, menu)
|
||||
editText.text?.getSpans(
|
||||
editText.selectionStart,
|
||||
editText.selectionEnd,
|
||||
Object::class.java
|
||||
)?.forEach { span ->
|
||||
if (span is StyleSpan) {
|
||||
if (span.style == Typeface.BOLD) {
|
||||
changeMenuItemsVisibility(
|
||||
menu = menu,
|
||||
inactiveId = R.id.boldInactive,
|
||||
activeId = R.id.boldActive
|
||||
)
|
||||
}
|
||||
if (span.style == Typeface.ITALIC) {
|
||||
changeMenuItemsVisibility(
|
||||
menu = menu,
|
||||
inactiveId = R.id.italicInactive,
|
||||
activeId = R.id.italicActive
|
||||
)
|
||||
}
|
||||
}
|
||||
if (span is StrikethroughSpan) {
|
||||
changeMenuItemsVisibility(
|
||||
menu = menu,
|
||||
inactiveId = R.id.strikeInactive,
|
||||
activeId = R.id.strikeActive
|
||||
)
|
||||
}
|
||||
|
||||
if (span is URLSpan) {
|
||||
changeMenuItemsVisibility(
|
||||
menu = menu,
|
||||
inactiveId = R.id.linkInactive,
|
||||
activeId = R.id.linkActive
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
}
|
||||
|
||||
private fun changeMenuItemsVisibility(menu: Menu?, inactiveId: Int, activeId: Int) {
|
||||
menu?.findItem(inactiveId)?.isVisible = false
|
||||
menu?.findItem(activeId)?.isVisible = true
|
||||
}
|
||||
|
||||
private fun removeStyleSpanFromSubstring(
|
||||
style: Int,
|
||||
start: Int,
|
||||
end: Int,
|
||||
spannable: SpannableStringBuilder
|
||||
): SpannableStringBuilder? {
|
||||
spannable.getSpans(start, end, StyleSpan::class.java)?.forEach { span ->
|
||||
if (span.style == style) {
|
||||
return SpannableStringBuilder().apply {
|
||||
if (start > 0) append(spannable.subSequence(0, start))
|
||||
|
||||
append(
|
||||
SpannableStringBuilder(spannable, start, end)
|
||||
.apply { removeSpan(span) }
|
||||
)
|
||||
|
||||
if (end < spannable.length) {
|
||||
append(spannable.subSequence(end, spannable.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun removeStrikeSpanFromSubstring(
|
||||
start: Int,
|
||||
end: Int,
|
||||
spannable: SpannableStringBuilder
|
||||
): SpannableStringBuilder? {
|
||||
spannable.getSpans(start, end, StrikethroughSpan::class.java)?.forEach { span ->
|
||||
return SpannableStringBuilder().apply {
|
||||
if (start > 0) append(spannable.subSequence(0, start))
|
||||
|
||||
append(
|
||||
SpannableStringBuilder(spannable, start, end)
|
||||
.apply { removeSpan(span) }
|
||||
)
|
||||
|
||||
if (end < spannable.length) {
|
||||
append(spannable.subSequence(end, spannable.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun removeUrlSpanFromSubstring(
|
||||
start: Int,
|
||||
end: Int, spannable: SpannableStringBuilder
|
||||
): SpannableStringBuilder? {
|
||||
spannable.getSpans(start, end, URLSpan::class.java)?.forEach { span ->
|
||||
return SpannableStringBuilder().apply {
|
||||
if (start > 0) append(spannable.subSequence(0, start))
|
||||
|
||||
append(
|
||||
SpannableStringBuilder(spannable, start, end)
|
||||
.apply { removeSpan(span) }
|
||||
)
|
||||
|
||||
if (end < spannable.length) {
|
||||
append(spannable.subSequence(end, spannable.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/button_active" android:state_selected="true"/>
|
||||
<item android:color="@color/button_active" android:state_pressed="true"/>
|
||||
<item android:color="@color/button_not_active"/>
|
||||
</selector>
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/design_default_color_primary" />
|
||||
<padding
|
||||
android:bottom="1dp"
|
||||
android:left="1dp"
|
||||
android:right="1dp"
|
||||
android:top="1dp" />
|
||||
</shape>
|
|
@ -1,15 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="16"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M9,1.997v-1L15,7H9z" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M2,2v16h12V6.828L9.172,2H2zM0,0h10l6,6v14H0V0z" />
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="13"
|
||||
android:viewportHeight="17">
|
||||
<path
|
||||
android:fillColor="@color/button_active"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M0,0.996h6.203c3.466,0 5.232,1.41 5.232,4.162v0.09c0,1.7 -0.795,2.909 -2.56,3.424 2.052,0.425 3.156,1.61 3.156,3.76v0.089c0,2.953 -1.854,4.475 -5.54,4.475H0v-16zM5.938,14.49c1.766,0 2.561,-0.806 2.561,-2.26v-0.09c0,-1.5 -0.795,-2.238 -2.737,-2.238H3.488v4.588h2.45zM5.63,7.575c1.744,0 2.407,-0.626 2.407,-2.059v-0.09c0,-1.342 -0.751,-1.946 -2.429,-1.946H3.488v4.095h2.141z" />
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="13"
|
||||
android:viewportHeight="17">
|
||||
<path
|
||||
android:fillColor="@color/button_not_active"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M0,0.996h6.203c3.466,0 5.232,1.41 5.232,4.162v0.09c0,1.7 -0.795,2.909 -2.56,3.424 2.052,0.425 3.156,1.61 3.156,3.76v0.089c0,2.953 -1.854,4.475 -5.54,4.475H0v-16zM5.938,14.49c1.766,0 2.561,-0.806 2.561,-2.26v-0.09c0,-1.5 -0.795,-2.238 -2.737,-2.238H3.488v4.588h2.45zM5.63,7.575c1.744,0 2.407,-0.626 2.407,-2.059v-0.09c0,-1.342 -0.751,-1.946 -2.429,-1.946H3.488v4.095h2.141z" />
|
||||
</vector>
|
|
@ -1,23 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="19"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M6.031,0.996H18v2H6.031zM6.031,6.996h12v2h-12zM6.031,12.996H18v2H6.031z" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M2,1.996m-1.8,0a1.8,1.8 0,1 1,3.6 0a1.8,1.8 0,1 1,-3.6 0" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M2,7.996m-1.8,0a1.8,1.8 0,1 1,3.6 0a1.8,1.8 0,1 1,-3.6 0" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M2,13.996m-1.8,0a1.8,1.8 0,1 1,3.6 0a1.8,1.8 0,1 1,-3.6 0" />
|
||||
</vector>
|
|
@ -1,15 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M10.023,18.012a8,8 0,1 0,0 -16,8 8,0 0,0 0,16zM10.023,20.012c-5.522,0 -10,-4.477 -10,-10s4.478,-10 10,-10c5.523,0 10,4.477 10,10s-4.477,10 -10,10z" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M8.509,11.228l5.085,-5.085 1.414,1.414 -6.5,6.5 -3.516,-3.517 1.414,-1.414z" />
|
||||
</vector>
|
|
@ -1,15 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M2,2v14h16L18,2L2,2zM1.111,0L18.89,0C19.503,0 20,0.448 20,1v16c0,0.552 -0.497,1 -1.111,1L1.11,18C0.497,18 0,17.552 0,17L0,1c0,-0.552 0.497,-1 1.111,-1z" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M5.688,9l2.284,2.3V13l-4,-4 4,-4v1.7zM14.284,9L12,11.3V13l4,-4 -4,-4v1.7z" />
|
||||
</vector>
|
|
@ -1,21 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="18"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M1,5h12v12H1z"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ACA996" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M6,0h12v2H6z" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M16,0h2v12h-2z" />
|
||||
</vector>
|
|
@ -1,6 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp"
|
||||
android:tint="#FFBB2C" android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0" android:width="24dp">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<!-- drawable/format_header_4.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:pathData="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M18,18V13H13V11L18,4H20V11H21V13H20V18H18M18,11V7.42L15.45,11H18Z" />
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:pathData="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M14,18V16H16V6.31L13.5,7.75V5.44L16,4H18V16H20V18H14Z" />
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:pathData="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M21,18H15A2,2 0 0,1 13,16C13,15.47 13.2,15 13.54,14.64L18.41,9.41C18.78,9.05 19,8.55 19,8A2,2 0 0,0 17,6A2,2 0 0,0 15,8H13A4,4 0 0,1 17,4A4,4 0 0,1 21,8C21,9.1 20.55,10.1 19.83,10.83L15,16H21V18Z" />
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<!-- drawable/format_header_3.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:pathData="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M15,4H19A2,2 0 0,1 21,6V16A2,2 0 0,1 19,18H15A2,2 0 0,1 13,16V15H15V16H19V12H15V10H19V6H15V7H13V6A2,2 0 0,1 15,4Z" />
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="17"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M5,2h12v2H5zM5,8h12v2H5zM5,14h12v2H5zM0,0h1v18H0z" />
|
||||
</vector>
|
|
@ -1,14 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="10"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="@color/button_active"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M5.343,0h1.943L4.657,16H2.714z" />
|
||||
<path
|
||||
android:fillColor="@color/button_active"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M2.004,1.508L2.258,0H10l-0.27,1.508zM0,16l0.254,-1.508h7.742L7.726,16z" />
|
||||
</vector>
|
|
@ -1,14 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="10"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="@color/button_not_active"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M5.343,0h1.943L4.657,16H2.714z" />
|
||||
<path
|
||||
android:fillColor="@color/button_not_active"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M2.004,1.508L2.258,0H10l-0.27,1.508zM0,16l0.254,-1.508h7.742L7.726,16z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/button_active"
|
||||
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/button_not_active"
|
||||
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
|
||||
</vector>
|
|
@ -1,15 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M9,1.997v-1L15,7H9z" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M2,2v16h12V6.828L9.172,2H2zM0,0h10l6,6v14H0V0z" />
|
||||
</vector>
|
|
@ -1,15 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="19"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportHeight="19">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M2.038,2.334l-1.22,0.8V1.973L2.206,0.988h1.195v6.016H2.038zM0.19,18.775l2.225,-2.352c0.586,-0.62 0.96,-1.155 0.96,-1.707 0,-0.509 -0.272,-0.823 -0.807,-0.823 -0.543,0 -0.874,0.305 -0.968,1.112H0.352c0.068,-1.418 0.909,-2.165 2.276,-2.165 0.67,0 1.197,0.195 1.562,0.51 0.348,0.33 0.544,0.789 0.544,1.332 0,0.807 -0.467,1.47 -1.172,2.149l-1.147,1.12h2.361v1.045H0.191v-0.22z" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M7.031,2.996H19v2H7.031zM7.031,8.996h9v2h-9zM7.031,14.996H19v2H7.031z" />
|
||||
</vector>
|
|
@ -1,30 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="46dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="46">
|
||||
<path
|
||||
android:pathData="M33.1,45.5C34.5,45.5 35.6,44.4 35.6,43L35.6,13.4C35.6,13 35.4,12.6 35.2,12.3L23.8,0.9C23.7,0.8 23.5,0.7 23.4,0.6C23.4,0.6 23.4,0.6 23.4,0.6C23.2,0.5 23,0.5 22.8,0.5L2.9,0.5C1.5,0.5 0.4,1.6 0.4,3L0.4,43C0.4,44.4 1.5,45.5 2.9,45.5L33.1,45.5ZM34,12.5L25.2,12.5C24.4,12.5 23.7,11.8 23.7,11L23.7,2.2L34,12.5ZM1.4,43L1.4,3C1.4,2.2 2.1,1.5 2.9,1.5L22.7,1.5L22.7,11C22.7,12.4 23.8,13.5 25.2,13.5L34.7,13.5L34.7,43C34.7,43.8 34,44.5 33.2,44.5L2.9,44.5C2,44.5 1.4,43.8 1.4,43Z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M7,22.3L17,22.3C17.3,22.3 17.5,22.1 17.5,21.8C17.5,21.5 17.3,21.3 17,21.3L7,21.3C6.7,21.3 6.5,21.5 6.5,21.8C6.5,22.1 6.7,22.3 7,22.3Z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M7,28.3L25,28.3C25.3,28.3 25.5,28.1 25.5,27.8C25.5,27.5 25.3,27.3 25,27.3L7,27.3C6.7,27.3 6.5,27.5 6.5,27.8C6.5,28.1 6.7,28.3 7,28.3Z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M7,34.3L25,34.3C25.3,34.3 25.5,34.1 25.5,33.8C25.5,33.5 25.3,33.3 25,33.3L7,33.3C6.7,33.3 6.5,33.5 6.5,33.8C6.5,34.1 6.7,34.3 7,34.3Z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
|
@ -1,14 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="@color/button_active"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M7.775,16c-4.04,0 -5.5,-2.292 -5.775,-4.955h1.777c0.233,1.855 0.973,3.427 3.998,3.427 1.946,0 3.448,-1.179 3.448,-2.947s-0.783,-2.532 -3.66,-2.99c-3.173,-0.524 -5.055,-1.506 -5.055,-4.3C2.508,1.855 4.518,0 7.373,0c3.046,0 4.887,1.506 5.204,4.3h-1.65c-0.36,-1.986 -1.439,-2.772 -3.554,-2.772 -2.094,0 -3.152,1.048 -3.152,2.532 0,1.506 0.55,2.336 3.639,2.794 3.363,0.524 5.14,1.615 5.14,4.54C13,14.014 10.779,16 7.775,16z" />
|
||||
<path
|
||||
android:fillColor="@color/button_active"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M0,7h16v2H0z" />
|
||||
</vector>
|
|
@ -1,14 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="@color/button_not_active"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M7.775,16c-4.04,0 -5.5,-2.292 -5.775,-4.955h1.777c0.233,1.855 0.973,3.427 3.998,3.427 1.946,0 3.448,-1.179 3.448,-2.947s-0.783,-2.532 -3.66,-2.99c-3.173,-0.524 -5.055,-1.506 -5.055,-4.3C2.508,1.855 4.518,0 7.373,0c3.046,0 4.887,1.506 5.204,4.3h-1.65c-0.36,-1.986 -1.439,-2.772 -3.554,-2.772 -2.094,0 -3.152,1.048 -3.152,2.532 0,1.506 0.55,2.336 3.639,2.794 3.363,0.524 5.14,1.615 5.14,4.54C13,14.014 10.779,16 7.775,16z" />
|
||||
<path
|
||||
android:fillColor="@color/button_not_active"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M0,7h16v2H0z" />
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:pathData="M18.5,4L19.66,8.35L18.7,8.61C18.25,7.74 17.79,6.87 17.26,6.43C16.73,6 16.11,6 15.5,6H13V16.5C13,17 13,17.5 13.33,17.75C13.67,18 14.33,18 15,18V19H9V18C9.67,18 10.33,18 10.67,17.75C11,17.5 11,17 11,16.5V6H8.5C7.89,6 7.27,6 6.74,6.43C6.21,6.87 5.75,7.74 5.3,8.61L4.34,8.35L5.5,4H18.5Z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M5,4v3h5.5v12h3V7H19V4z"/>
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="19"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M7.031,1.996H19v2H7.031zM7.031,7.996h9v2h-9zM7.031,13.996h12v2h-12zM1.997,2.985L0.001,0.99 0.99,0l2.987,2.983 -2.986,2.99L0,4.986zM1.997,14.985L0.001,12.99 0.99,12l2.987,2.983 -2.986,2.99L0,16.986z" />
|
||||
</vector>
|
|
@ -1,12 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M0.099,39.313l19.853,-19.121l-19.77,-19.504l4.27,-0.001l19.799,19.532l-19.826,19.094z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
|
@ -1,15 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="16"
|
||||
android:tint="@color/button_tint"
|
||||
android:viewportHeight="19">
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M3,6v11h10V6h2v11a2,2 0,0 1,-2 2H3a2,2 0,0 1,-2 -2V6h2zM0,2h16v1a1,1 0,0 1,-1 1H1a1,1 0,0 1,-1 -1V2z" />
|
||||
<path
|
||||
android:fillColor="#ACA996"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M5.992,0h4a1,1 0,0 1,1 1v3h-6V1a1,1 0,0 1,1 -1zM8.992,6h2v9h-2zM4.992,6h2v9h-2z" />
|
||||
</vector>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/dark_gray"/>
|
||||
<corners android:radius="4dp"/>
|
||||
|
||||
</shape>
|
|
@ -1,29 +0,0 @@
|
|||
<?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"
|
||||
android:background="@android:color/white">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/document_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/cont_desc_doc_icon"
|
||||
android:focusableInTouchMode="true"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_text_block"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/blockList"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/document_icon" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,59 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_margin="12dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<TextView
|
||||
android:textStyle="bold"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/title" app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="16dp" android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent" android:text="@android:string/paste_as_plain_text"
|
||||
app:layout_constraintEnd_toStartOf="@+id/image" android:layout_marginEnd="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:fontFamily="sans-serif-condensed-light"
|
||||
android:textSize="14sp"
|
||||
android:text="TextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/description" android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title" app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="16dp" app:layout_constraintEnd_toStartOf="@+id/image"
|
||||
android:layout_marginEnd="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textSize="12sp"
|
||||
android:text="TextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/url" android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/description" app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="16dp" android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/image"
|
||||
android:layout_marginEnd="16dp"/>
|
||||
|
||||
<ImageView
|
||||
android:background="@color/dark_gray"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintWidth_percent=".4"
|
||||
android:id="@+id/image"
|
||||
android:layout_width="0dp" android:layout_height="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
|
@ -1,50 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<View
|
||||
android:id="@+id/borderTopBullet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnBullet"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:contentDescription="@string/editor_toolbar"
|
||||
android:padding="8dp"
|
||||
app:srcCompat="@drawable/ic_edit" />
|
||||
|
||||
<com.agileburo.anytype.feature_editor.ui.ClearFocusEditText
|
||||
android:id="@+id/textBullet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_toEndOf="@id/btnBullet"
|
||||
android:background="@android:color/transparent"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
tools:text="Here are some quick tips for a first-time organization member."
|
||||
android:paddingTop="16dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:paddingBottom="16dp"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/borderBottomBullet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_below="@id/textBullet"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,58 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="@color/white">
|
||||
|
||||
<View
|
||||
android:id="@+id/borderTopCheckBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnCheckboxBlock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:contentDescription="@string/editor_toolbar"
|
||||
android:padding="8dp"
|
||||
app:srcCompat="@drawable/ic_edit" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@id/btnCheckboxBlock"/>
|
||||
|
||||
<com.agileburo.anytype.feature_editor.ui.ClearFocusEditText
|
||||
android:id="@+id/textCheckBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_toEndOf="@id/checkbox"
|
||||
android:background="@android:color/transparent"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:layout_centerVertical="true"
|
||||
tools:text="Here are some quick tips for a first-time organization member."
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/borderBottomCheckBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_below="@id/textCheckBox"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,46 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/white">
|
||||
|
||||
<View
|
||||
android:id="@+id/borderTopCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnCode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:contentDescription="@string/editor_toolbar"
|
||||
android:padding="8dp"
|
||||
app:srcCompat="@drawable/ic_edit" />
|
||||
|
||||
<com.agileburo.anytype.feature_editor.ui.ClearFocusEditText
|
||||
android:id="@+id/textCode"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/btnCode"
|
||||
android:background="@drawable/rounded_rectangle_gray"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:padding="16dp"
|
||||
android:textColor="#FFFF"
|
||||
android:textSize="16sp"
|
||||
android:typeface="monospace" />
|
||||
|
||||
<View
|
||||
android:id="@+id/borderBottomCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_below="@id/textCode"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<View android:background="@color/gray_background"
|
||||
android:layout_margin="16dp"
|
||||
android:alpha="0.5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"/>
|
||||
|
||||
</FrameLayout>
|
|
@ -1,52 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout 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:id="@+id/container_editable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<View
|
||||
android:id="@+id/border_top"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnEditable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:contentDescription="@string/editor_toolbar"
|
||||
android:padding="8dp"
|
||||
app:srcCompat="@drawable/ic_edit" />
|
||||
|
||||
<com.agileburo.anytype.feature_editor.ui.ClearFocusEditText
|
||||
android:id="@+id/textEditable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_toEndOf="@id/btnEditable"
|
||||
android:background="@android:color/transparent"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="16sp"
|
||||
tools:text="Here are some quick tips for a first-time organization member." />
|
||||
|
||||
<View
|
||||
android:id="@+id/border_bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_below="@id/textEditable"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,53 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<View
|
||||
android:id="@+id/borderTopHeaderFour"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnHeaderFour"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:contentDescription="@string/editor_toolbar"
|
||||
android:padding="8dp"
|
||||
app:srcCompat="@drawable/ic_edit" />
|
||||
|
||||
|
||||
<com.agileburo.anytype.feature_editor.ui.ClearFocusEditText
|
||||
android:id="@+id/textHeaderFour"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_toEndOf="@id/btnHeaderFour"
|
||||
android:background="@android:color/transparent"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_centerVertical="true"
|
||||
tools:text="Here are some quick tips for a first-time organization member."
|
||||
android:textColor="@android:color/black"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<View
|
||||
android:id="@+id/borderBottomHeaderFour"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_below="@id/textHeaderFour"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.agileburo.anytype.feature_editor.ui.ClearFocusEditText
|
||||
android:fontFamily="monospace"
|
||||
android:id="@+id/textHeaderOne"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textSize="32sp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:hint="Untitled"
|
||||
android:textColor="@android:color/black"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:background="@android:color/transparent"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,52 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<View
|
||||
android:id="@+id/borderTopHeaderThree"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnHeaderThree"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:padding="8dp"
|
||||
android:contentDescription="@string/editor_toolbar"
|
||||
app:srcCompat="@drawable/ic_edit"/>
|
||||
|
||||
|
||||
<com.agileburo.anytype.feature_editor.ui.ClearFocusEditText
|
||||
android:id="@+id/textHeaderThree"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_toEndOf="@id/btnHeaderThree"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
tools:text="Here are some quick tips for a first-time organization member."
|
||||
android:layout_centerVertical="true"
|
||||
android:textColor="@android:color/black"
|
||||
android:background="@android:color/transparent"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<View
|
||||
android:id="@+id/borderBottomHeaderThree"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_below="@id/textHeaderThree"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,52 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<View
|
||||
android:id="@+id/borderTopHeaderTwo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnHeaderTwo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:padding="8dp"
|
||||
android:contentDescription="@string/editor_toolbar"
|
||||
app:srcCompat="@drawable/ic_edit"/>
|
||||
|
||||
|
||||
<com.agileburo.anytype.feature_editor.ui.ClearFocusEditText
|
||||
android:id="@+id/textHeaderTwo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_toEndOf="@id/btnHeaderTwo"
|
||||
android:textSize="24sp"
|
||||
android:paddingTop="16dp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingBottom="16dp"
|
||||
tools:text="Here are some quick tips for a first-time organization member."
|
||||
android:layout_centerVertical="true"
|
||||
android:textColor="@android:color/black"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:background="@android:color/transparent"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<View
|
||||
android:id="@+id/borderBottomHeaderTwo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:layout_below="@id/textHeaderTwo"
|
||||
android:background="@color/design_default_color_primary"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</RelativeLayout>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue