Commit 8579f4f8 authored by Christophe Henry's avatar Christophe Henry

Merge branch 'share-template' into 'develop'

Adds template system for share intent

See merge request !6
parents aa31f92a d51a77b8
......@@ -126,6 +126,7 @@ dependencies {
implementation "joda-time:joda-time:2.10.1"
implementation "com.squareup.picasso:picasso:2.71828"
implementation "io.github.luizgrp.sectionedrecyclerviewadapter:sectionedrecyclerviewadapter:2.0.0"
implementation "com.x5dev:chunk-templates:3.4.0"
// Tests
testImplementation "junit:junit:4.12"
......
......@@ -11,19 +11,16 @@ import androidx.core.view.MenuItemCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.*
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptionarticles.shareintent.ShareIntent
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.database.models.Article
import fr.chenry.android.freshrss.store.database.models.ReadStatus
import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
import fr.chenry.android.freshrss.store.viewmodels.SubscriptionArticleVM
import fr.chenry.android.freshrss.store.viewmodels.SubscriptionArticleVMF
import fr.chenry.android.freshrss.utils.capitalizeFull
import fr.chenry.android.freshrss.utils.e
import kotlinx.android.synthetic.main.fragment_subscription_article_detail.*
import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.failUi
import java.util.regex.Pattern
import kotlin.text.RegexOption.IGNORE_CASE
class SubscriptionArticlesDetailFragment: Fragment() {
private lateinit var articleId: String
......@@ -34,6 +31,7 @@ class SubscriptionArticlesDetailFragment: Fragment() {
}
private var isFetching = MutableLiveData<Boolean>().apply {value = false}
private val article: Article get() = model.liveData.value!!
private val subscription: Subscription get() = model.subscription
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -82,7 +80,7 @@ class SubscriptionArticlesDetailFragment: Fragment() {
private fun setupShareAction(menu: Menu) {
menu.findItem(R.id.action_share)
.let {MenuItemCompat.getActionProvider(it) as ShareActionProvider}
.setShareIntent(ShareIntent())
.setShareIntent(ShareIntent(getTemplateAttributes()))
}
private fun setupOpenInBrowser(menu: Menu) {
......@@ -143,20 +141,11 @@ class SubscriptionArticlesDetailFragment: Fragment() {
return true
}
inner class ShareIntent: Intent(Intent.ACTION_SEND) {
private val stripAuthorRegex = "\\s*[^\\p{L}]*\\s*${Pattern.quote(article.author)}\\s*\\W*\\s*".toRegex(IGNORE_CASE)
private val articleTitle get() = stripAuthorRegex.replace(article.title, "").capitalize()
private val articleAuthor get() = if(article.author.isBlank()) "" else "— ${article.author}".capitalizeFull()
private val articleShareString get() = """
$articleTitle $articleAuthor
${article.href}
""".trimIndent().trim()
init {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, articleShareString)
}
}
private fun getTemplateAttributes() = mapOf(
"subscription" to subscription.title,
"author" to article.author,
"title" to article.title,
"content" to article.content,
"href" to article.href
)
}
package fr.chenry.android.freshrss.components.subscriptionarticles.shareintent
import android.content.Intent
import com.x5.template.Theme
import fr.chenry.android.freshrss.components.subscriptionarticles.shareintent.chunkfilters.SentenceCapFilter
import fr.chenry.android.freshrss.components.subscriptionarticles.shareintent.chunkfilters.StripFragmentFilter
class ShareIntent(attributes: Map<String, String>): Intent(Intent.ACTION_SEND) {
init {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, format(attributes))
}
companion object {
private val defaultTemplate = """
|{${'$'}title|strip_fragment(subscription)|sentence_cap} — {${'$'}subscription|capitalize}
|{${'$'}href}
""".trimMargin("|")
private val template = Theme().let {
it.registerFilter(SentenceCapFilter())
it.registerFilter(StripFragmentFilter())
it.makeChunk().apply {append(defaultTemplate)}
}
fun format(attributes: Map<String, String>) = template.apply {
attributes.forEach {t, u -> set(t, u)}
}.toString()
}
}
package fr.chenry.android.freshrss.components.subscriptionarticles.shareintent.chunkfilters
import com.x5.template.Chunk
import com.x5.template.filters.FilterArgs
import com.x5.template.filters.ObjectFilter
import java.util.regex.Pattern
import kotlin.text.RegexOption.IGNORE_CASE
// Will match any separator token, i.e, any word not containing at least a letter or a number
const val sepToken = "[\\S&&[^\\p{L}]&&[^\\d]]"
class SentenceCapFilter: ObjectFilter() {
override fun getFilterName() = "sentence_cap"
override fun transformObject(chunk: Chunk?, `object`: Any?, args: FilterArgs?) = `object`.toString().let {
var result = it
it.split("\\s*$sepToken+\\s*".toRegex())
.map {t ->
when {
t.isBlank() -> t
t.length == 1 -> t[0].toString().toUpperCase()
else -> "${t[0].toString().toUpperCase()}${t.substring(1).toLowerCase()}"
}
}
.forEach {t -> result = result.replace(t, t, ignoreCase = true)}
result
}
}
class StripFragmentFilter: ObjectFilter() {
override fun getFilterName() = "strip_fragment"
override fun transformObject(chunk: Chunk?, `object`: Any?, args: FilterArgs?) =
stripFragment(`object`.toString(), args?.getFilterArgs(chunk)?.getOrNull(0) ?: "")
companion object {
/**
* Will remove the author from the title and any separator character with it.
* For instance:
* >>>> stripFragment("My title - Author", "Author")
* "My title"
* >>>> stripFragment("Author | My title", "Author"))
* "My title"
* >>>> stripFragment("Series | Author | My title", "Author")
* "Series | My title"
*/
fun stripFragment(title: String, author: String): String {
return when {
// Case where author is in the middle of the title, for instance: "Video series #1 | Author | Title"
title.contains("\\s*$sepToken+\\s*${Pattern.quote(author)}\\s*$sepToken+\\s*".toRegex()) ->
"\\s*$sepToken*\\s*${Pattern.quote(author)}".toRegex(IGNORE_CASE).replace(title, "")
else ->
"\\s*$sepToken*\\s*${Pattern.quote(author)}\\s*$sepToken*\\s*".toRegex(IGNORE_CASE).replace(title, "")
}
}
}
}
......@@ -66,4 +66,4 @@ class MainSubscriptionFragment: Fragment(), BottomNavigationView.OnNavigationIte
Bundle().apply {putParcelable(SubscriptionsFragment.argumentKey, subscriptionSection)}
)
}
}
\ No newline at end of file
}
......@@ -59,7 +59,7 @@ enum class ReadStatus: Parcelable {
@Dao
interface ArticlesDAO {
@Insert(onConflict = OnConflictStrategy.FAIL)
@Insert(onConflict = OnConflictStrategy.ABORT)
fun insert(article: Article)
@Insert(onConflict = OnConflictStrategy.REPLACE)
......
package fr.chenry.android.freshrss.store.viewmodels
import androidx.lifecycle.*
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.store.database.FreshRSSDabatabase
import fr.chenry.android.freshrss.store.database.models.*
class SubscriptionArticleVM(private val articleId: ItemId): ViewModel() {
private val flowable by lazy { FreshRSSDabatabase.instance.getArticleById(articleId) }
private val source: LiveData<Articles> by lazy { flowable.toLiveData() }
val liveData: LiveData<Article> by lazy {
MutableLiveData<Article>().apply {
value = flowable.blockingFirst().first()
class SubscriptionArticleVM(articleId: ItemId): ViewModel() {
val liveData: LiveData<Article>
val subscription: Subscription
private val source: LiveData<Articles>
init {
val flowable = FreshRSSDabatabase.instance.getArticleById(articleId)
val article = flowable.blockingFirst().first()
source = flowable.toLiveData()
liveData = MutableLiveData<Article>().apply {
value = article
source.observeForever { value = it.first() }
}
subscription = FreshRSSApplication.database.getSubcriptionsById(article.streamId).blockingFirst().first()
}
}
......
package fr.chenry.android.freshrss.components.subscriptionarticles
import fr.chenry.android.freshrss.components.subscriptionarticles.shareintent.ShareIntent
import fr.chenry.android.freshrss.components.subscriptionarticles.shareintent.chunkfilters.StripFragmentFilter
import org.junit.Assert.assertEquals
import org.junit.Test
class ShareIntentTest {
private val attributes
get() = mapOf(
"subscription" to "The subscription",
"author" to "Humble Me",
"title" to "My Title - My Series #1 - THIS IS AWESOME!",
"content" to "Lorem ipsum",
"href" to "http://example.com"
)
@Test
fun stripAuthorTest() {
assertEquals("My title", StripFragmentFilter.stripFragment("My title - Author", "Author"))
assertEquals("My title", StripFragmentFilter.stripFragment("Author | My title", "Author"))
assertEquals("My title", StripFragmentFilter.stripFragment("Author | My title", "author"))
assertEquals("My title", StripFragmentFilter.stripFragment("author & My title", "Author"))
assertEquals(
"Video series #1 | Title",
StripFragmentFilter.stripFragment("Video series #1 | Author | Title", "Author")
)
}
@Test
fun format() {
assertEquals(
ShareIntent.format(attributes), """
|My title - My series #1 - This is awesome! — The Subscription
|http://example.com
""".trimMargin("|")
)
}
}
package fr.chenry.android.freshrss.store.api.models
import fr.chenry.android.freshrss.utils.JACKSON_OBJECT_MAPPER
import org.joda.time.*
import org.junit.*
import org.junit.Assert.assertEquals
class UnreadCountTest {
@Before
fun setUp() {
DateTimeUtils.setCurrentMillisFixed(System.currentTimeMillis())
}
@After
fun tearDown() {
DateTimeUtils.setCurrentMillisSystem()
}
@Test
fun testDeserialize() {
val value = JACKSON_OBJECT_MAPPER.readValue(
"""
{
"id": "feed/3",
"count": 0,
"newestItemTimestampUsec": "1552432206000000"
}
""".trimIndent(), UnreadCount::class.java
)
assertEquals(
UnreadCount("feed/3", 0, LocalDateTime("1552432206000000".toLong().div(1000), DateTimeZone.UTC)),
value
)
}
@Test
fun testDeserializeWithNullTimestamp() {
val value = JACKSON_OBJECT_MAPPER.readValue(
"""
{
"id": "feed/3",
"count": 0
}
""".trimIndent(), UnreadCount::class.java
)
assertEquals(UnreadCount("feed/3", 0, LocalDateTime.now()), value)
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment