Commit 11af8196 authored by Christophe Henry's avatar Christophe Henry

Solves #15: Implement Jetpack's NavigationUI and get rid of the router

Incidentaly solves #20 as expected
parent fcd87dcd
......@@ -3,6 +3,7 @@ apply plugin: "kotlin-android"
apply plugin: "kotlin-android-extensions"
apply plugin: "kotlinx-serialization"
apply plugin: "kotlin-kapt"
apply plugin: "androidx.navigation.safeargs.kotlin"
android {
compileSdkVersion 28
......@@ -24,8 +25,6 @@ android {
enabled = true
}
com.android.build.gradle.internal.api.ApplicationVariantImpl
applicationVariants.all { variant ->
if (variant.name.contains("elease")) {
variant.mergeAssetsProvider.get().doLast {
......@@ -33,6 +32,10 @@ android {
}
}
}
androidExtensions {
experimental = true
}
}
dependencies {
......@@ -43,14 +46,14 @@ dependencies {
def test_runnner_version = "1.1.1"
def promise_version = "3.3.0"
def android_support_version = "28.0.0"
def android_navigation = "1.0.0-rc02"
implementation 'androidx.legacy:legacy-support-v4:1.0.0-beta01'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0-beta01'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0'
configurations {
all*.exclude group: 'com.google.guava', module: 'listenablefuture'
}
implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation fileTree(dir: "libs", include: ["*.jar"])
// Kotlin stuff
......@@ -62,7 +65,7 @@ dependencies {
implementation "com.android.support:preference-v7:$android_support_version"
implementation "com.android.support:support-core-utils:$android_support_version"
implementation "com.android.support:support-fragment:$android_support_version"
implementation "com.android.support:support-compat:28.0.0"
implementation "com.android.support:support-compat:$android_support_version"
// AndroidX layout
implementation "androidx.appcompat:appcompat:1.0.0-beta01"
......@@ -72,9 +75,10 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.0.0"
// ViewModel and LiveData
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"
annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
// Room
......@@ -92,8 +96,13 @@ dependencies {
implementation "nl.komponents.kovenant:kovenant:$promise_version"
implementation "nl.komponents.kovenant:kovenant-android:$promise_version"
// Navigation
implementation "android.arch.navigation:navigation-fragment-ktx:$android_navigation"
implementation "android.arch.navigation:navigation-ui-ktx:$android_navigation"
// Utils
implementation "org.apache.commons:commons-text:1.4"
implementation "joda-time:joda-time:2.10.1"
// Tests
testImplementation "junit:junit:4.12"
......
......@@ -34,6 +34,8 @@ class RefresherService: Service() {
this.startForeground(NotificationHelper.ONGOING_REFRESH_NOTIFICATION, refreshNotification)
Store.ensureToken()
val promise = Store.getSubscriptions()
.bind {Store.getUnreadCount()}
.bind {all(Store.subscriptions.keys.map {Store.getStreamContents(it)}, cancelOthersOnError = false)}
......
package fr.chenry.android.freshrss.activities
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import fr.chenry.android.freshrss.*
import fr.chenry.android.freshrss.components.waiting.WaitingFragment
import fr.chenry.android.freshrss.store.*
import fr.chenry.android.freshrss.utils.*
import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.resolve
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
class MainActivity: AppCompatActivity() {
private val deferred = deferred<Unit, Exception>()
private val navigation: NavController by lazy {
Navigation.findNavController(this, R.id.main_activity_host_fragment)
}
private val appBarConfiguration by lazy {AppBarConfiguration(navigation.graph)}
init {
FreshRSSApplication.application.refresherService.whenNotNull {
......@@ -27,54 +32,13 @@ class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Router.observe(::onRouteChanged)
setupActionBar()
setupActionBarWithNavController(navigation, appBarConfiguration)
}
override fun onStart() {
super.onStart()
deferred.promise failUi {this.e(it)}
Router.navigate(RouteName.SUBSCRIPTIONS, pushHistory = false)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if(item == null) return super.onOptionsItemSelected(item)
return when(item.itemId) {
android.R.id.home -> onBackPressed().let {true}
else -> super.onOptionsItemSelected(item)
}
}
override fun onBackPressed() {
if(supportFragmentManager.backStackEntryCount > 0)
supportFragmentManager.popBackStackImmediate()
}
private fun onRouteChanged(to: Router.Route) {
val bundle = if(to.parameters.isEmpty()) Bundle.EMPTY else Bundle(to.parameters.entries.size).apply {
to.parameters.entries.forEach {(k, v) -> this.putString(k, v)}
}
val fragmentTransaction = supportFragmentManager
?.beginTransaction()
?.replace(
R.id.fragment_container,
Fragment.instantiate(this, to.fragment.qualifiedName, bundle)
)
if(to.pushHistory)
fragmentTransaction?.addToBackStack(null)
fragmentTransaction?.commit()
}
private fun setupActionBar() {
supportFragmentManager.addOnBackStackChangedListener {
if(supportFragmentManager.backStackEntryCount > 0) supportActionBar?.setDisplayHomeAsUpEnabled(true)
else supportActionBar?.setDisplayHomeAsUpEnabled(false)
}
supportFragmentManager?.beginTransaction()?.add(R.id.fragment_container, WaitingFragment())?.commit()
}
override fun onSupportNavigateUp() = navigation.navigateUp() || super.onSupportNavigateUp()
}
......@@ -27,17 +27,12 @@ class RecyclerViewAdapter(private val fragment: SubscriptionArticlesFragment):
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val article = articles[position]
holder.bind(article)
holder.binding.root.setOnClickListener{
Router.navigate(RouteName.SUBSCRIPTION_CONTENT_DETAIL,
mapOf("streamId" to fragment.streamId, "articleId" to article.id))
}
holder.binding.root.setOnClickListener{fragment.navigateToArticle(article.id)}
}
override fun getItemCount(): Int = articles.size
inner class ViewHolder(val binding: FragmentSubscriptionArticleBinding):
RecyclerView.ViewHolder(binding.root) {
inner class ViewHolder(val binding: FragmentSubscriptionArticleBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(article: Article) {
binding.setVariable(BR.article, article)
binding.executePendingBindings()
......
......@@ -15,6 +15,7 @@ import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.database.FreshRSSDabatabase
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
import fr.chenry.android.freshrss.store.databindingsupport.viewmodels.SubscriptionArticlesVM
import fr.chenry.android.freshrss.store.databindingsupport.viewmodels.SubscriptionArticlesVMF
import fr.chenry.android.freshrss.utils.capitalizeFull
......@@ -72,7 +73,7 @@ class SubscriptionArticlesDetailFragment: Fragment() {
override fun onResume() {
super.onResume()
setReadStatus(ReadStatus.READ)
if(article.readStatus == UNREAD) setReadStatus(ReadStatus.READ)
}
override fun onDestroyView() {
......@@ -137,17 +138,6 @@ class SubscriptionArticlesDetailFragment: Fragment() {
return false
}
val readText = when(readStatus) {
ReadStatus.READ -> this.getString(R.string.read)
ReadStatus.UNREAD -> this.getString(R.string.unread)
}.let {this.getString(R.string.mark_read_status_authorization, it)}
if(!FreshRSSDabatabase.instance.account.canPostRequests) {
val toastText = this.getString(R.string.cannot_make_post_requests, readText)
Toast.makeText(context, toastText, LENGTH_LONG).show()
return false
}
isFetching.value = true
Store.postReadStatus(article, readStatus)
......@@ -162,6 +152,12 @@ class SubscriptionArticlesDetailFragment: Fragment() {
}
.failUi {e ->
this.e(e)
val readText = when(readStatus) {
ReadStatus.READ -> this.getString(R.string.read)
ReadStatus.UNREAD -> this.getString(R.string.unread)
}.let {this.getString(R.string.mark_read_status_authorization, it)}
val toastText = context?.getString(R.string.unable_to, readText)
Toast.makeText(context, toastText, LENGTH_SHORT).show()
} alwaysUi {isFetching.value = false}
......
......@@ -3,46 +3,63 @@ package fr.chenry.android.freshrss.components.subscriptionarticles
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.*
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.api.models.StreamId
import fr.chenry.android.freshrss.store.database.FreshRSSDabatabase
import fr.chenry.android.freshrss.store.database.models.Articles
import fr.chenry.android.freshrss.store.databindingsupport.viewmodels.*
import fr.chenry.android.freshrss.store.database.models.ReadStatus
import fr.chenry.android.freshrss.store.database.models.ReadStatus.READ
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
import kotlinx.android.synthetic.main.fragment_subscription_articles.*
sealed class SubscriptionArticlesFragment: Fragment() {
lateinit var streamId: String
private set
private lateinit var model: LiveData<Articles>
open class SubscriptionArticlesFragment: Fragment(), Observer<Articles> {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
streamId = arguments?.getString("id")!!
model = getModel(streamId)
return inflater.inflate(R.layout.fragment_subscription_articles, container, false)
.let {
it as RecyclerView
it.layoutManager = LinearLayoutManager(context)
it.adapter = RecyclerViewAdapter(this).let {adapter ->
model.observe(this, Observer {articles ->
adapter.articles = articles
adapter.notifyDataSetChanged()
})
adapter
}
it
}
private val streamId: String by lazy {args.id}
private val readStatus: ReadStatus by lazy {args.readStatus}
private val args: SubscriptionArticlesFragmentArgs by navArgs()
private val model: LiveData<Articles> by lazy {
when(readStatus) {
READ -> FreshRSSDabatabase.instance.getArticleByStreamId(streamId)
UNREAD -> FreshRSSDabatabase.instance.getArticleByStreamIdAndUnread(streamId)
}
}
private val adapter by lazy {
RecyclerViewAdapter(this).apply {
model.observe(this@SubscriptionArticlesFragment, this@SubscriptionArticlesFragment)
}
}
abstract fun getModel(streamId: StreamId): LiveData<Articles>
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_subscription_articles, container, false)
class SubscriptionAllArticlesFragment: SubscriptionArticlesFragment() {
override fun getModel(streamId: StreamId) = FreshRSSDabatabase.instance.getArticleByStreamId(streamId)
}
model.observe(this, this)
view.findViewById<RecyclerView>(R.id.fragment_subscription_article_recycler).apply {
layoutManager = LinearLayoutManager(context)
adapter = this@SubscriptionArticlesFragment.adapter
}
class SubscriptionUnreadArticlesFragment: SubscriptionArticlesFragment() {
override fun getModel(streamId: StreamId) = FreshRSSDabatabase.instance.getArticleByStreamIdAndUnread(streamId)
return view
}
override fun onChanged(articles: Articles?) {
(articles ?: listOf()).let {
fragment_subscription_article_waiting.visibility = if(it.isNotEmpty()) View.GONE else View.VISIBLE
fragment_subscription_article_recycler.visibility = if(it.isEmpty()) View.GONE else View.VISIBLE
adapter.articles = it
adapter.notifyDataSetChanged()
}
}
fun navigateToArticle(articleId: String) {
val direction =
SubscriptionArticlesFragmentDirections
.actionSubscriptionArticlesFragmentToSubscriptionArticlesDetailFragment(articleId)
view?.findNavController()?.navigate(direction)
}
}
......@@ -2,9 +2,13 @@ package fr.chenry.android.freshrss.components.subscriptions
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.navigation.findNavController
import fr.chenry.android.freshrss.MainNavDirections
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.*
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.api.models.Subscriptions
import fr.chenry.android.freshrss.store.database.models.ReadStatus.READ
class AllSubscriptionsFragment: SubscriptionsFragment() {
override val liveData: LiveData<Subscriptions> = MutableLiveData<Subscriptions>().apply {
......@@ -13,6 +17,7 @@ class AllSubscriptionsFragment: SubscriptionsFragment() {
}
override fun onClick(subscription: Subscription) {
Router.navigate(RouteName.SUBSCRIPTION_ARTICLES_ALL, mapOf("id" to subscription.id))
val direction = MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, READ)
view?.findNavController()?.navigate(direction)
}
}
package fr.chenry.android.freshrss.components.subscriptions
import androidx.lifecycle.*
import fr.chenry.android.freshrss.store.RouteName
import fr.chenry.android.freshrss.store.Router
import androidx.lifecycle.MutableLiveData
import androidx.navigation.findNavController
import fr.chenry.android.freshrss.MainNavDirections
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.api.models.Subscriptions
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
class FavoritesSubscriptionsFragment: SubscriptionsFragment() {
override val liveData = MutableLiveData<Subscriptions>().apply {this.value = listOf()}
override fun onClick(subscription: Subscription) {
Router.navigate(RouteName.SUBSCRIPTION_ARTICLES_UNREAD, mapOf("id" to subscription.id))
val direction = MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, UNREAD)
view?.findNavController()?.navigate(direction)
}
}
......@@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.chenry.android.freshrss.FreshRSSApplication
......@@ -11,9 +12,9 @@ import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.api.models.Subscriptions
import fr.chenry.android.freshrss.utils.getOrDefault
import fr.chenry.android.freshrss.utils.whenNotNull
import kotlinx.android.synthetic.main.fragment_main_subscription.*
import kotlinx.android.synthetic.main.fragment_subscriptions.*
import kotlin.reflect.KClass
class MainSubscriptionFragment: Fragment() {
......@@ -75,23 +76,31 @@ class MainSubscriptionFragment: Fragment() {
}
}
abstract class SubscriptionsFragment: Fragment() {
abstract class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
abstract val liveData: LiveData<Subscriptions>
val subscriptions: Subscriptions get() = liveData.value ?: listOf()
val subscriptions: Subscriptions get() = liveData.value ?: listOf()
private val adapter by lazy {RecyclerViewAdapter(this)}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_subscriptions, container, false)
liveData.observe(this, this)
view.findViewById<RecyclerView>(R.id.subscriptions_list).let {
it.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
it.adapter = RecyclerViewAdapter(this).apply {
liveData.observeForever {this.notifyDataSetChanged()}
}
it.adapter = this.adapter
}
return view
}
override fun onChanged(subscriptions: Subscriptions?) {
(subscriptions ?: listOf()).let {
subscriptions_list.visibility = if(it.isEmpty()) View.GONE else View.VISIBLE
fragment_subscriptions_waiting.visibility = if(it.isNotEmpty()) View.GONE else View.VISIBLE
adapter.notifyDataSetChanged()
}
}
abstract fun onClick(subscription: Subscription)
}
\ No newline at end of file
......@@ -2,9 +2,10 @@ package fr.chenry.android.freshrss.components.subscriptions
import android.os.Bundle
import androidx.lifecycle.ViewModelProviders
import fr.chenry.android.freshrss.store.RouteName
import fr.chenry.android.freshrss.store.Router
import androidx.navigation.findNavController
import fr.chenry.android.freshrss.MainNavDirections
import fr.chenry.android.freshrss.store.api.models.Subscription
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
import fr.chenry.android.freshrss.store.databindingsupport.viewmodels.UnreadSubscriptionsViewModel
class UnreadSubscriptionsFragment: SubscriptionsFragment() {
......@@ -19,6 +20,7 @@ class UnreadSubscriptionsFragment: SubscriptionsFragment() {
}
override fun onClick(subscription: Subscription) {
Router.navigate(RouteName.SUBSCRIPTION_ARTICLES_UNREAD, mapOf("id" to subscription.id))
val direction = MainNavDirections.actionGlobalSubscriptionArticlesFragment(subscription.id, UNREAD)
view?.findNavController()?.navigate(direction)
}
}
package fr.chenry.android.freshrss.store
import androidx.annotation.MainThread
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import fr.chenry.android.freshrss.components.subscriptionarticles.*
import fr.chenry.android.freshrss.components.subscriptions.MainSubscriptionFragment
import fr.chenry.android.freshrss.store.Router.Route
import kotlin.reflect.KClass
enum class RouteName {
SUBSCRIPTIONS,
SUBSCRIPTIONS_ALL,
SUBSCRIPTIONS_UNREAD,
SUBSCRIPTIONS_FAVORITES,
SUBSCRIPTION_ARTICLES_ALL,
SUBSCRIPTION_ARTICLES_UNREAD,
SUBSCRIPTION_CONTENT_DETAIL
}
@MainThread
object Router {
private val currentRoute = MutableLiveData<Route>()
private fun navigate(to: Route?, params: Map<String, Any>, pushHistory: Boolean) {
if (to == null) return
val newTo = to.withPushHistory(pushHistory)
val groups = to.route.split("/")
.filter { it.isNotEmpty() && it.startsWith(":") }
.map { it.substring(1..it.length.minus(1)) }
if (groups.isEmpty()) return currentRoute.setValue(newTo)
val parameters = groups.map {
if (!params.containsKey(it))
throw StringIndexOutOfBoundsException("Required parameter $it not found in parameters")
it to params.getValue(it)
}
currentRoute.value = newTo.withParameters(parameters.toMap())
}
fun navigate(to: RouteName, params: Map<String, Any> = mapOf(), pushHistory: Boolean = true) =
this.navigate(routes.find { it.name.name == to.name }, params, pushHistory)
fun observe(onRouteChange: (Route) -> Unit) = currentRoute.observeForever { onRouteChange(it) }
data class Route(
val route: String,
val name: RouteName,
val fragment: KClass<out Fragment>,
val parameters: Map<String, String> = mapOf(),
val pushHistory: Boolean = true
) {
fun withParameters(params: Map<String, Any>) =
this.copy(parameters = params.map { it.key to it.value.toString() }.toMap())
fun withPushHistory(newPushHistory: Boolean) = this.copy(pushHistory = newPushHistory)
}
}
private val routes = listOf(
Route("/subscriptions", RouteName.SUBSCRIPTIONS, MainSubscriptionFragment::class),
Route("/subscription/:id/all", RouteName.SUBSCRIPTION_ARTICLES_ALL, SubscriptionAllArticlesFragment::class),
Route("/subscription/:id/unread", RouteName.SUBSCRIPTION_ARTICLES_UNREAD, SubscriptionUnreadArticlesFragment::class),
Route("/subscription/:streamId/article/:articleId", RouteName.SUBSCRIPTION_CONTENT_DETAIL, SubscriptionArticlesDetailFragment::class)
)
......@@ -32,6 +32,14 @@ object Store {
fun login(instance: String, user: String, password: String): Promise<Unit, Exception> =
Api.login(instance, user, password) then {init(it)}
fun ensureToken(): Promise<Unit, Exception> {
if(!FreshRSSDabatabase.instance.account.isWriteTokenExpired) return Promise.ofSuccess(Unit)
return api.getWriteToken()
.success {FreshRSSDabatabase.instance.account.writeToken = it}
.toSuccessVoid()
}
fun getSubscriptions(): Promise<Unit, Exception> =
api.getSubscriptions() then {subscriptions.addAll(it.map {self -> self.id to self})}
......@@ -67,29 +75,9 @@ object Store {
all(promises, cancelOthersOnError = false).toSuccessVoid()
}
fun getArticle(articleId: String): Promise<Article, ArticleNotFoundException> {
val deferred = deferred<Article, ArticleNotFoundException>()
task {
val articles = FreshRSSDabatabase.instance.getArticleById(articleId).blockingFirst()
if(articles.isEmpty()) deferred.reject(ArticleNotFoundException(articleId))
else deferred.resolve(articles.first())
}
return deferred.promise
}
fun postReadStatus(article: Article, readStatus: ReadStatus): Promise<Unit, Exception> =
ensureToken() bind {
api.postReadStatus(article.id, readStatus)
.success {FreshRSSDabatabase.instance.upsertArticle(article.copy(readStatus = readStatus))}
}
private fun ensureToken(): Promise<Unit, Exception> {
if(!FreshRSSDabatabase.instance.account.isWriteTokenExpired) return Promise.ofSuccess(Unit)
return api.getWriteToken()
.success {FreshRSSDabatabase.instance.account.writeToken = it}
.toSuccessVoid()
}
}
class ArticleNotFoundException(val id: String): Exception("Article with id $id cound not be found in store")
\ No newline at end of file
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ package fr.chenry.android.freshrss.store.database.models
import androidx.room.*
import com.github.kittinunf.fuel.core.ResponseDeserializable
import org.joda.time.LocalDateTime
import java.util.*
@Entity(tableName = "accounts")
......@@ -19,11 +20,11 @@ data class Account(
var writeToken = ""
set(value) {
field = value
writeTokenBirth = Date()
writeTokenBirth = LocalDateTime(0)
}
@Ignore
private var writeTokenBirth = Date(0)
val isWriteTokenExpired get() = writeTokenBirth.before(Date().apply {minutes -= 30})
private var writeTokenBirth = LocalDateTime.now()
val isWriteTokenExpired get() = writeTokenBirth.isBefore(LocalDateTime.now().minusMinutes(30))
val canPostRequests get() = writeToken.isNotBlank()
companion object: ResponseDeserializable<Account> {
......
package fr.chenry.android.freshrss.store.database.models
import android.net.Uri
import android.os.Parcelable
import androidx.databinding.BaseObservable
import androidx.lifecycle.LiveData
import androidx.room.*
import fr.chenry.android.freshrss.store.api.models.ContentItem
import fr.chenry.android.freshrss.store.api.models.StreamId
import io.reactivex.Flowable
import kotlinx.android.parcel.Parcelize
typealias ItemId = String
......@@ -44,7 +46,8 @@ data class Article(
}
}
enum class ReadStatus {
@Parcelize
enum class ReadStatus: Parcelable