Commit 1ce8a4a3 authored by Christophe Henry's avatar Christophe Henry

Big refacto

- Persists total unread count in DB
- Simplify bottom bar badge
- Clean styles
- Prevent drawer opening to display the burger menu
  when not on home page and tranform burger menu to
  arrow on opening
parent fae4f7e9
......@@ -107,6 +107,8 @@ spotless {
dependencies {
def appcompat_version = "1.1.0"
def activity_version = "1.1.0"
def fragment_version = "1.2.1"
def lifecycle_version = "2.2.0"
def room_version = '2.2.3'
def roomigrant_version = "0.1.7"
......@@ -133,6 +135,8 @@ dependencies {
// AndroidX
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation "androidx.core:core-ktx:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
......@@ -140,6 +144,9 @@ dependencies {
implementation "androidx.preference:preference-ktx:1.1.0"
implementation "com.google.android.material:material:1.2.0-alpha04"
// AndroidX testing
implementation "androidx.fragment:fragment-testing:$fragment_version"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
......
......@@ -17,7 +17,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsPictureInPicture="false"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:theme="@style/AppTheme.Bright"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity
......
......@@ -19,6 +19,7 @@ import org.acra.ACRA
import org.acra.annotation.*
import org.acra.data.StringFormat
import org.acra.sender.HttpSender
import java.util.Locale
@AcraCore(reportFormat = StringFormat.JSON)
@AcraHttpSender(
......@@ -41,7 +42,7 @@ class FreshRSSApplication: Application() {
val preferences: FreshRSSPreferences = FreshRSSPreferences(this)
val database by lazy {
val dbName = "${context.getString(R.string.app_name).toLowerCase()}.db"
val dbName = "${context.getString(R.string.app_name).toLowerCase(Locale.ENGLISH)}.db"
Room.databaseBuilder(context, FreshRSSDabatabase::class.java, dbName)
.addMigrations(*FreshRSSDabatabase_Migrations.build())
.build()
......
......@@ -20,9 +20,9 @@ import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import java.util.Properties
class LoginActivity : AppCompatActivity() {
class LoginActivity: AppCompatActivity() {
private lateinit var instanceUrl: InstanceUrl
private val inputManager by lazy {
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
}
......@@ -30,35 +30,40 @@ class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
instanceUrl = InstanceUrl(
InstanceUrl.authorizedProtocols[0],
resources.getString(R.string.instance_exemple)
)
setContentView(R.layout.activity_login)
// Set up the login form.
password.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) {
password.setOnEditorActionListener(TextView.OnEditorActionListener {_, id, _ ->
if(id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) {
attemptLogin()
return@OnEditorActionListener true
}
false
})
email_sign_in_button.setOnClickListener { attemptLogin() }
instanceUrl = InstanceUrl(InstanceUrl.authorizedProtocols[0], resources.getString(R.string.instance_exemple))
email_sign_in_button.setOnClickListener {attemptLogin()}
activity_login_protocol_selection.adapter =
ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, InstanceUrl.authorizedProtocols)
Try {
val properties = Properties()
properties.load(baseContext.assets.open("config.properties"))
instanceUrl = InstanceUrl.parse(properties.getProperty("instance"))
instance.setText(instanceUrl.base)
val protocolIdx = InstanceUrl.authorizedProtocols.indexOf(instanceUrl.protocole)
activity_login_protocol_selection.setSelection(if (protocolIdx >= 0) protocolIdx else 0)
login.setText(properties.getProperty("login"))
password.setText(properties.getProperty("password"))
}
val properties = Properties()
Try {properties.load(baseContext.assets.open("config.properties"))}
instanceUrl = Try {InstanceUrl.parse(properties.getProperty("instance"))}.getOrDefault(instanceUrl)
instance.setText(instanceUrl.base)
val protocolIdx = InstanceUrl.authorizedProtocols.indexOf(instanceUrl.protocole)
activity_login_protocol_selection.setSelection(if(protocolIdx >= 0) protocolIdx else 0)
Try{login.setText(properties.getProperty("login"))}
Try{password.setText(properties.getProperty("password"))}
computeInstanceURLHint()
instance.addTextChangedListener { computeInstanceURLHint() }
activity_login_protocol_selection.onItemSelectedListener = object : OnItemSelectedListener {
instance.addTextChangedListener {computeInstanceURLHint()}
activity_login_protocol_selection.onItemSelectedListener = object: OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) = computeInstanceURLHint()
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) =
computeInstanceURLHint()
......@@ -73,7 +78,7 @@ class LoginActivity : AppCompatActivity() {
var cancel = false
var focusView: View? = null
if (instanceUrl.isMalformed) {
if(instanceUrl.isMalformed) {
instance.error = getString(R.string.instance_url_malformed)
focusView = instance
cancel = true
......@@ -84,20 +89,20 @@ class LoginActivity : AppCompatActivity() {
val passwordStr = password.text.toString()
// Check for a valid password, if the user entered one.
if (passwordStr.isBlank()) {
if(passwordStr.isBlank()) {
password.error = getString(R.string.error_invalid_password)
focusView = password
cancel = true
}
// Check for a valid email address.
if (loginStr.isBlank()) {
if(loginStr.isBlank()) {
login.error = getString(R.string.error_field_required)
focusView = login
cancel = true
}
if (cancel) {
if(cancel) {
focusView?.requestFocus()
} else {
showProgress(true)
......@@ -114,7 +119,7 @@ class LoginActivity : AppCompatActivity() {
private fun computeInstanceURLHint() {
val instanceStr = instance.text.toString().let {
if (it.isBlank()) resources.getString(R.string.instance_exemple) else it
if(it.isBlank()) resources.getString(R.string.instance_exemple) else it
}
val protocole = activity_login_protocol_selection.selectedItem.toString()
......@@ -140,16 +145,16 @@ class LoginActivity : AppCompatActivity() {
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
login_form.visibility = if (show) View.GONE else View.VISIBLE
login_form.visibility = if(show) View.GONE else View.VISIBLE
login_form.animate()
.setDuration(shortAnimTime)
.alpha((if (show) 0 else 1).toFloat())
.setListener(object : AnimatorListenerAdapter() {
.alpha((if(show) 0 else 1).toFloat())
.setListener(object: AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
login_form.visibility = if (show) View.GONE else View.VISIBLE
login_form.visibility = if(show) View.GONE else View.VISIBLE
}
})
login_progress.visibility = if (show) View.VISIBLE else View.GONE
login_progress.visibility = if(show) View.VISIBLE else View.GONE
}
}
......@@ -3,9 +3,10 @@ package fr.chenry.android.freshrss.activities
import android.os.Bundle
import android.view.MenuItem
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Observer
import androidx.lifecycle.observe
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.ui.*
......@@ -14,11 +15,13 @@ import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.navigationdrawer.AddSubscriptionDialog
import fr.chenry.android.freshrss.components.subscriptions.MainSubscriptionFragmentDirections
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.viewmodels.AccountVM
import fr.chenry.android.freshrss.utils.BurgerMenuDrawerListener
import fr.chenry.android.freshrss.utils.e
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity: AppCompatActivity() {
private val accountVM by viewModels<AccountVM>()
private val navigation: NavController by lazy {
Navigation.findNavController(this, R.id.main_activity_host_fragment)
}
......@@ -36,11 +39,18 @@ class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
restoreState()
setContentView(R.layout.activity_main)
F.app.refresherService.value?.apply {refresh()}
setupActionBarWithNavController(navigation, appBarConfiguration)
drawerLayout.addDrawerListener(BurgerMenuDrawerListener(this))
drawerLayout.addDrawerListener(BurgerMenuDrawerListener(this.supportActionBar!!, navigation))
accountVM.account.observe(this) {
activity_main_navigation_view
.getHeaderView(0)
.findViewById<TextView>(R.id.navigation_header_container_user)
.text = it.login
}
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
......@@ -68,43 +78,6 @@ class MainActivity: AppCompatActivity() {
return true
}
override fun onResume() {
restoreState()
super.onResume()
activity_main_navigation_view
.getHeaderView(0)
.findViewById<TextView>(R.id.navigation_header_container_user)
.apply {
text = Store.account.value!!.login
Store.account.observe(this@MainActivity, Observer {text = Store.account.value!!.login})
}
}
override fun onPause() {
saveState()
super.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
saveState()
outState.putAll(navigation.saveState())
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
navigation.restoreState(savedInstanceState)
super.onRestoreInstanceState(savedInstanceState)
}
override fun onSupportNavigateUp(): Boolean =
navigation.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
private fun restoreState() {
Store.subscriptionsSection.value = F.preferences.subscriptionSection
}
private fun saveState() {
F.app.preferences.subscriptionSection = Store.subscriptionsSection.value!!
}
}
......@@ -34,13 +34,14 @@ class StartActivity: AppCompatActivity() {
setContentView(R.layout.activity_start)
val now = LocalDateTime.now()
val userName = model.liveData.value.let {
it?.let {F.app.refresherService.observeForever(observer)}
val userName = model.account.value.let {
when(it) {
null -> ""
VoidAccount -> ""
else -> " ${it.login}"
else -> {
F.app.refresherService.observeForever(observer)
" ${it.login}"
}
}
}
......@@ -53,8 +54,11 @@ class StartActivity: AppCompatActivity() {
override fun onResume() {
super.onResume()
model.liveData.value?.let {
startNextActivity(MainActivity::class.java)
model.account.value?.let {
when(it) {
VoidAccount -> startNextActivity(LoginActivity::class.java)
else -> startNextActivity(MainActivity::class.java)
}
}.whenNull {
startNextActivity(LoginActivity::class.java)
}
......
package fr.chenry.android.freshrss.components.subscriptions
import android.os.Bundle
import android.util.TypedValue
import android.view.*
import android.widget.LinearLayout
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import androidx.lifecycle.Observer
import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener
import com.google.android.material.bottomnavigation.BottomNavigationItemView
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.bottomnavigation.BottomNavigationView
import fr.chenry.android.freshrss.*
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.database.models.VoidCategory
import fr.chenry.android.freshrss.store.viewmodels.AccountVM
import fr.chenry.android.freshrss.utils.observe
import kotlinx.android.synthetic.main.fragment_main_subscription.*
import kotlinx.android.synthetic.main.menu_badge.*
class MainSubscriptionFragment: Fragment(), BottomNavigationView.OnNavigationItemSelectedListener {
private val adapter by lazy {MainSubscriptionPagerAdapter()}
class MainSubscriptionFragment: Fragment() {
private val viewModel by activityViewModels<AccountVM>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_main_subscription, container, false)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_main_subscription, container, false)
val bottomNav = view.findViewById<BottomNavigationView>(R.id.subcription_bottom_navigation)
val viewPager = view.findViewById<ViewPager2>(R.id.subcription_viewpager)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewPager.adapter = MainSubscriptionPagerAdapter()
viewPager.offscreenPageLimit = SubscriptionSection.values().size
setupBadgeCount()
subcription_fragment_container.adapter = adapter
subcription_fragment_container.offscreenPageLimit = SubscriptionSection.values().size
Store.subscriptionsSection.value!!.let {
subcription_bottom_navigation.selectedItemId = it.navigationButtonId
subcription_fragment_container.currentItem = it.ordinal
F.app.preferences.subscriptionSection.let {
bottomNav.selectedItemId = it.navigationButtonId
viewPager.setCurrentItem(it.ordinal, false)
}
subcription_fragment_container.addOnPageChangeListener(object: SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) = this@MainSubscriptionFragment.onPageSelected(position)
})
subcription_bottom_navigation.setOnNavigationItemSelectedListener(this)
}
viewPager.registerOnPageChangeCallback(PageChangeCallback())
bottomNav.setOnNavigationItemSelectedListener(BottomNavigationChangeCallback())
override fun onNavigationItemSelected(it: MenuItem): Boolean {
SubscriptionSection.fromNavigationButton(it.itemId).let {
Store.subscriptionsSection.value = it
setupBadgeStyle(it)
subcription_fragment_container.currentItem = it.ordinal
viewModel.account.observe(viewLifecycleOwner) {
bottomNav.getOrCreateBadge(R.id.subscriptions_bottom_navigation_unread).apply {
number = viewModel.unreadCount
}
}
return true
return view
}
fun onPageSelected(position: Int) = SubscriptionSection.byPosition(position).let {
F.app.preferences.subscriptionSection = it
Store.subscriptionsSection.value = it
subcription_bottom_navigation.selectedItemId = it.navigationButtonId
}
inner class MainSubscriptionPagerAdapter: FragmentStateAdapter(childFragmentManager, lifecycle) {
override fun getItemCount(): Int = SubscriptionSection.values().size
private fun setupBadgeCount() {
val menuItems = subcription_bottom_navigation.getChildAt(0)
menuItems.findViewById<BottomNavigationItemView>(R.id.subscriptions_bottom_navigation_unread).apply {
addView(layoutInflater.inflate(R.layout.menu_badge, menuItems as ViewGroup, false))
}
override fun createFragment(position: Int): Fragment =
instantiateFragment(SubscriptionSection.byPosition(position))
val observer = Observer<Int?> {count ->
menu_counter_badge_count.visibility = if((count ?: 0) > 0) View.VISIBLE else View.INVISIBLE
menu_counter_badge_count.text = (count ?: 0).let {if(it > 99) "99+" else it.toString()}
private fun instantiateFragment(subscriptionSection: SubscriptionSection): Fragment {
val arguments = MainNavDirections.toSubscriptions(VoidCategory, subscriptionSection).arguments
return SubscriptionsFragment().apply {setArguments(arguments)}
}
setupBadgeStyle(Store.subscriptionsSection.value!!)
observer.onChanged(Store.totalUnreadCount.value)
Store.totalUnreadCount.observe(viewLifecycleOwner, observer)
}
private fun setupBadgeStyle(activeSubscriptionSection: SubscriptionSection) {
val offset =
(resources.getDimensionPixelSize(R.dimen.badge_active_text_size) -
resources.getDimensionPixelSize(R.dimen.badge_inactive_text_size))
if(activeSubscriptionSection == SubscriptionSection.UNREAD) {
menu_counter_badge_count.updateLayoutParams<LinearLayout.LayoutParams> {
setMargins(0, 0, 0, resources.getDimensionPixelSize(R.dimen.badge_margin_bottom))
marginStart = resources.getDimensionPixelSize(R.dimen.badge_margin_start) + (offset / 2)
inner class PageChangeCallback: ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) = SubscriptionSection.byPosition(position).let {
if(subcription_bottom_navigation.selectedItemId != it.navigationButtonId) {
F.preferences.subscriptionSection = it
subcription_bottom_navigation.selectedItemId = it.navigationButtonId
}
menu_counter_badge_count
.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.badge_active_text_size))
menu_counter_badge_count.background =
resources.getDrawable(R.drawable.red_circle_background, activity!!.theme)
} else {
menu_counter_badge_count.updateLayoutParams<LinearLayout.LayoutParams> {
setMargins(0, 0, 0, resources.getDimensionPixelSize(R.dimen.badge_margin_bottom) - (offset * 2))
marginStart = resources.getDimensionPixelSize(R.dimen.badge_margin_start)
}
menu_counter_badge_count
.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.badge_inactive_text_size))
menu_counter_badge_count.background =
resources.getDrawable(R.drawable.grey_circle_background, activity!!.theme)
}
}
inner class MainSubscriptionPagerAdapter:
FragmentPagerAdapter(childFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItem(position: Int) = instantiateFragment(SubscriptionSection.byPosition(position))
override fun getCount() = SubscriptionSection.values().size
private fun instantiateFragment(subscriptionSection: SubscriptionSection): Fragment {
val arguments = MainNavDirections.toSubscriptions(VoidCategory, subscriptionSection).arguments
return childFragmentManager
.fragmentFactory
.instantiate(activity!!.classLoader, SubscriptionsFragment::class.qualifiedName!!)
.apply {setArguments(arguments)}
inner class BottomNavigationChangeCallback: BottomNavigationView.OnNavigationItemSelectedListener {
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val next = SubscriptionSection.fromNavigationButton(item.itemId).ordinal
if(subcription_viewpager.currentItem != next) subcription_viewpager.currentItem = next
return true
}
}
}
......@@ -2,45 +2,35 @@ package fr.chenry.android.freshrss.store
import androidx.lifecycle.*
import fr.chenry.android.freshrss.F
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection
import fr.chenry.android.freshrss.store.api.Api
import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.store.database.models.ReadStatus.READ
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
import fr.chenry.android.freshrss.store.database.models.SubscriptionCategory.Companion.fromApiItem
import fr.chenry.android.freshrss.utils.firstOrElse
import fr.chenry.android.freshrss.utils.whenNull
import nl.komponents.kovenant.*
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.successUi
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max
object Store {
val totalUnreadCount = MutableLiveData<Int>()
val subscriptionsSection =
MutableLiveData<SubscriptionSection>().apply {this.value = SubscriptionSection.ALL}
val tags = MutableLiveData<List<String>>().apply {this.value = listOf()}
val refreshingPromise = MutableLiveData<Promise<Unit, Exception>>()
val account by lazy {
MutableLiveData<Account>().apply {
accountLiveData.value?.firstOrNull()?.let {value = it}.whenNull {value = VoidAccount}
accountLiveData.observeForever {self -> self.firstOrNull()?.let {value = it}.whenNull {value = VoidAccount}}
}
val account: LiveData<Account> = Transformations.distinctUntilChanged(F.db.getAccount().toLiveData()).map {
it.firstOrElse(VoidAccount)
}
private var lastFetchTimestamp = 0L
private var api: Api
private val accountLiveData: LiveData<List<Account>>
private val onGoingPostRequests = ConcurrentHashMap<String, Promise<Unit, Exception>>()
init {
val flowable = F.db.getAccount()
val account = flowable.blockingFirst().firstOrNull() ?: VoidAccount
api = Api(account)
accountLiveData = F.db.getAccount().toLiveData().apply {
observeForever {if(it.isNotEmpty()) api = Api(it.first())}
}
val acc = F.db.getAccount().blockingFirst().firstOrNull() ?: VoidAccount
api = Api(acc)
account.observeForever {api = Api(it)}
}
fun login(instance: String, user: String, password: String): Promise<Unit, Exception> =
......@@ -74,9 +64,8 @@ object Store {
fun getUnreadCount(): Promise<Unit, Exception> =
api.getUnreadCount() then {
totalUnreadCount.postValue(it.max)
it.unreadcounts
.forEach {self -> F.db.updateSubscriptionCount(self.id, self.count)}
F.db.updateTotalUnreadCount(api.account.id, it.max)
it.unreadcounts.forEach {self -> F.db.updateSubscriptionCount(self.id, self.count)}
}
fun getStreamContents(id: String): Promise<Unit, Exception> =
......@@ -114,11 +103,11 @@ object Store {
when(readStatus) {
READ -> {
F.db.decrementSubscriptionCount(article.streamId)
totalUnreadCount.postValue(max((totalUnreadCount.value ?: 0) - 1, 0))
F.db.decrementTotalUnreadCount(api.account.id)
}
UNREAD -> {
F.db.incrementSubscriptionCount(article.streamId)
totalUnreadCount.postValue((totalUnreadCount.value ?: 0) + 1)
F.db.incrementTotalUnreadCount(api.account.id)
}
}
}
......
package fr.chenry.android.freshrss.store.database
import android.graphics.Bitmap
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.*
import dev.matrix.roomigrant.GenerateRoomMigrations
import fr.chenry.android.freshrss.store.api.models.StreamId
import fr.chenry.android.freshrss.store.database.models.Account
import fr.chenry.android.freshrss.store.database.models.Article
import fr.chenry.android.freshrss.store.database.models.ArticlesDAO
import fr.chenry.android.freshrss.store.database.models.AuthTokensDAO
import fr.chenry.android.freshrss.store.database.models.ItemId
import fr.chenry.android.freshrss.store.database.models.ReadStatus
import fr.chenry.android.freshrss.store.database.models.Subscription
import fr.chenry.android.freshrss.store.database.models.SubscriptionCategoriesDAO
import fr.chenry.android.freshrss.store.database.models.SubscriptionCategory
import fr.chenry.android.freshrss.store.database.models.Subscriptions
import fr.chenry.android.freshrss.store.database.models.SubscriptionsDAO
import fr.chenry.android.freshrss.store.database.models.areSimilar
import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.utils.Try
import fr.chenry.android.freshrss.utils.escapeHtml4
import nl.komponents.kovenant.all
import nl.komponents.kovenant.*
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.task