Compare commits

..

No commits in common. "a4527940b85de608cab74ee682bc63bdb062813e" and "16b10dc1b77e638e87d3d7b663fa4c2d150b6bb7" have entirely different histories.

29 changed files with 369 additions and 521 deletions

View File

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

View File

@ -6,7 +6,6 @@ plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
id("com.mikepenz.aboutlibraries.plugin")
}
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
@ -63,7 +62,7 @@ android {
kotlinOptions {
jvmTarget = "11"
}
compileSdk = 33
compileSdk = 32
buildToolsVersion = "31.0.0"
buildFeatures {
viewBinding = true
@ -71,7 +70,7 @@ android {
defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 21
targetSdk = 33
targetSdk = 32
versionCode = versionCodeFromGit()
versionName = versionNameFromGit()
@ -139,8 +138,15 @@ dependencies {
implementation("androidx.multidex:multidex:2.0.1")
// About
implementation("com.mikepenz:aboutlibraries-core:10.5.1")
implementation("com.mikepenz:aboutlibraries:10.5.1")
implementation("com.mikepenz:aboutlibraries-core:8.9.4")
implementation("com.mikepenz:aboutlibraries:8.9.4")
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
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
@ -204,13 +210,3 @@ tasks.withType<Test> {
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,8 +30,15 @@
<fields>;
}
-dontwarn okio.**
-dontwarn retrofit2.Platform$Java8
-keep class retrofit.** { *; }
-keepclasseswithmembers class * {
@retrofit.http.* <methods>;
}
-keepattributes *Annotation*,Signature
-keepattributes Exceptions
-dontwarn okio.**
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault

View File

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

View File

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

View File

@ -3,14 +3,11 @@ package bou.amine.apps.readerforselfossv2.android
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.res.Configuration
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.*
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
@ -51,7 +48,6 @@ class MyApp : MultiDexApplication(), DIAware {
private val viewModel: AppViewModel by instance()
private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance()
private val appSettingsService : AppSettingsService by instance()
// TODO: handle with the "previous" way
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
@ -136,19 +132,6 @@ 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 {
override fun onResume(owner: LifecycleOwner) {

View File

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

View File

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

View File

@ -0,0 +1,35 @@
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

@ -0,0 +1,59 @@
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

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

View File

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

View File

@ -1,7 +0,0 @@
<?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">
<item>1</item> <!--MODE_NIGHT_NO-->
<item>2</item> <!--MODE_NIGHT_YES-->
<item>-1</item> <!--MODE_NIGHT_FOLLOW_SYSTEM-->
<item>0</item> <!--MODE_NIGHT_AUTO_TIME-->
</string-array>
<string-array name="Voice">

View File

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

View File

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

View File

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

View File

@ -125,7 +125,6 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</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="roboto_font_id" translatable="false">roboto</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>

View File

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

View File

@ -12,7 +12,6 @@ plugins {
kotlin("android").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("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
}
apply(plugin = "org.sonarqube")

View File

@ -1,6 +1,5 @@
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.service.AppSettingsService
import org.kodein.di.DI
@ -11,5 +10,4 @@ import org.kodein.di.singleton
val networkModule by DI.Module {
bind<AppSettingsService>() with singleton { AppSettingsService() }
bind<SelfossApi>() with singleton { SelfossApi(instance()) }
bind<MercuryApi>() with singleton { MercuryApi() }
}

View File

@ -1,157 +0,0 @@
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

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

View File

@ -1,43 +0,0 @@
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
import bou.amine.apps.readerforselfossv2.model.*
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.ktor.client.*
import io.ktor.client.call.*
@ -66,7 +66,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client = createHttpClient()
}
suspend fun login(): SuccessResponse =
suspend fun login(): SelfossModel.SuccessResponse =
maybeResponse(client.get(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
@ -80,7 +80,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
search: String?,
updatedSince: String?,
items: Int? = null
): StatusAndData<List<SelfossModel.Item>> =
): SelfossModel.StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(client.get(url("/items")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
@ -93,64 +93,64 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("offset", offset)
})
suspend fun stats(): StatusAndData<SelfossModel.Stats> =
suspend fun stats(): SelfossModel.StatusAndData<SelfossModel.Stats> =
bodyOrFailure(client.get(url("/stats")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
suspend fun tags(): SelfossModel.StatusAndData<List<SelfossModel.Tag>> =
bodyOrFailure(client.get(url("/tags")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun update(): StatusAndData<String> =
suspend fun update(): SelfossModel.StatusAndData<String> =
bodyOrFailure(client.get(url("/update")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
suspend fun spouts(): SelfossModel.StatusAndData<Map<String, SelfossModel.Spout>> =
bodyOrFailure(client.get(url("/sources/spouts")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> =
suspend fun sources(): SelfossModel.StatusAndData<ArrayList<SelfossModel.Source>> =
bodyOrFailure(client.get(url("/sources/list")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun version(): StatusAndData<SelfossModel.ApiVersion> =
suspend fun version(): SelfossModel.StatusAndData<SelfossModel.ApiVersion> =
bodyOrFailure(client.get(url("/api/about")))
suspend fun markAsRead(id: String): SuccessResponse =
suspend fun markAsRead(id: String): SelfossModel.SuccessResponse =
maybeResponse(client.post(url("/mark/$id")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun unmarkAsRead(id: String): SuccessResponse =
suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse =
maybeResponse(client.post(url("/unmark/$id")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun starr(id: String): SuccessResponse =
suspend fun starr(id: String): SelfossModel.SuccessResponse =
maybeResponse(client.post(url("/starr/$id")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun unstarr(id: String): SuccessResponse =
suspend fun unstarr(id: String): SelfossModel.SuccessResponse =
maybeResponse(client.post(url("/unstarr/$id")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse =
maybeResponse(client.submitForm(
url = url("/mark"),
formParameters = Parameters.build {
@ -167,7 +167,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
tags: String,
filter: String,
version: Int
): SuccessResponse =
): SelfossModel.SuccessResponse =
maybeResponse(
if (version > 1) {
createSource2(title, url, spout, tags, filter)
@ -212,9 +212,25 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
}
)
suspend fun deleteSource(id: Int): SuccessResponse =
suspend fun deleteSource(id: Int): SelfossModel.SuccessResponse =
maybeResponse(client.delete(url("/source/$id")) {
parameter("username", appSettingsService.getUserName())
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,7 +35,6 @@ class AppSettingsService {
private var _fontSize: Int? = null
private var _staticBar: Boolean? = null
private var _font: String = ""
private var _theme: Int? = null
init {
@ -319,17 +318,6 @@ class AppSettingsService {
return _font
}
private fun refreshCurrentTheme() {
_theme = settings.getString(CURRENT_THEME, "-1").toInt()
}
fun getCurrentTheme(): Int {
if (_theme == null) {
refreshCurrentTheme()
}
return _theme ?: -1
}
fun refreshApiSettings() {
refreshPassword()
refreshUsername()
@ -358,7 +346,6 @@ class AppSettingsService {
refreshFontSize()
refreshFont()
refreshStaticBarEnabled()
refreshCurrentTheme()
}
fun refreshLoginInformation(
@ -457,7 +444,5 @@ class AppSettingsService {
const val INFINITE_LOADING = "infinite_loading"
const val ITEMS_CACHING = "items_caching"
const val CURRENT_THEME = "currentMode"
}
}