Compare commits

...

15 Commits

Author SHA1 Message Date
Amine Louveau
a4527940b8 Merge pull request 'Mercury issues fixing.' (#96) from mercury-common into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/96
2022-11-08 21:17:23 +00:00
aminecmi
9e8a25ed3e Fixing tests. 2022-11-08 22:02:20 +01:00
aminecmi
8ea46e146b Cleaning. 2022-11-08 21:54:12 +01:00
aminecmi
5ecf3c3f87 Mercury api in common code. 2022-11-08 21:31:40 +01:00
aminecmi
325f103417 Timeout. 2022-11-08 20:49:18 +01:00
Amine Louveau
ab4b1ae644 Merge pull request 'Theme should automatically change on phone settings change.' (#95) from theme-update into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/95
2022-11-08 07:38:54 +00:00
aminecmi
87ea44754e Font update. 2022-11-07 22:36:20 +01:00
aminecmi
04dec50808 Theme should automatically change on phone settings change. 2022-11-07 22:07:35 +01:00
Amine Louveau
e36189e2e7 Merge pull request 'About config upgrade.' (#93) from aboutconfig into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/93
2022-11-05 21:20:17 +00:00
aminecmi
d6bdf510a4 About config upgrade. 2022-11-05 22:00:16 +01:00
Amine Louveau
a464e93370 Merge pull request 'Immediately update bottom badges after reading or starring articles' (#91) from davidoskky/ReaderForSelfoss-multiplatform:badges into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/91
2022-11-02 19:06:48 +00:00
4b63afe62a Update badges tests 2022-11-01 21:51:46 +01:00
ac4c4b9441 Merge branch 'master' into badges 2022-11-01 20:35:13 +00:00
02d734eee8 Do not edit the repository items from outside the repository 2022-11-01 21:29:04 +01:00
c5cdfc0d53 Update bottom bar badges through a state flow 2022-11-01 21:28:14 +01:00
29 changed files with 522 additions and 370 deletions

View File

@ -8,7 +8,7 @@ steps:
commands: commands:
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Configure gradle..." - echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false" >> ~/.gradle/gradle.properties - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Analysing..." - echo "Analysing..."
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN - ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN
@ -90,7 +90,7 @@ steps:
commands: commands:
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Configure gradle..." - echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false" >> ~/.gradle/gradle.properties - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Generate APK" - echo "Generate APK"
- ./gradlew :androidApp:assembleGithubConfigRelease -P pushCache=false - ./gradlew :androidApp:assembleGithubConfigRelease -P pushCache=false

View File

@ -6,6 +6,7 @@ plugins {
id("com.android.application") id("com.android.application")
kotlin("android") kotlin("android")
kotlin("kapt") kotlin("kapt")
id("com.mikepenz.aboutlibraries.plugin")
} }
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String { fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
@ -62,7 +63,7 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
} }
compileSdk = 32 compileSdk = 33
buildToolsVersion = "31.0.0" buildToolsVersion = "31.0.0"
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
@ -70,7 +71,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android" applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 21 minSdk = 21
targetSdk = 32 targetSdk = 33
versionCode = versionCodeFromGit() versionCode = versionCodeFromGit()
versionName = versionNameFromGit() versionName = versionNameFromGit()
@ -138,15 +139,8 @@ dependencies {
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
// About // About
implementation("com.mikepenz:aboutlibraries-core:8.9.4") implementation("com.mikepenz:aboutlibraries-core:10.5.1")
implementation("com.mikepenz:aboutlibraries:8.9.4") implementation("com.mikepenz:aboutlibraries:10.5.1")
implementation("com.mikepenz:aboutlibraries-definitions:8.9.4")
// Retrofit + http logging + okhttp
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.burgstaller:okhttp-digest:2.5")
// Material-ish things // Material-ish things
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
@ -210,3 +204,13 @@ tasks.withType<Test> {
showStandardStreams = true showStandardStreams = true
} }
} }
aboutLibraries {
offlineMode = true
fetchRemoteLicense = false
fetchRemoteFunding = false
includePlatform = false
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
}

View File

@ -30,15 +30,8 @@
<fields>; <fields>;
} }
-dontwarn okio.**
-dontwarn retrofit2.Platform$Java8
-keep class retrofit.** { *; }
-keepclasseswithmembers class * {
@retrofit.http.* <methods>;
}
-keepattributes *Annotation*,Signature -keepattributes *Annotation*,Signature
-keepattributes Exceptions -keepattributes Exceptions
-dontwarn okio.**
-dontwarn javax.annotation.Nullable -dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault -dontwarn javax.annotation.ParametersAreNonnullByDefault

View File

@ -15,7 +15,8 @@
android:supportsRtl="true" android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/NoBar" android:theme="@style/NoBar"
android:dataExtractionRules="@xml/data_extraction_rules"> android:dataExtractionRules="@xml/data_extraction_rules"
android:configChanges="uiMode">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:theme="@style/SplashTheme" android:theme="@style/SplashTheme"

View File

