diff --git a/app-sample/build.gradle b/app-sample/build.gradle
index 5883ac72..9e4f0f23 100644
--- a/app-sample/build.gradle
+++ b/app-sample/build.gradle
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
+apply plugin: 'kotlin-android-extensions'
android {
@@ -40,14 +41,23 @@ kapt {
}
}
+androidExtensions {
+ features = ["parcelize"]
+}
+
dependencies {
kapt project(':sample-utils:processor')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation project(':markwon-core')
+
deps.with {
- api it['x-recycler-view']
- api it['adapt']
- api it['debug']
+ implementation it['x-recycler-view']
+ implementation it['x-cardview']
+ implementation it['x-fragment']
+ implementation it['gson']
+ implementation it['adapt']
+ implementation it['debug']
}
}
diff --git a/app-sample/src/debug/res/layout/flowlayout_preview.xml b/app-sample/src/debug/res/layout/flowlayout_preview.xml
new file mode 100644
index 00000000..4f9f98a1
--- /dev/null
+++ b/app-sample/src/debug/res/layout/flowlayout_preview.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/App.kt b/app-sample/src/main/java/io/noties/markwon/app/App.kt
index 0ae04ee6..444ded43 100644
--- a/app-sample/src/main/java/io/noties/markwon/app/App.kt
+++ b/app-sample/src/main/java/io/noties/markwon/app/App.kt
@@ -3,12 +3,20 @@ package io.noties.markwon.app
import android.app.Application
import io.noties.debug.AndroidLogDebugOutput
import io.noties.debug.Debug
+import java.util.concurrent.Executors
+@Suppress("unused")
class App : Application() {
override fun onCreate() {
super.onCreate()
Debug.init(AndroidLogDebugOutput(BuildConfig.DEBUG))
+
+ sampleManager = SampleManager(this, Executors.newCachedThreadPool())
+ }
+
+ companion object {
+ lateinit var sampleManager: SampleManager
}
}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/MainActivity.kt b/app-sample/src/main/java/io/noties/markwon/app/MainActivity.kt
index c265ce04..f0fc8e35 100644
--- a/app-sample/src/main/java/io/noties/markwon/app/MainActivity.kt
+++ b/app-sample/src/main/java/io/noties/markwon/app/MainActivity.kt
@@ -1,61 +1,19 @@
package io.noties.markwon.app
-import android.app.Activity
import android.os.Bundle
-import android.view.ViewTreeObserver
-import androidx.recyclerview.widget.DefaultItemAnimator
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import io.noties.adapt.Adapt
-import io.noties.debug.Debug
-import io.noties.markwon.app.adapt.SampleItem
-import io.noties.markwon.app.base.SearchBar
-import io.noties.markwon.sample.annotations.MarkwonArtifact
+import android.view.Window
+import androidx.fragment.app.FragmentActivity
+import io.noties.markwon.app.ui.SampleListFragment
-class MainActivity : Activity() {
+class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- val searchBar: SearchBar = findViewById(R.id.search_bar)
- searchBar.onSearchListener = {
- Debug.i("search: '$it'")
+ if (supportFragmentManager.findFragmentById(Window.ID_ANDROID_CONTENT) == null) {
+ supportFragmentManager.beginTransaction()
+ .add(Window.ID_ANDROID_CONTENT, SampleListFragment.init())
+ .commitNowAllowingStateLoss()
}
-
- val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
- recyclerView.layoutManager = LinearLayoutManager(this)
- recyclerView.itemAnimator = DefaultItemAnimator()
- recyclerView.setHasFixedSize(true)
- recyclerView.clipToPadding = false
-
- searchBar.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
- override fun onPreDraw(): Boolean {
- searchBar.viewTreeObserver.removeOnPreDrawListener(this)
- recyclerView.setPadding(
- recyclerView.paddingLeft,
- recyclerView.paddingTop + searchBar.height,
- recyclerView.paddingRight,
- recyclerView.paddingBottom
- )
- return true
- }
- })
-
- val adapt = Adapt.create()
- recyclerView.adapter = adapt
-
- val list = listOf(
- MarkwonSampleItem(
- "first",
- "1",
- "Title first",
- "Description her egoes and goes ang goes, so will it ever stop?",
- listOf(MarkwonArtifact.CORE, MarkwonArtifact.EDITOR),
- listOf("first", "second")
- )
- )
-
- adapt.setItems(list.map { SampleItem(it) })
}
}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/MarkwonSample.kt b/app-sample/src/main/java/io/noties/markwon/app/MarkwonSample.kt
deleted file mode 100644
index ed4f4d43..00000000
--- a/app-sample/src/main/java/io/noties/markwon/app/MarkwonSample.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package io.noties.markwon.app
-
-abstract class MarkwonSample {
-
-}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/MarkwonSampleItem.kt b/app-sample/src/main/java/io/noties/markwon/app/Sample.kt
similarity index 71%
rename from app-sample/src/main/java/io/noties/markwon/app/MarkwonSampleItem.kt
rename to app-sample/src/main/java/io/noties/markwon/app/Sample.kt
index 29942fba..5a82c7cb 100644
--- a/app-sample/src/main/java/io/noties/markwon/app/MarkwonSampleItem.kt
+++ b/app-sample/src/main/java/io/noties/markwon/app/Sample.kt
@@ -1,12 +1,15 @@
package io.noties.markwon.app
+import android.os.Parcelable
import io.noties.markwon.sample.annotations.MarkwonArtifact
+import kotlinx.android.parcel.Parcelize
-data class MarkwonSampleItem(
+@Parcelize
+data class Sample(
val javaClassName: String,
val id: String,
val title: String,
val description: String,
val artifacts: List,
val tags: List
-)
\ No newline at end of file
+) : Parcelable
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/SampleManager.kt b/app-sample/src/main/java/io/noties/markwon/app/SampleManager.kt
new file mode 100644
index 00000000..7addbabb
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/SampleManager.kt
@@ -0,0 +1,71 @@
+package io.noties.markwon.app
+
+import android.content.Context
+import io.noties.markwon.app.utils.Cancellable
+import io.noties.markwon.app.utils.SampleUtils
+import io.noties.markwon.sample.annotations.MarkwonArtifact
+import java.util.concurrent.ExecutorService
+
+class SampleManager(
+ private val context: Context,
+ private val executorService: ExecutorService
+) {
+
+ private val samples: List by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
+ SampleUtils.readSamples(context)
+ }
+
+ fun samples(search: SampleSearch?, callback: (List) -> Unit): Cancellable {
+
+ var action: ((List) -> Unit)? = callback
+
+ val future = executorService.submit {
+
+ val source = when (search) {
+ is SampleSearch.Artifact -> samples.filter { it.artifacts.contains(search.artifact) }
+ is SampleSearch.Tag -> samples.filter { it.tags.contains(search.tag) }
+ else -> samples.toList() // just copy all
+ }
+
+ val text = search?.text
+ val results = if (text == null) {
+ // no further filtering, just return the full source here
+ source
+ } else {
+ source.filter { filter(it, text) }
+ }
+
+ action?.invoke(results)
+ }
+
+ return object : Cancellable {
+ override val isCancelled: Boolean
+ get() = future.isDone
+
+ override fun cancel() {
+ action = null
+ future.cancel(true)
+ }
+ }
+ }
+
+ // if title contains,
+ // if description contains,
+ // if tags contains
+ // if artifacts contains,
+ private fun filter(sample: Sample, text: String): Boolean {
+ return sample.javaClassName.contains(text, true)
+ || sample.title.contains(text, true)
+ || sample.description.contains(text, true)
+ || filterTags(sample.tags, text)
+ || filterArtifacts(sample.artifacts, text)
+ }
+
+ private fun filterTags(tags: List, text: String): Boolean {
+ return tags.firstOrNull { it.contains(text, true) } != null
+ }
+
+ private fun filterArtifacts(artifacts: List, text: String): Boolean {
+ return artifacts.firstOrNull { it.artifactName().contains(text, true) } != null
+ }
+}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/SampleSearch.kt b/app-sample/src/main/java/io/noties/markwon/app/SampleSearch.kt
new file mode 100644
index 00000000..6f79aef4
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/SampleSearch.kt
@@ -0,0 +1,13 @@
+package io.noties.markwon.app
+
+import io.noties.markwon.sample.annotations.MarkwonArtifact
+
+sealed class SampleSearch(val text: String?) {
+ class Artifact(text: String?, val artifact: MarkwonArtifact) : SampleSearch(text)
+ class Tag(text: String?, val tag: String) : SampleSearch(text)
+ class All(text: String?) : SampleSearch(text)
+
+ override fun toString(): String {
+ return "SampleSearch(text=$text,type=${javaClass.simpleName})"
+ }
+}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/adapt/ArtifactItem.kt b/app-sample/src/main/java/io/noties/markwon/app/adapt/ArtifactItem.kt
deleted file mode 100644
index 1dcfe5c0..00000000
--- a/app-sample/src/main/java/io/noties/markwon/app/adapt/ArtifactItem.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package io.noties.markwon.app.adapt
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.TextView
-import io.noties.adapt.Item
-import io.noties.markwon.sample.annotations.MarkwonArtifact
-
-class ArtifactItem(artifact: MarkwonArtifact): Item(artifact.name.hashCode().toLong()) {
-
- override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
- return Holder(inflater.inflate(0, parent, false))
- }
-
- override fun render(holder: Holder) {
- }
-
- class Holder(itemView: View): Item.Holder(itemView) {
- val textView: TextView = requireView(0)
- }
-}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/adapt/SampleItem.kt b/app-sample/src/main/java/io/noties/markwon/app/adapt/SampleItem.kt
index 9aa75ff9..64d1c605 100644
--- a/app-sample/src/main/java/io/noties/markwon/app/adapt/SampleItem.kt
+++ b/app-sample/src/main/java/io/noties/markwon/app/adapt/SampleItem.kt
@@ -1,89 +1,100 @@
package io.noties.markwon.app.adapt
-import android.graphics.Color
-import android.text.Spannable
-import android.text.SpannableStringBuilder
-import android.text.TextPaint
-import android.text.method.LinkMovementMethod
-import android.text.style.ClickableSpan
+import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.noties.adapt.Item
-import io.noties.debug.Debug
-import io.noties.markwon.app.MarkwonSampleItem
+import io.noties.markwon.Markwon
import io.noties.markwon.app.R
+import io.noties.markwon.app.Sample
+import io.noties.markwon.app.base.FlowLayout
+import io.noties.markwon.app.utils.displayName
+import io.noties.markwon.app.utils.hidden
+import io.noties.markwon.app.utils.tagDisplayName
import io.noties.markwon.sample.annotations.MarkwonArtifact
+import io.noties.markwon.utils.NoCopySpannableFactory
-class SampleItem(private val item: MarkwonSampleItem) : Item(item.id.hashCode().toLong()) {
+class SampleItem(
+ private val markwon: Markwon,
+ private val sample: Sample,
+ private val onArtifactClick: (MarkwonArtifact) -> Unit,
+ private val onTagClick: (String) -> Unit,
+ private val onSampleClick: (Sample) -> Unit
+) : Item(sample.id.hashCode().toLong()) {
- var search: String? = null
+// var search: String? = null
+
+ private val text: Spanned by lazy(LazyThreadSafetyMode.NONE) {
+ markwon.toMarkdown(sample.description)
+ }
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
- val holder = Holder(inflater.inflate(R.layout.adapt_sample, parent, false))
- holder.artifactsAndTags.movementMethod = LinkMovementMethod.getInstance()
- return holder
+ return Holder(inflater.inflate(R.layout.adapt_sample, parent, false)).apply {
+ description.setSpannableFactory(NoCopySpannableFactory.getInstance())
+ }
}
override fun render(holder: Holder) {
holder.apply {
- title.text = item.title
- description.text = item.description
- artifactsAndTags.text = buildArtifactsAndTags
- }
- }
+ title.text = sample.title
- private val buildArtifactsAndTags: CharSequence
- get() {
- val builder = SpannableStringBuilder()
-
- item.artifacts
- .forEach {
- val length = builder.length
- builder.append("\u00a0${it.name}\u00a0")
- builder.setSpan(ArtifactSpan(it), length, builder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
-
- if (!builder.isEmpty()) {
- builder.append("\n")
+ val text = this@SampleItem.text
+ if (text.isEmpty()) {
+ description.text = ""
+ description.hidden = true
+ } else {
+ markwon.setParsedMarkdown(description, text)
+ description.hidden = false
}
- item.tags
- .forEach {
- val length = builder.length
- builder.append("\u00a0$it\u00a0")
- builder.setSpan(TagSpan(it), length, builder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ // there is no need to display the core artifact (it is implicit),
+ // hide if empty (removed core)
+ artifacts.ensure(sample.artifacts.size, R.layout.view_artifact)
+ .zip(sample.artifacts)
+ .forEach { (view, artifact) ->
+ (view as TextView).text = artifact.displayName
+ view.setOnClickListener {
+ onArtifactClick(artifact)
+ }
}
- return builder
+ tags.ensure(sample.tags.size, R.layout.view_tag)
+ .zip(sample.tags)
+ .forEach { (view, tag) ->
+ (view as TextView).text = tag.tagDisplayName
+ view.setOnClickListener {
+ onTagClick(tag)
+ }
+ }
+
+ itemView.setOnClickListener {
+ onSampleClick(sample)
+ }
}
+ }
class Holder(itemView: View) : Item.Holder(itemView) {
val title: TextView = requireView(R.id.title)
val description: TextView = requireView(R.id.description)
- val artifactsAndTags: TextView = requireView(R.id.artifacts_and_tags)
+ val artifacts: FlowLayout = requireView(R.id.artifacts)
+ val tags: FlowLayout = requireView(R.id.tags)
}
+}
- private class ArtifactSpan(val artifact: MarkwonArtifact) : ClickableSpan() {
- override fun onClick(widget: View) {
- Debug.i("clicked artifact: $artifact")
+private fun FlowLayout.ensure(viewsCount: Int, layoutResId: Int): List {
+ if (viewsCount > childCount) {
+ // inflate new views
+ val inflater = LayoutInflater.from(context)
+ for (i in 0 until (viewsCount - childCount)) {
+ addView(inflater.inflate(layoutResId, this, false))
}
-
- override fun updateDrawState(ds: TextPaint) {
- ds.isUnderlineText = false
- ds.bgColor = Color.GREEN
+ } else {
+ // return requested vies and GONE the rest
+ for (i in viewsCount until childCount) {
+ getChildAt(i).hidden = true
}
}
-
- private class TagSpan(val tag: String) : ClickableSpan() {
- override fun onClick(widget: View) {
- Debug.i("clicked tag: $tag")
- }
-
- override fun updateDrawState(ds: TextPaint) {
- ds.isUnderlineText = false
- ds.bgColor = Color.BLUE
- }
- }
-}
\ No newline at end of file
+ return (0 until viewsCount).map { getChildAt(it) }
+}
diff --git a/app-sample/src/main/java/io/noties/markwon/app/base/FlowLayout.kt b/app-sample/src/main/java/io/noties/markwon/app/base/FlowLayout.kt
index 6aa1ad10..be8fca71 100644
--- a/app-sample/src/main/java/io/noties/markwon/app/base/FlowLayout.kt
+++ b/app-sample/src/main/java/io/noties/markwon/app/base/FlowLayout.kt
@@ -3,8 +3,25 @@ package io.noties.markwon.app.base
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
+import io.noties.markwon.app.R
+import kotlin.math.max
class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) {
+
+ private val spacingVertical: Int
+ private val spacingHorizontal: Int
+
+ init {
+ val array = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout)
+ try {
+ val spacing = array.getDimensionPixelSize(R.styleable.FlowLayout_fl_spacing, 0)
+ spacingVertical = array.getDimensionPixelSize(R.styleable.FlowLayout_fl_spacingVertical, spacing)
+ spacingHorizontal = array.getDimensionPixelSize(R.styleable.FlowLayout_fl_spacingHorizontal, spacing)
+ } finally {
+ array.recycle()
+ }
+ }
+
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in 0 until childCount) {
val child = getChildAt(i)
@@ -21,7 +38,61 @@ class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, att
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ val width = MeasureSpec.getSize(widthMeasureSpec)
+
+ // we must have width (match_parent or exact dimension)
+ if (width <= 0) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ return
+ }
+
+ val availableWidth = width - paddingLeft - paddingRight
+
+ // child must not exceed our width
+ val childWidthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST)
+
+ // we also could enforce flexible height here (instead of exact one)
+ val childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+
+ var x = 0
+ var y = 0
+
+ var lineHeight = 0
+
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+
+ // measure
+ child.measure(childWidthSpec, childHeightSpec)
+
+ val params = child.layoutParams as LayoutParams
+ val measuredWidth = child.measuredWidth
+
+ if (measuredWidth > (availableWidth - x)) {
+ // new line
+ // make next child start at child measure width (starting at x = 0)
+ params.x = 0
+ params.y = y + lineHeight + spacingVertical
+
+ x = measuredWidth + spacingHorizontal
+ // move vertically by max value of child height on this line
+ y += lineHeight + spacingVertical
+
+ lineHeight = child.measuredHeight
+
+ } else {
+ // we fit this line
+ params.x = x
+ params.y = y
+
+ x += measuredWidth + spacingHorizontal
+ lineHeight = max(lineHeight, child.measuredHeight)
+ }
+ }
+
+ val height = y + lineHeight + paddingTop + paddingBottom
+
+ setMeasuredDimension(width, height)
}
override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
diff --git a/app-sample/src/main/java/io/noties/markwon/app/base/SearchBar.kt b/app-sample/src/main/java/io/noties/markwon/app/base/SearchBar.kt
index 6ab10838..ba4f644d 100644
--- a/app-sample/src/main/java/io/noties/markwon/app/base/SearchBar.kt
+++ b/app-sample/src/main/java/io/noties/markwon/app/base/SearchBar.kt
@@ -61,6 +61,12 @@ class SearchBar(context: Context, attrs: AttributeSet?) : LinearLayout(context,
clear.setOnClickListener {
textField.setText("")
+ // ensure that we have focus when clear is clicked
+ if (!textField.hasFocus()) {
+ textField.requestFocus()
+ // additionally ensure keyboard is showing
+ KeyboardUtils.show(textField)
+ }
}
cancel.setOnClickListener {
@@ -69,6 +75,10 @@ class SearchBar(context: Context, attrs: AttributeSet?) : LinearLayout(context,
}
}
+ fun search(text: String) {
+ textField.setText(text)
+ }
+
private fun textFieldChanged(text: CharSequence) {
val isEmpty = text.isEmpty()
clear.hidden = isEmpty
diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/FirstSample.kt b/app-sample/src/main/java/io/noties/markwon/app/samples/FirstSample.kt
index 20b65e25..f7c484b4 100644
--- a/app-sample/src/main/java/io/noties/markwon/app/samples/FirstSample.kt
+++ b/app-sample/src/main/java/io/noties/markwon/app/samples/FirstSample.kt
@@ -1,6 +1,6 @@
package io.noties.markwon.app.samples
-import io.noties.markwon.app.MarkwonSample
+import io.noties.markwon.app.ui.MarkwonSample
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
diff --git a/app-sample/src/main/java/io/noties/markwon/app/ui/MarkwonSample.kt b/app-sample/src/main/java/io/noties/markwon/app/ui/MarkwonSample.kt
new file mode 100644
index 00000000..c339bcf0
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/ui/MarkwonSample.kt
@@ -0,0 +1,5 @@
+package io.noties.markwon.app.ui
+
+abstract class MarkwonSample {
+
+}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/ui/SampleFragment.kt b/app-sample/src/main/java/io/noties/markwon/app/ui/SampleFragment.kt
new file mode 100644
index 00000000..ae5babe6
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/ui/SampleFragment.kt
@@ -0,0 +1,20 @@
+package io.noties.markwon.app.ui
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import io.noties.markwon.app.Sample
+
+class SampleFragment : Fragment() {
+
+ companion object {
+ private const val ARG_SAMPLE = "arg.Sample"
+
+ fun init(sample: Sample): SampleFragment {
+ val fragment = SampleFragment()
+ fragment.arguments = Bundle().apply {
+ putParcelable(ARG_SAMPLE, sample)
+ }
+ return fragment
+ }
+ }
+}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/ui/SampleListFragment.kt b/app-sample/src/main/java/io/noties/markwon/app/ui/SampleListFragment.kt
new file mode 100644
index 00000000..57f906ca
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/ui/SampleListFragment.kt
@@ -0,0 +1,288 @@
+package io.noties.markwon.app.ui
+
+import android.content.Context
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.DefaultItemAnimator
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.noties.adapt.Adapt
+import io.noties.adapt.DiffUtilDataSetChanged
+import io.noties.debug.Debug
+import io.noties.markwon.Markwon
+import io.noties.markwon.app.App
+import io.noties.markwon.app.R
+import io.noties.markwon.app.Sample
+import io.noties.markwon.app.SampleManager
+import io.noties.markwon.app.SampleSearch
+import io.noties.markwon.app.adapt.SampleItem
+import io.noties.markwon.app.base.SearchBar
+import io.noties.markwon.app.utils.Cancellable
+import io.noties.markwon.app.utils.displayName
+import io.noties.markwon.app.utils.onPreDraw
+import io.noties.markwon.app.utils.recyclerView
+import io.noties.markwon.app.utils.tagDisplayName
+import io.noties.markwon.movement.MovementMethodPlugin
+import io.noties.markwon.sample.annotations.MarkwonArtifact
+import kotlinx.android.parcel.Parcelize
+
+class SampleListFragment : Fragment() {
+
+ private val adapt: Adapt = Adapt.create(DiffUtilDataSetChanged.create())
+ private lateinit var markwon: Markwon
+
+ private val type: Type by lazy(LazyThreadSafetyMode.NONE) {
+ parseType(arguments!!)
+ }
+
+ private var search: String? = null
+
+ // postpone state restoration
+ private var pendingRecyclerScrollPosition: RecyclerScrollPosition? = null
+
+ private var cancellable: Cancellable? = null
+
+ private val sampleManager: SampleManager
+ get() = App.sampleManager
+
+ override fun onAttach(context: Context?) {
+ super.onAttach(context)
+
+ context?.also {
+ markwon = markwon(it)
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_sample_list, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ initAppBar(view)
+
+ val context = requireContext()
+
+ val searchBar: SearchBar = view.findViewById(R.id.search_bar)
+ searchBar.onSearchListener = {
+ search = it
+ fetch()
+ }
+
+ val recyclerView: RecyclerView = view.findViewById(R.id.recycler_view)
+ recyclerView.layoutManager = LinearLayoutManager(context)
+ recyclerView.itemAnimator = DefaultItemAnimator()
+ recyclerView.setHasFixedSize(true)
+ recyclerView.adapter = adapt
+
+ // additional padding for RecyclerView
+ searchBar.onPreDraw {
+ recyclerView.setPadding(
+ recyclerView.paddingLeft,
+ recyclerView.paddingTop + searchBar.height,
+ recyclerView.paddingRight,
+ recyclerView.paddingBottom
+ )
+ }
+
+ val state: State? = savedInstanceState?.getParcelable(STATE)
+ pendingRecyclerScrollPosition = state?.recyclerScrollPosition
+ if (state?.search != null) {
+ searchBar.search(state.search)
+ } else {
+ fetch()
+ }
+ }
+
+ override fun onDestroyView() {
+ val cancellable = this.cancellable
+ if (cancellable != null && !cancellable.isCancelled) {
+ cancellable.cancel()
+ this.cancellable = null
+ }
+ super.onDestroyView()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+
+ val state = State(
+ search,
+ adapt.recyclerView?.scrollPosition
+ )
+ outState.putParcelable(STATE, state)
+ }
+
+ private fun initAppBar(view: View) {
+ val appBar = view.findViewById(R.id.app_bar)
+
+ val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon)
+ val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title)
+
+ val type = this.type
+ if (type is Type.All) {
+ return
+ }
+
+ appBarIcon.setImageResource(R.drawable.ic_arrow_back_white_24dp)
+ appBarIcon.setOnClickListener {
+ requireActivity().onBackPressed()
+ }
+
+ val (text, background) = when (type) {
+ is Type.Artifact -> Pair(type.artifact.displayName, R.drawable.bg_artifact)
+ is Type.Tag -> Pair(type.tag.tagDisplayName, R.drawable.bg_tag)
+ else -> error("Unexpected type: $type")
+ }
+
+ appBarTitle.text = text
+ appBarTitle.setBackgroundResource(background)
+ }
+
+ private fun bindSamples(samples: List) {
+ val items = samples.map {
+ SampleItem(
+ markwon,
+ it,
+ { artifact -> openArtifact(artifact) },
+ { tag -> openTag(tag) },
+ { sample -> openSample(sample) }
+ )
+ }
+ adapt.setItems(items)
+
+ val scrollPosition = pendingRecyclerScrollPosition
+ if (scrollPosition != null) {
+ pendingRecyclerScrollPosition = null
+ val recyclerView = adapt.recyclerView ?: return
+ recyclerView.onPreDraw {
+ (recyclerView.layoutManager as? LinearLayoutManager)
+ ?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset)
+ }
+ }
+ }
+
+ private fun openArtifact(artifact: MarkwonArtifact) {
+ Debug.i(artifact)
+ openResultFragment(init(artifact))
+ }
+
+ private fun openTag(tag: String) {
+ Debug.i(tag)
+ openResultFragment(init(tag))
+ }
+
+ private fun openResultFragment(fragment: SampleListFragment) {
+ openFragment(fragment)
+ }
+
+ private fun openSample(sample: Sample) {
+ openFragment(SampleFragment.init(sample))
+ }
+
+ private fun openFragment(fragment: Fragment) {
+ fragmentManager!!.beginTransaction()
+ .setCustomAnimations(R.anim.screen_in, R.anim.screen_out, R.anim.screen_in_pop, R.anim.screen_out_pop)
+ .replace(Window.ID_ANDROID_CONTENT, fragment)
+ .addToBackStack(null)
+ .commitAllowingStateLoss()
+ }
+
+ private fun fetch() {
+
+ val sampleSearch: SampleSearch = when (val type = this.type) {
+ is Type.Artifact -> SampleSearch.Artifact(search, type.artifact)
+ is Type.Tag -> SampleSearch.Tag(search, type.tag)
+ else -> SampleSearch.All(search)
+ }
+
+ // clear current
+ cancellable?.let {
+ if (!it.isCancelled) {
+ it.cancel()
+ }
+ }
+
+ cancellable = sampleManager.samples(sampleSearch) {
+ bindSamples(it)
+ }
+ }
+
+ companion object {
+ private const val ARG_ARTIFACT = "arg.Artifact"
+ private const val ARG_TAG = "arg.Tag"
+ private const val STATE = "key.State"
+
+ fun init(): SampleListFragment {
+ val fragment = SampleListFragment()
+ fragment.arguments = Bundle()
+ return fragment
+ }
+
+ fun init(artifact: MarkwonArtifact): SampleListFragment {
+ val fragment = SampleListFragment()
+ fragment.arguments = Bundle().apply {
+ putString(ARG_ARTIFACT, artifact.name)
+ }
+ return fragment
+ }
+
+ fun init(tag: String): SampleListFragment {
+ val fragment = SampleListFragment()
+ fragment.arguments = Bundle().apply {
+ putString(ARG_TAG, tag)
+ }
+ return fragment
+ }
+
+ fun markwon(context: Context): Markwon {
+ return Markwon.builder(context)
+ .usePlugin(MovementMethodPlugin.none())
+ .build()
+ }
+
+ private fun parseType(arguments: Bundle): Type {
+ val name = arguments.getString(ARG_ARTIFACT)
+ val tag = arguments.getString(ARG_TAG)
+ return when {
+ name != null -> Type.Artifact(MarkwonArtifact.valueOf(name))
+ tag != null -> Type.Tag(tag)
+ else -> Type.All
+ }
+ }
+ }
+
+ @Parcelize
+ private data class State(
+ val search: String?,
+ val recyclerScrollPosition: RecyclerScrollPosition?
+ ) : Parcelable
+
+ @Parcelize
+ private data class RecyclerScrollPosition(
+ val position: Int,
+ val offset: Int
+ ) : Parcelable
+
+ private val RecyclerView.scrollPosition: RecyclerScrollPosition?
+ get() {
+ val holder = findViewHolderForLayoutPosition(0) ?: return null
+ val position = holder.adapterPosition
+ val offset = holder.itemView.top
+ return RecyclerScrollPosition(position, offset)
+ }
+
+ private sealed class Type {
+ class Artifact(val artifact: MarkwonArtifact) : Type()
+ class Tag(val tag: String) : Type()
+ object All : Type()
+ }
+}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/utils/AdaptUtils.kt b/app-sample/src/main/java/io/noties/markwon/app/utils/AdaptUtils.kt
new file mode 100644
index 00000000..0c4637e8
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/utils/AdaptUtils.kt
@@ -0,0 +1,16 @@
+package io.noties.markwon.app.utils
+
+import androidx.recyclerview.widget.RecyclerView
+import io.noties.adapt.Adapt
+import io.noties.debug.Debug
+
+val Adapt.recyclerView: RecyclerView?
+ get() {
+ // internally throws if recycler is not present (detached from recyclerView)
+ return try {
+ recyclerView()
+ } catch (t: Throwable) {
+ Debug.e(t)
+ null
+ }
+ }
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/utils/Cancellable.kt b/app-sample/src/main/java/io/noties/markwon/app/utils/Cancellable.kt
new file mode 100644
index 00000000..4f519ad4
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/utils/Cancellable.kt
@@ -0,0 +1,7 @@
+package io.noties.markwon.app.utils
+
+interface Cancellable {
+ val isCancelled: Boolean
+
+ fun cancel()
+}
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/utils/SampleUtils.java b/app-sample/src/main/java/io/noties/markwon/app/utils/SampleUtils.java
new file mode 100644
index 00000000..2f4195e3
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/utils/SampleUtils.java
@@ -0,0 +1,37 @@
+package io.noties.markwon.app.utils;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+
+import io.noties.markwon.app.Sample;
+
+public abstract class SampleUtils {
+
+ @NonNull
+ public static List readSamples(@NonNull Context context) {
+
+ final Gson gson = new Gson();
+
+ try (InputStream inputStream = context.getAssets().open("samples.json")) {
+ return gson.fromJson(
+ new InputStreamReader(inputStream),
+ new TypeToken>() {
+ }.getType()
+ );
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private SampleUtils() {
+ }
+}
diff --git a/app-sample/src/main/java/io/noties/markwon/app/utils/SampleUtilsKt.kt b/app-sample/src/main/java/io/noties/markwon/app/utils/SampleUtilsKt.kt
new file mode 100644
index 00000000..c01f251a
--- /dev/null
+++ b/app-sample/src/main/java/io/noties/markwon/app/utils/SampleUtilsKt.kt
@@ -0,0 +1,9 @@
+package io.noties.markwon.app.utils
+
+import io.noties.markwon.sample.annotations.MarkwonArtifact
+
+val MarkwonArtifact.displayName: String
+ get() = "@${artifactName()}"
+
+val String.tagDisplayName: String
+ get() = "#$this"
\ No newline at end of file
diff --git a/app-sample/src/main/java/io/noties/markwon/app/utils/ViewUtils.kt b/app-sample/src/main/java/io/noties/markwon/app/utils/ViewUtils.kt
index 497ba370..b6a5e8f9 100644
--- a/app-sample/src/main/java/io/noties/markwon/app/utils/ViewUtils.kt
+++ b/app-sample/src/main/java/io/noties/markwon/app/utils/ViewUtils.kt
@@ -3,9 +3,24 @@ package io.noties.markwon.app.utils
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
+import android.view.ViewTreeObserver
var View.hidden: Boolean
get() = visibility == GONE
set(value) {
visibility = if (value) GONE else VISIBLE
- }
\ No newline at end of file
+ }
+
+fun View.onPreDraw(action: () -> Unit) {
+ viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ val vto = viewTreeObserver
+ if (vto.isAlive) {
+ vto.removeOnPreDrawListener(this)
+ }
+ action()
+ // do not block drawing
+ return true
+ }
+ })
+}
\ No newline at end of file
diff --git a/app-sample/src/main/res/anim/screen_in.xml b/app-sample/src/main/res/anim/screen_in.xml
new file mode 100644
index 00000000..ee314aa4
--- /dev/null
+++ b/app-sample/src/main/res/anim/screen_in.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/anim/screen_in_pop.xml b/app-sample/src/main/res/anim/screen_in_pop.xml
new file mode 100644
index 00000000..44de9d54
--- /dev/null
+++ b/app-sample/src/main/res/anim/screen_in_pop.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/anim/screen_out.xml b/app-sample/src/main/res/anim/screen_out.xml
new file mode 100644
index 00000000..4ce2f3e6
--- /dev/null
+++ b/app-sample/src/main/res/anim/screen_out.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/anim/screen_out_pop.xml b/app-sample/src/main/res/anim/screen_out_pop.xml
new file mode 100644
index 00000000..dd168243
--- /dev/null
+++ b/app-sample/src/main/res/anim/screen_out_pop.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/drawable/bg_search_bar.xml b/app-sample/src/main/res/drawable/bg_search_bar.xml
new file mode 100644
index 00000000..c7417ecd
--- /dev/null
+++ b/app-sample/src/main/res/drawable/bg_search_bar.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/drawable/bg_splash.xml b/app-sample/src/main/res/drawable/bg_splash.xml
new file mode 100644
index 00000000..66e4b273
--- /dev/null
+++ b/app-sample/src/main/res/drawable/bg_splash.xml
@@ -0,0 +1,14 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/drawable/ic_arrow_back_white_24dp.xml b/app-sample/src/main/res/drawable/ic_arrow_back_white_24dp.xml
new file mode 100644
index 00000000..fa122e18
--- /dev/null
+++ b/app-sample/src/main/res/drawable/ic_arrow_back_white_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app-sample/src/main/res/layout/adapt_artifact.xml b/app-sample/src/main/res/layout/adapt_artifact.xml
deleted file mode 100644
index 27d5d894..00000000
--- a/app-sample/src/main/res/layout/adapt_artifact.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app-sample/src/main/res/layout/adapt_sample.xml b/app-sample/src/main/res/layout/adapt_sample.xml
index 8499e6a7..2885673d 100644
--- a/app-sample/src/main/res/layout/adapt_sample.xml
+++ b/app-sample/src/main/res/layout/adapt_sample.xml
@@ -1,51 +1,87 @@
-
+ android:orientation="vertical"
+ android:paddingTop="@dimen/content_padding_half"
+ android:paddingEnd="@dimen/content_padding"
+ android:paddingBottom="@dimen/content_padding">
-
-
-
-
+ android:orientation="horizontal">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
\ No newline at end of file
+
diff --git a/app-sample/src/main/res/layout/activity_main.xml b/app-sample/src/main/res/layout/fragment_sample_list.xml
similarity index 50%
rename from app-sample/src/main/res/layout/activity_main.xml
rename to app-sample/src/main/res/layout/fragment_sample_list.xml
index 9a866cab..a5c4ec48 100644
--- a/app-sample/src/main/res/layout/activity_main.xml
+++ b/app-sample/src/main/res/layout/fragment_sample_list.xml
@@ -7,25 +7,39 @@
android:orientation="vertical">
+ android:orientation="horizontal"
+ tools:ignore="UseCompoundDrawables">
-
+ android:layout_marginEnd="@dimen/app_bar_height">
+
+
+
+
@@ -37,13 +51,18 @@
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:translationY="56dip" />
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:overScrollMode="never"
+ android:paddingBottom="36dip"
+ tools:layout_marginTop="56dip" />
diff --git a/app-sample/src/main/res/layout/sample_text_view.xml b/app-sample/src/main/res/layout/sample_text_view.xml
new file mode 100644
index 00000000..bde44965
--- /dev/null
+++ b/app-sample/src/main/res/layout/sample_text_view.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/layout/view_artifact.xml b/app-sample/src/main/res/layout/view_artifact.xml
new file mode 100644
index 00000000..2d1e5049
--- /dev/null
+++ b/app-sample/src/main/res/layout/view_artifact.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/layout/view_search_bar.xml b/app-sample/src/main/res/layout/view_search_bar.xml
index de614f06..56a04e50 100644
--- a/app-sample/src/main/res/layout/view_search_bar.xml
+++ b/app-sample/src/main/res/layout/view_search_bar.xml
@@ -1,7 +1,7 @@
@@ -31,10 +32,10 @@
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/values/attrs.xml b/app-sample/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..b20ac7fb
--- /dev/null
+++ b/app-sample/src/main/res/values/attrs.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/values/colors.xml b/app-sample/src/main/res/values/colors.xml
index 7f60f058..0703abc1 100644
--- a/app-sample/src/main/res/values/colors.xml
+++ b/app-sample/src/main/res/values/colors.xml
@@ -6,6 +6,10 @@
#999999
#FFFFFF
- #EEEEEE
+ @color/white
#FF0000
+
+ #eee
+ #BFFFFFFF
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/values/dimens.xml b/app-sample/src/main/res/values/dimens.xml
index c4f1ce17..2b409b32 100644
--- a/app-sample/src/main/res/values/dimens.xml
+++ b/app-sample/src/main/res/values/dimens.xml
@@ -4,6 +4,7 @@
8dip
16dip
+ 4dip
36dip
36dip
diff --git a/app-sample/src/main/res/values/ids.xml b/app-sample/src/main/res/values/ids.xml
new file mode 100644
index 00000000..15c85d84
--- /dev/null
+++ b/app-sample/src/main/res/values/ids.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app-sample/src/main/res/values/styles.xml b/app-sample/src/main/res/values/styles.xml
index 19eec27c..e1416f34 100644
--- a/app-sample/src/main/res/values/styles.xml
+++ b/app-sample/src/main/res/values/styles.xml
@@ -1,11 +1,12 @@
-
+
@@ -23,4 +24,19 @@
- ?android:attr/selectableItemBackgroundBorderless
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 8d0b833d..d02d46bc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
}
dependencies {
// on `3.5.3` tests are not run from CLI
- classpath 'com.android.tools.build:gradle:3.5.2'
+ classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
@@ -69,6 +69,8 @@ ext {
'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0',
'x-core' : 'androidx.core:core:1.0.2',
'x-appcompat' : 'androidx.appcompat:appcompat:1.1.0',
+ 'x-cardview' : 'androidx.cardview:cardview:1.0.0',
+ 'x-fragment' : 'androidx.fragment:fragment:1.0.0',
'commonmark' : "com.atlassian.commonmark:commonmark:$commonMarkVersion",
'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion",
'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion",
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 430dfabc..76c5eda1 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,6 @@
+#Wed Jun 17 17:05:04 MSK 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonArtifact.java b/sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonArtifact.java
index d9abd329..dffc0da4 100644
--- a/sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonArtifact.java
+++ b/sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonArtifact.java
@@ -1,5 +1,9 @@
package io.noties.markwon.sample.annotations;
+import androidx.annotation.NonNull;
+
+import java.util.Locale;
+
public enum MarkwonArtifact {
CORE,
EDITOR,
@@ -17,5 +21,10 @@ public enum MarkwonArtifact {
RECYCLER,
RECYCLER_TABLE,
SIMPLE_EXT,
- SYNTAX_HIGHLIGHT
+ SYNTAX_HIGHLIGHT;
+
+ @NonNull
+ public String artifactName() {
+ return name().toLowerCase(Locale.US).replace('_', '-');
+ }
}