@ -18,6 +18,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.doOnNextLayout import androidx.core.view.doOnNextLayout
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.*
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
@ -178,8 +179,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
adapter.handleItemAtIndex(position) adapter.handleItemAtIndex(position)
reloadBadgeContent()
val tagHashes = i.tags.map { it.longHash() } val tagHashes = i.tags.map { it.longHash() }
tagsBadge = tagsBadge.map { tagsBadge = tagsBadge.map {
if (tagHashes.contains(it.key)) { if (tagHashes.contains(it.key)) {
@ -207,6 +206,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView) ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
} }
private fun updateBottomBarBadgeCount(badge: TextBadgeItem, count: Int) {
if (count > 0) {
badge
.setText(count.toString())
.maybeShow()
} else {
badge.removeBadge()
}
}
private fun handleBottomBar() { private fun handleBottomBar() {
tabNewBadge = TextBadgeItem() tabNewBadge = TextBadgeItem()
@ -219,6 +228,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false).hide(false)
if (appSettingsService.isDisplayUnreadCountEnabled()) {
lifecycleScope.launch {
repository.badgeUnread.collect {
updateBottomBarBadgeCount(tabNewBadge, it)
}
}
}
if (appSettingsService.isDisplayAllCountEnabled()) {
lifecycleScope.launch {
repository.badgeAll.collect {
updateBottomBarBadgeCount(tabArchiveBadge, it)
}
}
lifecycleScope.launch {
repository.badgeStarred.collect {
updateBottomBarBadgeCount(tabStarredBadge, it)
}
}
}
val tabNew = val tabNew =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp, R.drawable.ic_tab_fiber_new_black_24dp,
@ -714,29 +745,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadBadges() { private fun reloadBadges() {
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) { if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.IO).launch {
repository.reloadBadges() repository.reloadBadges()
reloadBadgeContent()
} }
} }
} }
private fun reloadBadgeContent() {
if (appSettingsService.isDisplayUnreadCountEnabled()) {
tabNewBadge
.setText(repository.badgeUnread.toString())
.maybeShow()
}
if (appSettingsService.isDisplayAllCountEnabled()) {
tabArchiveBadge
.setText(repository.badgeAll.toString())
.maybeShow()
tabStarredBadge
.setText(repository.badgeStarred.toString())
.maybeShow()
}
}
private fun reloadTagsBadges() { private fun reloadTagsBadges() {
tagsBadge.forEach { tagsBadge.forEach {
binding.mainDrawer.updateBadge(it.key, StringHolder(it.value.toString())) binding.mainDrawer.updateBadge(it.key, StringHolder(it.value.toString()))
@ -858,10 +872,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun maxItemNumber(): Int = private fun maxItemNumber(): Int =
when (elementsShown) { when (elementsShown) {
ItemType.UNREAD -> repository.badgeUnread ItemType.UNREAD -> repository.badgeUnread.value
ItemType.ALL -> repository.badgeAll ItemType.ALL -> repository.badgeAll.value
ItemType.STARRED -> repository.badgeStarred ItemType.STARRED -> repository.badgeStarred.value
else -> repository.badgeUnread // if !elementsShown then unread are fetched. else -> repository.badgeUnread.value // if !elementsShown then unread are fetched.
} }
private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) { private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {

View File

@ -3,11 +3,14 @@ package bou.amine.apps.readerforselfossv2.android
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.*
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
@ -48,6 +51,7 @@ class MyApp : MultiDexApplication(), DIAware {
private val viewModel: AppViewModel by instance() private val viewModel: AppViewModel by instance()
private val connectivityStatus: ConnectivityStatus by instance() private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance() private val driverFactory: DriverFactory by instance()
private val appSettingsService : AppSettingsService by instance()
// TODO: handle with the "previous" way // TODO: handle with the "previous" way
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true) private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
@ -132,6 +136,19 @@ class MyApp : MultiDexApplication(), DIAware {
} }
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (appSettingsService.getCurrentTheme() == MODE_NIGHT_FOLLOW_SYSTEM) {
var mode = when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> MODE_NIGHT_YES
else -> MODE_NIGHT_NO
}
setDefaultNightMode(mode)
}
}
class AppLifeCycleObserver(val connectivityStatus: ConnectivityStatus, val repository: Repository) : DefaultLifecycleObserver { class AppLifeCycleObserver(val connectivityStatus: ConnectivityStatus, val repository: Repository) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {

View File

@ -36,9 +36,9 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private fun showMenuItem(willAddToFavorite: Boolean) { private fun showMenuItem(willAddToFavorite: Boolean) {
if (willAddToFavorite) { if (willAddToFavorite) {
toolbarMenu.findItem(R.id.star).icon.setTint(Color.WHITE) toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE)
} else { } else {
toolbarMenu.findItem(R.id.star).icon.setTint(Color.RED) toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED)
} }
} }

View File

@ -111,13 +111,11 @@ class ItemCardAdapter(
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item) repository.unstarr(item)
} }
item.starred = false
binding.favButton.isSelected = false binding.favButton.isSelected = false
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.starr(item) repository.starr(item)
} }
item.starred = true
binding.favButton.isSelected = true binding.favButton.isSelected = true
} }
} }

View File

@ -1,35 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.mercury
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class MercuryApi() {
private val service: MercuryService
init {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.NONE
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
val gson = GsonBuilder()
.setLenient()
.create()
val retrofit =
Retrofit
.Builder()
.baseUrl("https://www.amine-louveau.fr")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
service = retrofit.create(MercuryService::class.java)
}
fun parseUrl(url: String): Call<ParsedContent> {
return service.parseUrl(url)
}
}

View File

@ -1,59 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.mercury
import android.os.Parcel
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
class ParsedContent(
@SerializedName("title") val title: String,
@SerializedName("content") val content: String?,
@SerializedName("date_published") val date_published: String,
@SerializedName("lead_image_url") val lead_image_url: String?,
@SerializedName("dek") val dek: String,
@SerializedName("url") val url: String,
@SerializedName("domain") val domain: String,
@SerializedName("excerpt") val excerpt: String,
@SerializedName("total_pages") val total_pages: Int,
@SerializedName("rendered_pages") val rendered_pages: Int,
@SerializedName("next_page_url") val next_page_url: String
) : Parcelable {
companion object {
@JvmField
val CREATOR: Parcelable.Creator<ParsedContent> =
object : Parcelable.Creator<ParsedContent> {
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
title = source.readString().orEmpty(),
content = source.readString(),
date_published = source.readString().orEmpty(),
lead_image_url = source.readString(),
dek = source.readString().orEmpty(),
url = source.readString().orEmpty(),
domain = source.readString().orEmpty(),
excerpt = source.readString().orEmpty(),
total_pages = source.readInt(),
rendered_pages = source.readInt(),
next_page_url = source.readString().orEmpty()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(title)
dest.writeString(content)
dest.writeString(date_published)
dest.writeString(lead_image_url)
dest.writeString(dek)
dest.writeString(url)
dest.writeString(domain)
dest.writeString(excerpt)
dest.writeInt(total_pages)
dest.writeInt(rendered_pages)
dest.writeString(next_page_url)
}
}

View File

@ -1,10 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.mercury
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface MercuryService {
@GET("parser.php")
fun parseUrl(@Query("link") link: String): Call<ParsedContent>
}

View File

@ -21,8 +21,6 @@ import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.ImageActivity import bou.amine.apps.readerforselfossv2.android.ImageActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi
import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding
import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
import bou.amine.apps.readerforselfossv2.android.model.toModel import bou.amine.apps.readerforselfossv2.android.model.toModel
@ -32,6 +30,7 @@ import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.shareLink import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages import bou.amine.apps.readerforselfossv2.utils.getImages
@ -49,9 +48,6 @@ import org.kodein.di.DI
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import java.util.* import java.util.*
@ -81,6 +77,9 @@ class ArticleFragment : Fragment(), DIAware {
private var font = "" private var font = ""
private var staticBar = false private var staticBar = false
private val mercuryApi : MercuryApi by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -249,26 +248,21 @@ class ArticleFragment : Fragment(), DIAware {
private fun getContentFromMercury() { private fun getContentFromMercury() {
if (repository.isNetworkAvailable()) { if (repository.isNetworkAvailable()) {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
parser.parseUrl(url).enqueue( CoroutineScope(Dispatchers.Main).launch {
object : Callback<ParsedContent> { val response = mercuryApi.query(url)
override fun onResponse( if (response.success) {
call: Call<ParsedContent>,
response: Response<ParsedContent>
) {
// TODO: clean all the following after finding the mercury content issue
try { try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) { if (response.data != null && response.data!!.content != null && !response.data!!.content.isNullOrEmpty()) {
try { try {
binding.titleView.text = response.body()!!.title binding.titleView.text = response.data!!.title
if (typeface != null) { if (typeface != null) {
binding.titleView.typeface = typeface binding.titleView.typeface = typeface
} }
try { try {
// Note: Mercury may return relative urls... If it does the url val will not be changed. // Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.body()!!.url) URL(response.data!!.url)
url = response.body()!!.url url = response.data!!.url
} catch (e: MalformedURLException) { } catch (e: MalformedURLException) {
// Mercury returned a relative url. We do nothing. // Mercury returned a relative url. We do nothing.
} }
@ -276,20 +270,20 @@ class ArticleFragment : Fragment(), DIAware {
} }
try { try {
contentText = response.body()!!.content.orEmpty() contentText = response.data!!.content.orEmpty()
htmlToWebview() htmlToWebview()
} catch (e: Exception) { } catch (e: Exception) {
} }
try { try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) { if (response.data!!.lead_image_url != null && !response.data!!.lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
try { try {
Glide Glide
.with(requireContext()) .with(requireContext())
.asBitmap() .asBitmap()
.load( .load(
response.body()!!.lead_image_url.orEmpty() response.data!!.lead_image_url.orEmpty()
) )
.apply(RequestOptions.fitCenterTransform()) .apply(RequestOptions.fitCenterTransform())
.into(binding.imageView) .into(binding.imageView)
@ -323,14 +317,10 @@ class ArticleFragment : Fragment(), DIAware {
if (context != null) { if (context != null) {
} }
} }
} else {
openInBrowserAfterFailing()
} }
override fun onFailure(
call: Call<ParsedContent>,
t: Throwable
) = openInBrowserAfterFailing()
} }
)
} }
} }
@ -389,7 +379,7 @@ class ArticleFragment : Fragment(), DIAware {
} }
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent?): Boolean { override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick() return performClick()
} }
}) })
@ -410,6 +400,7 @@ class ArticleFragment : Fragment(), DIAware {
val fontName = when (font) { val fontName = when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans" getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto" getString(R.string.roboto_font_id) -> "Roboto"
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> "" else -> ""
} }

View File

@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.model
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import com.google.gson.annotations.SerializedName
fun SelfossModel.Item.toParcelable() : ParecelableItem = fun SelfossModel.Item.toParcelable() : ParecelableItem =
ParecelableItem( ParecelableItem(
@ -34,17 +33,17 @@ fun ParecelableItem.toModel() : SelfossModel.Item =
this.tags.split(",") this.tags.split(",")
) )
data class ParecelableItem( data class ParecelableItem(
@SerializedName("id") val id: Int, val id: Int,
@SerializedName("datetime") val datetime: String, val datetime: String,
@SerializedName("title") val title: String, val title: String,
@SerializedName("content") val content: String, val content: String,
@SerializedName("unread") var unread: Boolean, var unread: Boolean,
@SerializedName("starred") var starred: Boolean, var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String?, val thumbnail: String?,
@SerializedName("icon") val icon: String?, val icon: String?,
@SerializedName("link") val link: String, val link: String,
@SerializedName("sourcetitle") val sourcetitle: String, val sourcetitle: String,
@SerializedName("tags") val tags: String val tags: String
) : Parcelable { ) : Parcelable {
companion object { companion object {

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="name=Source Code Pro&amp;weight=500"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>

View File

@ -9,7 +9,7 @@
<string-array name="ModeValues"> <string-array name="ModeValues">
<item>1</item> <!--MODE_NIGHT_NO--> <item>1</item> <!--MODE_NIGHT_NO-->
<item>2</item> <!--MODE_NIGHT_YES--> <item>2</item> <!--MODE_NIGHT_YES-->
<item>0</item> <!--MODE_NIGHT_AUTO_TIME--> <item>-1</item> <!--MODE_NIGHT_FOLLOW_SYSTEM-->
</string-array> </string-array>
<string-array name="Voice"> <string-array name="Voice">

View File

@ -3,5 +3,6 @@
<array name="preloaded_fonts" translatable="false"> <array name="preloaded_fonts" translatable="false">
<item>@font/open_sans</item> <item>@font/open_sans</item>
<item>@font/roboto</item> <item>@font/roboto</item>
<item>@font/source_code_pro_medium</item>
</array> </array>
</resources> </resources>

View File

@ -4,5 +4,6 @@
<item></item> <item></item>
<item>@string/open_sans_font_id</item> <item>@string/open_sans_font_id</item>
<item>@string/roboto_font_id</item> <item>@string/roboto_font_id</item>
<item>@string/source_code_pro_font_id</item>
</array> </array>
</resources> </resources>

View File

@ -4,5 +4,6 @@
<item>Systems</item> <item>Systems</item>
<item>Open Sans</item> <item>Open Sans</item>
<item>Roboto</item> <item>Roboto</item>
<item>Source Code Pro</item>
</array> </array>
</resources> </resources>

View File

@ -125,6 +125,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string>
<string name="open_sans_font_id" translatable="false">open_sans</string> <string name="open_sans_font_id" translatable="false">open_sans</string>
<string name="roboto_font_id" translatable="false">roboto</string> <string name="roboto_font_id" translatable="false">roboto</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string> <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>

View File

@ -4,6 +4,8 @@ import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.dao.SOURCE import bou.amine.apps.readerforselfossv2.dao.SOURCE
import bou.amine.apps.readerforselfossv2.dao.TAG import bou.amine.apps.readerforselfossv2.dao.TAG
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
@ -47,11 +49,11 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.version() } returns SelfossModel.StatusAndData( coEvery { api.version() } returns StatusAndData(
success = true, success = true,
data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0") data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0")
) )
coEvery { api.stats() } returns SelfossModel.StatusAndData( coEvery { api.stats() } returns StatusAndData(
success = true, success = true,
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED) data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED)
) )
@ -83,7 +85,7 @@ class RepositoryTest {
fun get_api_4_date_with_api_1_version_stored() { fun get_api_4_date_with_api_1_version_stored() {
every { appSettingsService.getApiVersion() } returns 1 every { appSettingsService.getApiVersion() } returns 1
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
every { appSettingsService.updateApiVersion(any()) } returns Unit every { appSettingsService.updateApiVersion(any()) } returns Unit
initializeRepository() initializeRepository()
@ -98,11 +100,11 @@ class RepositoryTest {
@Test @Test
fun get_api_1_date_with_api_4_version_stored() { fun get_api_1_date_with_api_4_version_stored() {
every { appSettingsService.getApiVersion() } returns 4 every { appSettingsService.getApiVersion() } returns 4
coEvery { api.version() } returns SelfossModel.StatusAndData(success = false, null) coEvery { api.version() } returns StatusAndData(success = false, null)
val itemParameters = FakeItemParameters() val itemParameters = FakeItemParameters()
itemParameters.datetime = "2021-04-23 11:45:32" itemParameters.datetime = "2021-04-23 11:45:32"
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData( StatusAndData(
success = true, success = true,
data = generateTestApiItem(itemParameters) data = generateTestApiItem(itemParameters)
) )
@ -118,7 +120,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_items() { fun get_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
runBlocking { runBlocking {
@ -133,7 +135,7 @@ class RepositoryTest {
@Test @Test
fun get_all_newer_items() { fun get_all_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.ALL repository.displayedItems = ItemType.ALL
@ -149,7 +151,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_starred_items() { fun get_newer_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -242,7 +244,7 @@ class RepositoryTest {
@Test @Test
fun get_older_items() { fun get_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -258,7 +260,7 @@ class RepositoryTest {
@Test @Test
fun get_all_older_items() { fun get_all_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -275,7 +277,7 @@ class RepositoryTest {
@Test @Test
fun get_older_starred_items() { fun get_older_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -299,16 +301,16 @@ class RepositoryTest {
} }
assertSame(true, success) assertSame(true, success)
assertSame(NUMBER_ARTICLES, repository.badgeAll) assertEquals(NUMBER_ARTICLES, repository.badgeAll.value)
assertSame(NUMBER_UNREAD, repository.badgeUnread) assertEquals(NUMBER_UNREAD, repository.badgeUnread.value)
assertSame(NUMBER_STARRED, repository.badgeStarred) assertEquals(NUMBER_STARRED, repository.badgeStarred.value)
coVerify(atLeast = 1) { api.stats() } coVerify(atLeast = 1) { api.stats() }
verify(exactly = 0) { db.itemsQueries.items().executeAsList() } verify(exactly = 0) { db.itemsQueries.items().executeAsList() }
} }
@Test @Test
fun reload_badges_without_response() { fun reload_badges_without_response() {
coEvery { api.stats() } returns SelfossModel.StatusAndData(success = false, data = null) coEvery { api.stats() } returns StatusAndData(success = false, data = null)
var success: Boolean var success: Boolean
@ -318,9 +320,9 @@ class RepositoryTest {
} }
assertSame(false, success) assertSame(false, success)
assertSame(0, repository.badgeAll) assertSame(0, repository.badgeAll.value)
assertSame(0, repository.badgeUnread) assertSame(0, repository.badgeUnread.value)
assertSame(0, repository.badgeStarred) assertSame(0, repository.badgeStarred.value)
coVerify(atLeast = 1) { api.stats() } coVerify(atLeast = 1) { api.stats() }
verify(exactly = 0) { db.itemsQueries.items().executeAsList() } verify(exactly = 0) { db.itemsQueries.items().executeAsList() }
} }
@ -338,9 +340,9 @@ class RepositoryTest {
} }
assertTrue(success) assertTrue(success)
assertSame(1, repository.badgeAll) assertEquals(1, repository.badgeAll.value)
assertSame(1, repository.badgeUnread) assertEquals(1, repository.badgeUnread.value)
assertSame(1, repository.badgeStarred) assertEquals(1, repository.badgeStarred.value)
coVerify(exactly = 0) { api.stats() } coVerify(exactly = 0) { api.stats() }
verify(atLeast = 1) { db.itemsQueries.items().executeAsList() } verify(atLeast = 1) { db.itemsQueries.items().executeAsList() }
} }
@ -358,9 +360,9 @@ class RepositoryTest {
} }
assertFalse(success) assertFalse(success)
assertSame(0, repository.badgeAll) assertSame(0, repository.badgeAll.value)
assertSame(0, repository.badgeUnread) assertSame(0, repository.badgeUnread.value)
assertSame(0, repository.badgeStarred) assertSame(0, repository.badgeStarred.value)
coVerify(exactly = 0) { api.stats() } coVerify(exactly = 0) { api.stats() }
verify(exactly = 0) { db.itemsQueries.items().executeAsList() } verify(exactly = 0) { db.itemsQueries.items().executeAsList() }
} }
@ -376,7 +378,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -403,7 +405,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -433,7 +435,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
@ -460,7 +462,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
@ -489,7 +491,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -517,7 +519,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
@ -544,7 +546,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -572,7 +574,7 @@ class RepositoryTest {
TAG("second_DB", "yellow", 0) TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
@ -627,7 +629,7 @@ class RepositoryTest {
) )
) )
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -681,7 +683,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -738,7 +740,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -792,7 +794,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -844,7 +846,7 @@ class RepositoryTest {
) )
) )
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -898,7 +900,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -952,7 +954,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -1006,7 +1008,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
@ -1022,7 +1024,7 @@ class RepositoryTest {
@Test @Test
fun create_source() { fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns
SelfossModel.SuccessResponse(true) SuccessResponse(true)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1052,7 +1054,7 @@ class RepositoryTest {
@Test @Test
fun create_source_but_response_fails() { fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns
SelfossModel.SuccessResponse(false) SuccessResponse(false)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1082,7 +1084,7 @@ class RepositoryTest {
@Test @Test
fun create_source_without_connection() { fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns
SelfossModel.SuccessResponse(true) SuccessResponse(true)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -1111,7 +1113,7 @@ class RepositoryTest {
@Test @Test
fun delete_source() { fun delete_source() {
coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(true) coEvery { api.deleteSource(any()) } returns SuccessResponse(true)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1125,7 +1127,7 @@ class RepositoryTest {
@Test @Test
fun delete_source_but_response_fails() { fun delete_source_but_response_fails() {
coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false) coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1139,7 +1141,7 @@ class RepositoryTest {
@Test @Test
fun delete_source_without_connection() { fun delete_source_without_connection() {
coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false) coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -1153,7 +1155,7 @@ class RepositoryTest {
@Test @Test
fun update_remote() { fun update_remote() {
coEvery { api.update() } returns SelfossModel.StatusAndData( coEvery { api.update() } returns StatusAndData(
success = true, success = true,
data = "finished" data = "finished"
) )
@ -1170,7 +1172,7 @@ class RepositoryTest {
@Test @Test
fun update_remote_but_response_fails() { fun update_remote_but_response_fails() {
coEvery { api.update() } returns SelfossModel.StatusAndData( coEvery { api.update() } returns StatusAndData(
success = false, success = false,
data = "unallowed access" data = "unallowed access"
) )
@ -1187,7 +1189,7 @@ class RepositoryTest {
@Test @Test
fun update_remote_with_unallowed_access() { fun update_remote_with_unallowed_access() {
coEvery { api.update() } returns SelfossModel.StatusAndData( coEvery { api.update() } returns StatusAndData(
success = true, success = true,
data = "unallowed access" data = "unallowed access"
) )
@ -1204,7 +1206,7 @@ class RepositoryTest {
@Test @Test
fun update_remote_without_connection() { fun update_remote_without_connection() {
coEvery { api.update() } returns SelfossModel.StatusAndData( coEvery { api.update() } returns StatusAndData(
success = true, success = true,
data = "undocumented..." data = "undocumented..."
) )
@ -1221,7 +1223,7 @@ class RepositoryTest {
@Test @Test
fun login() { fun login() {
coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true) coEvery { api.login() } returns SuccessResponse(success = true)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1235,7 +1237,7 @@ class RepositoryTest {
@Test @Test
fun login_but_response_fails() { fun login_but_response_fails() {
coEvery { api.login() } returns SelfossModel.SuccessResponse(success = false) coEvery { api.login() } returns SuccessResponse(success = false)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1249,7 +1251,7 @@ class RepositoryTest {
@Test @Test
fun login_but_without_connection() { fun login_but_without_connection() {
coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true) coEvery { api.login() } returns SuccessResponse(success = true)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -1297,9 +1299,9 @@ class RepositoryTest {
any() any()
) )
} returnsMany listOf( } returnsMany listOf(
SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter2)), StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
) )
initializeRepository() initializeRepository()
@ -1323,7 +1325,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_but_response_fails() { fun cache_items_but_response_fails() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.tagFilter = SelfossModel.Tag("Tag", "read", 0) repository.tagFilter = SelfossModel.Tag("Tag", "read", 0)
@ -1346,7 +1348,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_without_connection() { fun cache_items_without_connection() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
SelfossModel.StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
repository.tagFilter = SelfossModel.Tag("Tag", "read", 0) repository.tagFilter = SelfossModel.Tag("Tag", "read", 0)

View File

@ -12,6 +12,7 @@ plugins {
kotlin("android").version("1.7.20").apply(false) kotlin("android").version("1.7.20").apply(false)
kotlin("multiplatform").version("1.7.20").apply(false) kotlin("multiplatform").version("1.7.20").apply(false)
id("org.sonarqube").version("3.4.0.2513").apply(false) id("org.sonarqube").version("3.4.0.2513").apply(false)
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
} }
apply(plugin = "org.sonarqube") apply(plugin = "org.sonarqube")

View File

@ -1,5 +1,6 @@
package bou.amine.apps.readerforselfossv2.DI package bou.amine.apps.readerforselfossv2.DI
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import org.kodein.di.DI import org.kodein.di.DI
@ -10,4 +11,5 @@ import org.kodein.di.singleton
val networkModule by DI.Module { val networkModule by DI.Module {
bind<AppSettingsService>() with singleton { AppSettingsService() } bind<AppSettingsService>() with singleton { AppSettingsService() }
bind<SelfossApi>() with singleton { SelfossApi(instance()) } bind<SelfossApi>() with singleton { SelfossApi(instance()) }
bind<MercuryApi>() with singleton { MercuryApi() }
} }

View File

@ -0,0 +1,157 @@
package bou.amine.apps.readerforselfossv2.model
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
class MercuryModel {
@Serializable
class ParsedContent(
val title: String,
val content: String?,
val lead_image_url: String?,
val url: String
)
@Serializable
data class Tag(
val tag: String,
val color: String,
val unread: Int
)
@Serializable
class Stats(
val total: Int,
val unread: Int,
val starred: Int
)
@Serializable
data class Spout(
val name: String,
val description: String
)
@Serializable
data class ApiVersion(
val version: String?,
val apiversion: String?
) {
fun getApiMajorVersion() : Int {
var versionNumber = 0
if (apiversion != null) {
versionNumber = apiversion.substringBefore(".").toInt()
}
return versionNumber
}
}
@Serializable
data class Source(
val id: Int,
val title: String,
@Serializable(with = TagsListSerializer::class)
val tags: List<String>,
val spout: String,
val error: String,
val icon: String?
)
@Serializable
data class Item(
val id: Int,
val datetime: String,
val title: String,
val content: String,
@Serializable(with = BooleanSerializer::class)
var unread: Boolean,
@Serializable(with = BooleanSerializer::class)
var starred: Boolean,
val thumbnail: String?,
val icon: String?,
val link: String,
val sourcetitle: String,
@Serializable(with = TagsListSerializer::class)
val tags: List<String>
) {
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
// handle url not starting with http
if (stringUrl.startsWith("//")) {
stringUrl = "http:$stringUrl"
}
return stringUrl
}
fun sourceAndDateText(): String =
this.sourcetitle.getHtmlDecoded() + DateUtils.parseRelativeDate(this.datetime)
fun toggleStar(): Item {
this.starred = !this.starred
return this
}
}
// TODO: this seems to be super slow.
object TagsListSerializer : KSerializer<List<String>> {
override fun deserialize(decoder: Decoder): List<String> {
return when(val json = ((decoder as JsonDecoder).decodeJsonElement())) {
is JsonArray -> json.toList().map { it.toString() }
else -> json.toString().split(",")
}
}
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: List<String>) {
TODO("Not yet implemented")
}
}
object BooleanSerializer : KSerializer<Boolean> {
override fun deserialize(decoder: Decoder): Boolean {
val json = ((decoder as JsonDecoder).decodeJsonElement()).jsonPrimitive
return if (json.booleanOrNull != null) {
json.boolean
} else {
json.int == 1
}
}
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("b", PrimitiveKind.BOOLEAN)
override fun serialize(encoder: Encoder, value: Boolean) {
TODO("Not yet implemented")
}
}
}

View File

@ -0,0 +1,40 @@
package bou.amine.apps.readerforselfossv2.model
import io.ktor.client.call.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.serialization.Serializable
@Serializable
class SuccessResponse(val success: Boolean) {
val isSuccess: Boolean
get() = success
}
class StatusAndData<T>(val success: Boolean, val data: T? = null) {
companion object {
fun <T> succes(d: T): StatusAndData<T> {
return StatusAndData(true, d)
}
fun <T> error(): StatusAndData<T> {
return StatusAndData(false)
}
}
}
suspend fun maybeResponse(r: HttpResponse): SuccessResponse {
return if (r.status.isSuccess()) {
r.body()
} else {
SuccessResponse(false)
}
}
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse): StatusAndData<T> {
return if (r.status.isSuccess()) {
StatusAndData.succes(r.body())
} else {
StatusAndData.error()
}
}

View File

@ -20,12 +20,6 @@ class SelfossModel {
val unread: Int val unread: Int
) )
@Serializable
class SuccessResponse(val success: Boolean) {
val isSuccess: Boolean
get() = success
}
@Serializable @Serializable
class Stats( class Stats(
val total: Int, val total: Int,
@ -152,16 +146,4 @@ class SelfossModel {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
class StatusAndData<T>(val success: Boolean, val data: T? = null) {
companion object {
fun <T> succes(d: T): StatusAndData<T> {
return StatusAndData(true, d)
}
fun <T> error(): StatusAndData<T> {
return StatusAndData(false)
}
}
}
} }

View File

@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.repository
import bou.amine.apps.readerforselfossv2.dao.* import bou.amine.apps.readerforselfossv2.dao.*
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.* import bou.amine.apps.readerforselfossv2.utils.*
@ -10,6 +11,7 @@ import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, val isConnectionAvailable: MutableStateFlow<Boolean>, private val db: ReaderForSelfossDB) { class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, val isConnectionAvailable: MutableStateFlow<Boolean>, private val db: ReaderForSelfossDB) {
@ -27,19 +29,19 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
var offlineOverride = false var offlineOverride = false
var badgeUnread = 0 private val _badgeUnread = MutableStateFlow(0)
set(value) {field = if (value < 0) { 0 } else { value } } val badgeUnread = _badgeUnread.asStateFlow()
var badgeAll = 0 private val _badgeAll = MutableStateFlow(0)
set(value) {field = if (value < 0) { 0 } else { value } } val badgeAll = _badgeAll.asStateFlow()
var badgeStarred = 0 private val _badgeStarred = MutableStateFlow(0)
set(value) {field = if (value < 0) { 0 } else { value } } val badgeStarred = _badgeStarred.asStateFlow()
private var fetchedSources = false private var fetchedSources = false
private var fetchedTags = false private var fetchedTags = false
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Use the updatedSince parameter // TODO: Use the updatedSince parameter
var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
var fromDB = false var fromDB = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
fetchedItems = api.getItems( fetchedItems = api.getItems(
@ -64,7 +66,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
if (sourceFilter != null) { if (sourceFilter != null) {
dbItems = dbItems.filter { it.sourcetitle == sourceFilter!!.title } dbItems = dbItems.filter { it.sourcetitle == sourceFilter!!.title }
} }
fetchedItems = SelfossModel.StatusAndData.succes( fetchedItems = StatusAndData.succes(
dbItems.map { it.toView() } dbItems.map { it.toView() }
) )
} }
@ -80,7 +82,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
} }
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> { suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val offset = items.size val offset = items.size
fetchedItems = api.getItems( fetchedItems = api.getItems(
@ -125,17 +127,17 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val response = api.stats() val response = api.stats()
if (response.success && response.data != null) { if (response.success && response.data != null) {
badgeUnread = response.data.unread _badgeUnread.value = response.data.unread
badgeAll = response.data.total _badgeAll.value = response.data.total
badgeStarred = response.data.starred _badgeStarred.value = response.data.starred
success = true success = true
} }
} else if (appSettingsService.isItemCachingEnabled()) { } else if (appSettingsService.isItemCachingEnabled()) {
// TODO: do this differently, because it's not efficient // TODO: do this differently, because it's not efficient
val dbItems = getDBItems() val dbItems = getDBItems()
badgeUnread = dbItems.filter { item -> item.unread }.size _badgeUnread.value = dbItems.filter { item -> item.unread }.size
badgeStarred = dbItems.filter { item -> item.starred }.size _badgeStarred.value = dbItems.filter { item -> item.starred }.size
badgeAll = dbItems.size _badgeAll.value = dbItems.size
success = true success = true
} }
return success return success
@ -283,7 +285,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private fun markAsReadLocally(item: SelfossModel.Item) { private fun markAsReadLocally(item: SelfossModel.Item) {
if (item.unread) { if (item.unread) {
item.unread = false item.unread = false
badgeUnread -= 1 _badgeUnread.value -= 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -294,7 +296,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private fun unmarkAsReadLocally(item: SelfossModel.Item) { private fun unmarkAsReadLocally(item: SelfossModel.Item) {
if (!item.unread) { if (!item.unread) {
item.unread = true item.unread = true
badgeUnread += 1 _badgeUnread.value += 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -305,7 +307,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private fun starrLocally(item: SelfossModel.Item) { private fun starrLocally(item: SelfossModel.Item) {
if (!item.starred) { if (!item.starred) {
item.starred = true item.starred = true
badgeStarred += 1 _badgeStarred.value += 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -316,7 +318,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private fun unstarrLocally(item: SelfossModel.Item) { private fun unstarrLocally(item: SelfossModel.Item) {
if (item.starred) { if (item.starred) {
item.starred = false item.starred = false
badgeStarred -= 1 _badgeStarred.value -= 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {

View File

@ -0,0 +1,43 @@
package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.*
import io.github.aakira.napier.Napier
import io.ktor.client.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
class MercuryApi() {
var client = createHttpClient()
private fun createHttpClient(): HttpClient {
return HttpClient {
install(ContentNegotiation) {
install(HttpCache)
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Napier.d(message, tag = "LogMercuryCalls")
}
}
level = LogLevel.INFO
}
expectSuccess = false
}
}
suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> =
bodyOrFailure(client.get("https://amine-louveau.fr/parser.php") {
parameter("link", url)
})
}

View File

@ -1,6 +1,6 @@
package bou.amine.apps.readerforselfossv2.rest package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.*
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
@ -66,7 +66,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client = createHttpClient() client = createHttpClient()
} }
suspend fun login(): SelfossModel.SuccessResponse = suspend fun login(): SuccessResponse =
maybeResponse(client.get(url("/login")) { maybeResponse(client.get(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
@ -80,7 +80,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
search: String?, search: String?,
updatedSince: String?, updatedSince: String?,
items: Int? = null items: Int? = null
): SelfossModel.StatusAndData<List<SelfossModel.Item>> = ): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(client.get(url("/items")) { bodyOrFailure(client.get(url("/items")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
@ -93,64 +93,64 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("offset", offset) parameter("offset", offset)
}) })
suspend fun stats(): SelfossModel.StatusAndData<SelfossModel.Stats> = suspend fun stats(): StatusAndData<SelfossModel.Stats> =
bodyOrFailure(client.get(url("/stats")) { bodyOrFailure(client.get(url("/stats")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun tags(): SelfossModel.StatusAndData<List<SelfossModel.Tag>> = suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
bodyOrFailure(client.get(url("/tags")) { bodyOrFailure(client.get(url("/tags")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun update(): SelfossModel.StatusAndData<String> = suspend fun update(): StatusAndData<String> =
bodyOrFailure(client.get(url("/update")) { bodyOrFailure(client.get(url("/update")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun spouts(): SelfossModel.StatusAndData<Map<String, SelfossModel.Spout>> = suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
bodyOrFailure(client.get(url("/sources/spouts")) { bodyOrFailure(client.get(url("/sources/spouts")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun sources(): SelfossModel.StatusAndData<ArrayList<SelfossModel.Source>> = suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> =
bodyOrFailure(client.get(url("/sources/list")) { bodyOrFailure(client.get(url("/sources/list")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun version(): SelfossModel.StatusAndData<SelfossModel.ApiVersion> = suspend fun version(): StatusAndData<SelfossModel.ApiVersion> =
bodyOrFailure(client.get(url("/api/about"))) bodyOrFailure(client.get(url("/api/about")))
suspend fun markAsRead(id: String): SelfossModel.SuccessResponse = suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse(client.post(url("/mark/$id")) { maybeResponse(client.post(url("/mark/$id")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse = suspend fun unmarkAsRead(id: String): SuccessResponse =
maybeResponse(client.post(url("/unmark/$id")) { maybeResponse(client.post(url("/unmark/$id")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun starr(id: String): SelfossModel.SuccessResponse = suspend fun starr(id: String): SuccessResponse =
maybeResponse(client.post(url("/starr/$id")) { maybeResponse(client.post(url("/starr/$id")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun unstarr(id: String): SelfossModel.SuccessResponse = suspend fun unstarr(id: String): SuccessResponse =
maybeResponse(client.post(url("/unstarr/$id")) { maybeResponse(client.post(url("/unstarr/$id")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse = suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
maybeResponse(client.submitForm( maybeResponse(client.submitForm(
url = url("/mark"), url = url("/mark"),
formParameters = Parameters.build { formParameters = Parameters.build {
@ -167,7 +167,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
tags: String, tags: String,
filter: String, filter: String,
version: Int version: Int
): SelfossModel.SuccessResponse = ): SuccessResponse =
maybeResponse( maybeResponse(
if (version > 1) { if (version > 1) {
createSource2(title, url, spout, tags, filter) createSource2(title, url, spout, tags, filter)
@ -212,25 +212,9 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
) )
suspend fun deleteSource(id: Int): SelfossModel.SuccessResponse = suspend fun deleteSource(id: Int): SuccessResponse =
maybeResponse(client.delete(url("/source/$id")) { maybeResponse(client.delete(url("/source/$id")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
suspend fun maybeResponse(r: HttpResponse): SelfossModel.SuccessResponse {
return if (r.status.isSuccess()) {
r.body()
} else {
SelfossModel.SuccessResponse(false)
}
}
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse): SelfossModel.StatusAndData<T> {
return if (r.status.isSuccess()) {
SelfossModel.StatusAndData.succes(r.body())
} else {
SelfossModel.StatusAndData.error()
}
}
} }

View File

@ -35,6 +35,7 @@ class AppSettingsService {
private var _fontSize: Int? = null private var _fontSize: Int? = null
private var _staticBar: Boolean? = null private var _staticBar: Boolean? = null
private var _font: String = "" private var _font: String = ""
private var _theme: Int? = null
init { init {
@ -318,6 +319,17 @@ class AppSettingsService {
return _font return _font
} }
private fun refreshCurrentTheme() {
_theme = settings.getString(CURRENT_THEME, "-1").toInt()
}
fun getCurrentTheme(): Int {
if (_theme == null) {
refreshCurrentTheme()
}
return _theme ?: -1
}
fun refreshApiSettings() { fun refreshApiSettings() {
refreshPassword() refreshPassword()
refreshUsername() refreshUsername()
@ -346,6 +358,7 @@ class AppSettingsService {
refreshFontSize() refreshFontSize()
refreshFont() refreshFont()
refreshStaticBarEnabled() refreshStaticBarEnabled()
refreshCurrentTheme()
} }
fun refreshLoginInformation( fun refreshLoginInformation(
@ -444,5 +457,7 @@ class AppSettingsService {
const val INFINITE_LOADING = "infinite_loading" const val INFINITE_LOADING = "infinite_loading"
const val ITEMS_CACHING = "items_caching" const val ITEMS_CACHING = "items_caching"
const val CURRENT_THEME = "currentMode"
} }
} }