Compare commits
	
		
			24 Commits
		
	
	
		
			16b10dc1b7
			...
			v122113171
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					25bf68cf0c | ||
| 
						 | 
					afc6f392c6 | ||
| 
						 | 
					a0b5e2052b | ||
| 
						 | 
					87d1ef2bce | ||
| 
						 | 
					537a6d3a0b | ||
| dbe97f564e | |||
| 
						 | 
					3a3bf03114 | ||
| c09a32e9ad | |||
| b02a588dff | |||
| 
						 | 
					a4527940b8 | ||
| 
						 | 
					9e8a25ed3e | ||
| 
						 | 
					8ea46e146b | ||
| 
						 | 
					5ecf3c3f87 | ||
| 
						 | 
					325f103417 | ||
| 
						 | 
					ab4b1ae644 | ||
| 
						 | 
					87ea44754e | ||
| 
						 | 
					04dec50808 | ||
| 
						 | 
					e36189e2e7 | ||
| 
						 | 
					d6bdf510a4 | ||
| 
						 | 
					a464e93370 | ||
| 4b63afe62a | |||
| ac4c4b9441 | |||
| 02d734eee8 | |||
| c5cdfc0d53 | 
							
								
								
									
										12
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								.drone.yml
									
									
									
									
									
								
							@@ -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" >> ~/.gradle/gradle.properties
 | 
			
		||||
      - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\npushCache=false\nmatomoUrl=\"$MATOMO_URL\"\nmatomoSite=\"$MATOMO_SITE\"\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
 | 
			
		||||
      - echo "---------------------------------------------------------"
 | 
			
		||||
      - echo "Analysing..."
 | 
			
		||||
      - ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN
 | 
			
		||||
@@ -24,6 +24,10 @@ steps:
 | 
			
		||||
        from_secret: sonarScannerHostUrl
 | 
			
		||||
      SONAR_LOGIN:
 | 
			
		||||
        from_secret: sonarScannerLogin
 | 
			
		||||
      MATOMO_URL:
 | 
			
		||||
        from_secret: matomoUrl
 | 
			
		||||
      MATOMO_SITE:
 | 
			
		||||
        from_secret: matomoSite
 | 
			
		||||
trigger:
 | 
			
		||||
  event:
 | 
			
		||||
    - push
 | 
			
		||||
@@ -90,7 +94,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" >> ~/.gradle/gradle.properties
 | 
			
		||||
      - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nmatomoUrl=\"$MATOMO_URL\"\nmatomoSite=\"$MATOMO_SITE\"\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
 | 
			
		||||
      - echo "---------------------------------------------------------"
 | 
			
		||||
      - echo "Generate APK"
 | 
			
		||||
      - ./gradlew :androidApp:assembleGithubConfigRelease  -P pushCache=false
 | 
			
		||||
@@ -111,6 +115,10 @@ steps:
 | 
			
		||||
        from_secret: keyPass
 | 
			
		||||
      YOUR_KEY_ALIAS:
 | 
			
		||||
        from_secret: keyAlias
 | 
			
		||||
      MATOMO_URL:
 | 
			
		||||
        from_secret: matomoUrl
 | 
			
		||||
      MATOMO_SITE:
 | 
			
		||||
        from_secret: matomoSite
 | 
			
		||||
 | 
			
		||||
  - name: gitea_release
 | 
			
		||||
    image: plugins/gitea-release
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							@@ -55,19 +55,18 @@ You'll have to:
 | 
			
		||||
 | 
			
		||||
- Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples)
 | 
			
		||||
 | 
			
		||||
    - appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.**
 | 
			
		||||
    - matomoUrl and matomoSite: url and siteId for a matomo instance
 | 
			
		||||
 | 
			
		||||
### Examples:
 | 
			
		||||
#### Inside ~/.gradle/gradle.properties
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
appLoginUrl="URL" # It can be empty.
 | 
			
		||||
appLoginUsername="LOGIN" # It can be empty.
 | 
			
		||||
appLoginPassword="PASS" # It can be empty.
 | 
			
		||||
matomoUrl="URL" # It can be empty.
 | 
			
		||||
matomoSite="1" # It can be empty, but needs to be an integer
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### As gradle parameters
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS"
 | 
			
		||||
./gradlew .... -P matomoUrl="URL" -P matomoSite="1"
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ plugins {
 | 
			
		||||
    id("com.android.application")
 | 
			
		||||
    kotlin("android")
 | 
			
		||||
    kotlin("kapt")
 | 
			
		||||
    id("com.mikepenz.aboutlibraries.plugin")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
 | 
			
		||||
@@ -62,7 +63,7 @@ android {
 | 
			
		||||
    kotlinOptions {
 | 
			
		||||
        jvmTarget = "11"
 | 
			
		||||
    }
 | 
			
		||||
    compileSdk = 32
 | 
			
		||||
    compileSdk = 33
 | 
			
		||||
    buildToolsVersion = "31.0.0"
 | 
			
		||||
    buildFeatures {
 | 
			
		||||
        viewBinding = true
 | 
			
		||||
@@ -70,7 +71,7 @@ android {
 | 
			
		||||
    defaultConfig {
 | 
			
		||||
        applicationId = "bou.amine.apps.readerforselfossv2.android"
 | 
			
		||||
        minSdk = 21
 | 
			
		||||
        targetSdk = 32
 | 
			
		||||
        targetSdk = 33
 | 
			
		||||
        versionCode = versionCodeFromGit()
 | 
			
		||||
        versionName = versionNameFromGit()
 | 
			
		||||
 | 
			
		||||
@@ -82,6 +83,9 @@ android {
 | 
			
		||||
 | 
			
		||||
        // tests
 | 
			
		||||
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
 | 
			
		||||
 | 
			
		||||
        buildConfigField("String", "MATOMO_URL", properties["matomoUrl"] as String)
 | 
			
		||||
        buildConfigField("String", "MATOMO_SITE", properties["matomoSite"] as String)
 | 
			
		||||
    }
 | 
			
		||||
    packagingOptions {
 | 
			
		||||
        resources {
 | 
			
		||||
@@ -95,9 +99,6 @@ android {
 | 
			
		||||
            proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
 | 
			
		||||
        }
 | 
			
		||||
        getByName("debug") {
 | 
			
		||||
            buildConfigField("String", "LOGIN_URL", properties["appLoginUrl"] as String)
 | 
			
		||||
            buildConfigField("String", "LOGIN_PASSWORD", properties["appLoginPassword"] as String)
 | 
			
		||||
            buildConfigField("String", "LOGIN_USERNAME", properties["appLoginUsername"] as String)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    flavorDimensions.add("build")
 | 
			
		||||
@@ -138,15 +139,8 @@ dependencies {
 | 
			
		||||
    implementation("androidx.multidex:multidex:2.0.1")
 | 
			
		||||
 | 
			
		||||
    // About
 | 
			
		||||
    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")
 | 
			
		||||
    implementation("com.mikepenz:aboutlibraries-core:10.5.1")
 | 
			
		||||
    implementation("com.mikepenz:aboutlibraries:10.5.1")
 | 
			
		||||
 | 
			
		||||
    // Material-ish things
 | 
			
		||||
    implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
 | 
			
		||||
@@ -195,6 +189,9 @@ dependencies {
 | 
			
		||||
    testImplementation("io.mockk:mockk:1.12.0")
 | 
			
		||||
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
 | 
			
		||||
    implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
 | 
			
		||||
 | 
			
		||||
    // Matomo
 | 
			
		||||
    implementation("com.github.matomo-org:matomo-sdk-android:4.1.4")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.withType<Test> {
 | 
			
		||||
@@ -209,4 +206,14 @@ 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								androidApp/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								androidApp/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							@@ -30,15 +30,8 @@
 | 
			
		||||
    <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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,8 @@
 | 
			
		||||
        android:supportsRtl="true"
 | 
			
		||||
        android:networkSecurityConfig="@xml/network_security_config"
 | 
			
		||||
        android:theme="@style/NoBar"
 | 
			
		||||
        android:dataExtractionRules="@xml/data_extraction_rules">
 | 
			
		||||
        android:dataExtractionRules="@xml/data_extraction_rules"
 | 
			
		||||
        android:configChanges="uiMode">
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".MainActivity"
 | 
			
		||||
            android:theme="@style/SplashTheme"
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ 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
 | 
			
		||||
@@ -34,7 +35,10 @@ import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.*
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.longHash
 | 
			
		||||
import com.ashokvarma.bottomnavigation.BottomNavigationBar
 | 
			
		||||
import com.ashokvarma.bottomnavigation.BottomNavigationItem
 | 
			
		||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
 | 
			
		||||
@@ -58,6 +62,9 @@ import kotlinx.coroutines.launch
 | 
			
		||||
import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
import org.matomo.sdk.Tracker
 | 
			
		||||
import org.matomo.sdk.extra.DimensionQueue
 | 
			
		||||
import org.matomo.sdk.extra.TrackHelper
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -94,12 +101,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
    override val di by closestDI()
 | 
			
		||||
    private val repository : Repository by instance()
 | 
			
		||||
    private val appSettingsService : AppSettingsService by instance()
 | 
			
		||||
    private val tracker : Tracker by instance()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        binding = ActivityHomeBinding.inflate(layoutInflater)
 | 
			
		||||
        val view = binding.root
 | 
			
		||||
 | 
			
		||||
        TrackHelper.track().screen("/home").with(tracker)
 | 
			
		||||
 | 
			
		||||
        fromTabShortcut =  intent.getIntExtra("shortcutTab", -1) != -1
 | 
			
		||||
        repository.offlineOverride =  intent.getBooleanExtra("startOffline", false)
 | 
			
		||||
 | 
			
		||||
@@ -178,8 +189,6 @@ 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)) {
 | 
			
		||||
@@ -207,6 +216,16 @@ 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()
 | 
			
		||||
@@ -219,6 +238,28 @@ 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,
 | 
			
		||||
@@ -714,29 +755,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
 | 
			
		||||
    private fun reloadBadges() {
 | 
			
		||||
        if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
 | 
			
		||||
            CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
            CoroutineScope(Dispatchers.IO).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()))
 | 
			
		||||
@@ -858,10 +882,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
 | 
			
		||||
    private fun maxItemNumber(): Int =
 | 
			
		||||
        when (elementsShown) {
 | 
			
		||||
            ItemType.UNREAD -> repository.badgeUnread
 | 
			
		||||
            ItemType.ALL -> repository.badgeAll
 | 
			
		||||
            ItemType.STARRED -> repository.badgeStarred
 | 
			
		||||
            else -> repository.badgeUnread // if !elementsShown then unread are fetched.
 | 
			
		||||
            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.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android
 | 
			
		||||
 | 
			
		||||
import android.animation.Animator
 | 
			
		||||
import android.animation.AnimatorListenerAdapter
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.text.TextUtils
 | 
			
		||||
@@ -24,6 +25,12 @@ import kotlinx.coroutines.launch
 | 
			
		||||
import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
import org.matomo.sdk.Tracker
 | 
			
		||||
import org.matomo.sdk.extra.DimensionQueue
 | 
			
		||||
import org.matomo.sdk.extra.DownloadTracker
 | 
			
		||||
import org.matomo.sdk.extra.TrackHelper
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
 | 
			
		||||
@@ -35,10 +42,17 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
    override val di by closestDI()
 | 
			
		||||
    private val repository : Repository by instance()
 | 
			
		||||
    private val appSettingsService : AppSettingsService by instance()
 | 
			
		||||
    private val tracker : Tracker by instance()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
 | 
			
		||||
 | 
			
		||||
        TrackHelper.track().download().identifier(DownloadTracker.Extra.ApkChecksum(applicationContext))
 | 
			
		||||
            .with(tracker)
 | 
			
		||||
        TrackHelper.track().screen("/login").with(tracker)
 | 
			
		||||
 | 
			
		||||
        handleTheme()
 | 
			
		||||
 | 
			
		||||
        binding = ActivityLoginBinding.inflate(layoutInflater)
 | 
			
		||||
        val view = binding.root
 | 
			
		||||
@@ -56,6 +70,11 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
        handleActions()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("WrongConstant") // Constant is fetched from the settings
 | 
			
		||||
    private fun handleTheme() {
 | 
			
		||||
        AppCompatDelegate.setDefaultNightMode(appSettingsService.getCurrentTheme())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleActions() {
 | 
			
		||||
 | 
			
		||||
        binding.passwordView.setOnEditorActionListener(
 | 
			
		||||
@@ -95,6 +114,15 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
    private fun goToMain() {
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
            repository.updateApiVersion()
 | 
			
		||||
 | 
			
		||||
            val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
 | 
			
		||||
            messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
 | 
			
		||||
            tracker.userId = String(messageDigest.digest())
 | 
			
		||||
 | 
			
		||||
            val mDimensionQueue = DimensionQueue(tracker)
 | 
			
		||||
            mDimensionQueue.add(1, appSettingsService.getApiVersion().toString())
 | 
			
		||||
 | 
			
		||||
            tracker.isOptOut = !appSettingsService.isAnalyticsEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        val intent = Intent(this, HomeActivity::class.java)
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner
 | 
			
		||||
import androidx.lifecycle.ProcessLifecycleOwner
 | 
			
		||||
import androidx.multidex.MultiDexApplication
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.DI.networkModule
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
			
		||||
@@ -32,6 +30,9 @@ import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.kodein.di.*
 | 
			
		||||
import org.matomo.sdk.Matomo
 | 
			
		||||
import org.matomo.sdk.Tracker
 | 
			
		||||
import org.matomo.sdk.TrackerBuilder
 | 
			
		||||
 | 
			
		||||
class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
 | 
			
		||||
@@ -42,12 +43,15 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
        bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) }
 | 
			
		||||
        bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
 | 
			
		||||
        bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
 | 
			
		||||
        bind<Tracker>() with singleton { TrackerBuilder.createDefault(BuildConfig.MATOMO_URL, BuildConfig.MATOMO_SITE.toInt()).build(
 | 
			
		||||
            Matomo.getInstance(applicationContext)) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val repository: Repository by instance()
 | 
			
		||||
    private val viewModel: AppViewModel by instance()
 | 
			
		||||
    private val connectivityStatus: ConnectivityStatus by instance()
 | 
			
		||||
    private val driverFactory: DriverFactory by instance()
 | 
			
		||||
    private val tracker: Tracker by instance()
 | 
			
		||||
 | 
			
		||||
    // TODO: handle with the "previous" way
 | 
			
		||||
    private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
 | 
			
		||||
@@ -122,10 +126,9 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
        val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
 | 
			
		||||
 | 
			
		||||
        Thread.setDefaultUncaughtExceptionHandler { thread, e ->
 | 
			
		||||
            if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any {
 | 
			
		||||
            if (e is NoClassDefFoundError && e.stackTrace.asList().any {
 | 
			
		||||
                    it.toString().contains("android.view.ViewDebug")
 | 
			
		||||
                }) {
 | 
			
		||||
                Unit
 | 
			
		||||
            } else {
 | 
			
		||||
                oldHandler.uncaughtException(thread, e)
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -111,13 +111,11 @@ 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
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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>
 | 
			
		||||
}
 | 
			
		||||
@@ -21,8 +21,6 @@ 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
 | 
			
		||||
@@ -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.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
 | 
			
		||||
@@ -49,9 +48,6 @@ 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.*
 | 
			
		||||
@@ -81,6 +77,9 @@ 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)
 | 
			
		||||
 | 
			
		||||
@@ -249,88 +248,79 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
    private fun getContentFromMercury() {
 | 
			
		||||
        if (repository.isNetworkAvailable()) {
 | 
			
		||||
            binding.progressBar.visibility = View.VISIBLE
 | 
			
		||||
            val parser = MercuryApi()
 | 
			
		||||
 | 
			
		||||
            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.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
 | 
			
		||||
            CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
                 val response = mercuryApi.query(url)
 | 
			
		||||
                if (response.success) {
 | 
			
		||||
                    try {
 | 
			
		||||
                        if (response.data != null && response.data!!.content != null && !response.data!!.content.isNullOrEmpty()) {
 | 
			
		||||
                            try {
 | 
			
		||||
                                binding.titleView.text = response.data!!.title
 | 
			
		||||
                                if (typeface != null) {
 | 
			
		||||
                                    binding.titleView.typeface = typeface
 | 
			
		||||
                                }
 | 
			
		||||
                                try {
 | 
			
		||||
                                    binding.titleView.text = response.body()!!.title
 | 
			
		||||
                                    if (typeface != null) {
 | 
			
		||||
                                        binding.titleView.typeface = typeface
 | 
			
		||||
                                    }
 | 
			
		||||
                                    // Note: Mercury may return relative urls... If it does the url val will not be changed.
 | 
			
		||||
                                    URL(response.data!!.url)
 | 
			
		||||
                                    url = response.data!!.url
 | 
			
		||||
                                } catch (e: MalformedURLException) {
 | 
			
		||||
                                    // Mercury returned a relative url. We do nothing.
 | 
			
		||||
                                }
 | 
			
		||||
                            } catch (e: Exception) {
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            try {
 | 
			
		||||
                                contentText = response.data!!.content.orEmpty()
 | 
			
		||||
                                htmlToWebview()
 | 
			
		||||
                            } catch (e: Exception) {
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            try {
 | 
			
		||||
                                if (response.data!!.lead_image_url != null && !response.data!!.lead_image_url.isNullOrEmpty() && context != null) {
 | 
			
		||||
                                    binding.imageView.visibility = View.VISIBLE
 | 
			
		||||
                                    try {
 | 
			
		||||
                                        // Note: Mercury may return relative urls... If it does the url val will not be changed.
 | 
			
		||||
                                        URL(response.body()!!.url)
 | 
			
		||||
                                        url = response.body()!!.url
 | 
			
		||||
                                    } catch (e: MalformedURLException) {
 | 
			
		||||
                                        // Mercury returned a relative url. We do nothing.
 | 
			
		||||
                                        Glide
 | 
			
		||||
                                            .with(requireContext())
 | 
			
		||||
                                            .asBitmap()
 | 
			
		||||
                                            .load(
 | 
			
		||||
                                                response.data!!.lead_image_url.orEmpty()
 | 
			
		||||
                                            )
 | 
			
		||||
                                            .apply(RequestOptions.fitCenterTransform())
 | 
			
		||||
                                            .into(binding.imageView)
 | 
			
		||||
                                    } catch (e: Exception) {
 | 
			
		||||
                                    }
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    binding.imageView.visibility = View.GONE
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                try {
 | 
			
		||||
                                    contentText = response.body()!!.content.orEmpty()
 | 
			
		||||
                                    htmlToWebview()
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                try {
 | 
			
		||||
                                    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.body()!!.lead_image_url.orEmpty()
 | 
			
		||||
                                                )
 | 
			
		||||
                                                .apply(RequestOptions.fitCenterTransform())
 | 
			
		||||
                                                .into(binding.imageView)
 | 
			
		||||
                                        } catch (e: Exception) {
 | 
			
		||||
                                        }
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        binding.imageView.visibility = View.GONE
 | 
			
		||||
                                    }
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                    if (context != null) {
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                try {
 | 
			
		||||
                                    binding.nestedScrollView.scrollTo(0, 0)
 | 
			
		||||
 | 
			
		||||
                                    binding.progressBar.visibility = View.GONE
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                    if (context != null) {
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                try {
 | 
			
		||||
                                    openInBrowserAfterFailing()
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                    if (context != null) {
 | 
			
		||||
                                    }
 | 
			
		||||
                            } catch (e: Exception) {
 | 
			
		||||
                                if (context != null) {
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        } catch (e: Exception) {
 | 
			
		||||
                            if (context != null) {
 | 
			
		||||
 | 
			
		||||
                            try {
 | 
			
		||||
                                binding.nestedScrollView.scrollTo(0, 0)
 | 
			
		||||
 | 
			
		||||
                                binding.progressBar.visibility = View.GONE
 | 
			
		||||
                            } catch (e: Exception) {
 | 
			
		||||
                                if (context != null) {
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        } else {
 | 
			
		||||
                            try {
 | 
			
		||||
                                openInBrowserAfterFailing()
 | 
			
		||||
                            } catch (e: Exception) {
 | 
			
		||||
                                if (context != null) {
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } catch (e: Exception) {
 | 
			
		||||
                        if (context != null) {
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onFailure(
 | 
			
		||||
                        call: Call<ParsedContent>,
 | 
			
		||||
                        t: Throwable
 | 
			
		||||
                    ) = openInBrowserAfterFailing()
 | 
			
		||||
                } else {
 | 
			
		||||
                    openInBrowserAfterFailing()
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -389,7 +379,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()
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
@@ -410,6 +400,7 @@ 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 -> ""
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ 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(
 | 
			
		||||
@@ -34,17 +33,17 @@ fun ParecelableItem.toModel() : SelfossModel.Item =
 | 
			
		||||
        this.tags.split(",")
 | 
			
		||||
    )
 | 
			
		||||
data class ParecelableItem(
 | 
			
		||||
    @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
 | 
			
		||||
    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
 | 
			
		||||
) : Parcelable {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,16 +17,26 @@ import androidx.preference.PreferenceFragmentCompat
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
import org.matomo.sdk.Tracker
 | 
			
		||||
import org.matomo.sdk.extra.TrackHelper
 | 
			
		||||
 | 
			
		||||
private const val TITLE_TAG = "settingsActivityTitle"
 | 
			
		||||
 | 
			
		||||
class SettingsActivity : AppCompatActivity(),
 | 
			
		||||
        PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
 | 
			
		||||
        PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, DIAware {
 | 
			
		||||
    override val di by closestDI()
 | 
			
		||||
 | 
			
		||||
    private val tracker : Tracker by instance()
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        val binding = ActivitySettingsBinding.inflate(layoutInflater)
 | 
			
		||||
 | 
			
		||||
        TrackHelper.track().screen("/settings").with(tracker)
 | 
			
		||||
 | 
			
		||||
        setContentView(binding.root)
 | 
			
		||||
        if (savedInstanceState == null) {
 | 
			
		||||
            supportFragmentManager
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								androidApp/src/main/res/drawable/checkerboard.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								androidApp/src/main/res/drawable/checkerboard.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
<bitmap
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:dither="true"
 | 
			
		||||
    android:src="@drawable/checktile"
 | 
			
		||||
    android:tileMode="repeat"/>
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								androidApp/src/main/res/drawable/checktile.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								androidApp/src/main/res/drawable/checktile.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 235 B  | 
							
								
								
									
										7
									
								
								androidApp/src/main/res/font/source_code_pro_medium.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								androidApp/src/main/res/font/source_code_pro_medium.xml
									
									
									
									
									
										Normal 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&weight=500"
 | 
			
		||||
        app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
 | 
			
		||||
</font-family>
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent">
 | 
			
		||||
@@ -9,8 +9,8 @@
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:layout_centerVertical="true"
 | 
			
		||||
        android:layout_centerHorizontal="true"
 | 
			
		||||
        android:background="@android:color/black"
 | 
			
		||||
        android:adjustViewBounds="true"
 | 
			
		||||
        android:background="@drawable/checkerboard"
 | 
			
		||||
        app:srcCompat="@android:drawable/screen_background_dark" />
 | 
			
		||||
 | 
			
		||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
 | 
			
		||||
</RelativeLayout>
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Utiliser les paramètres système</string>
 | 
			
		||||
    <string name="mode_light">Thème clair</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								androidApp/src/main/res/values-night/strings.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								androidApp/src/main/res/values-night/strings.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<resources>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">遵循系统设置</string>
 | 
			
		||||
    <string name="mode_light">浅色模式</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -133,4 +133,5 @@
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="drawer_error_loading_sources">Error loading sources…</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
    <string-array name="ModeValues">
 | 
			
		||||
        <item>1</item> <!--MODE_NIGHT_NO-->
 | 
			
		||||
        <item>2</item> <!--MODE_NIGHT_YES-->
 | 
			
		||||
        <item>0</item> <!--MODE_NIGHT_AUTO_TIME-->
 | 
			
		||||
        <item>-1</item> <!--MODE_NIGHT_FOLLOW_SYSTEM-->
 | 
			
		||||
    </string-array>
 | 
			
		||||
 | 
			
		||||
    <string-array name="Voice">
 | 
			
		||||
 
 | 
			
		||||
@@ -3,5 +3,6 @@
 | 
			
		||||
    <array name="preloaded_fonts" translatable="false">
 | 
			
		||||
        <item>@font/open_sans</item>
 | 
			
		||||
        <item>@font/roboto</item>
 | 
			
		||||
        <item>@font/source_code_pro_medium</item>
 | 
			
		||||
    </array>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,5 +4,6 @@
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -4,5 +4,6 @@
 | 
			
		||||
        <item>Systems</item>
 | 
			
		||||
        <item>Open Sans</item>
 | 
			
		||||
        <item>Roboto</item>
 | 
			
		||||
        <item>Source Code Pro</item>
 | 
			
		||||
    </array>
 | 
			
		||||
</resources>
 | 
			
		||||
@@ -125,6 +125,7 @@
 | 
			
		||||
    <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>
 | 
			
		||||
@@ -135,4 +136,5 @@
 | 
			
		||||
    <string name="mode_dark">Dark mode</string>
 | 
			
		||||
    <string name="mode_system">Follow the system setting</string>
 | 
			
		||||
    <string name="mode_light">Light mode</string>
 | 
			
		||||
    <string name="pref_switch_enable_analytics">Enable analytics</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,12 @@
 | 
			
		||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto">
 | 
			
		||||
 | 
			
		||||
    <SwitchPreference
 | 
			
		||||
        android:defaultValue="true"
 | 
			
		||||
        app:iconSpaceReserved="false"
 | 
			
		||||
        android:key="enable_analytics"
 | 
			
		||||
        android:title="@string/pref_switch_enable_analytics" />
 | 
			
		||||
 | 
			
		||||
    <EditTextPreference
 | 
			
		||||
        android:inputType="number"
 | 
			
		||||
        android:key="api_timeout"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ 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
 | 
			
		||||
@@ -47,11 +49,11 @@ class RepositoryTest {
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
 | 
			
		||||
        coEvery { api.version() } returns SelfossModel.StatusAndData(
 | 
			
		||||
        coEvery { api.version() } returns StatusAndData(
 | 
			
		||||
            success = true,
 | 
			
		||||
            data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0")
 | 
			
		||||
        )
 | 
			
		||||
        coEvery { api.stats() } returns SelfossModel.StatusAndData(
 | 
			
		||||
        coEvery { api.stats() } returns StatusAndData(
 | 
			
		||||
            success = true,
 | 
			
		||||
            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() {
 | 
			
		||||
        every { appSettingsService.getApiVersion() } returns 1
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -98,11 +100,11 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_api_1_date_with_api_4_version_stored() {
 | 
			
		||||
        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()
 | 
			
		||||
        itemParameters.datetime = "2021-04-23 11:45:32"
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.StatusAndData(
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = generateTestApiItem(itemParameters)
 | 
			
		||||
                )
 | 
			
		||||
@@ -118,7 +120,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_newer_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        runBlocking {
 | 
			
		||||
@@ -133,7 +135,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_all_newer_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.displayedItems = ItemType.ALL
 | 
			
		||||
@@ -149,7 +151,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_newer_starred_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.displayedItems = ItemType.STARRED
 | 
			
		||||
@@ -242,7 +244,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_older_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.items = ArrayList(generateTestApiItem())
 | 
			
		||||
@@ -258,7 +260,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_all_older_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.items = ArrayList(generateTestApiItem())
 | 
			
		||||
@@ -275,7 +277,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_older_starred_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.displayedItems = ItemType.STARRED
 | 
			
		||||
@@ -299,16 +301,16 @@ class RepositoryTest {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertSame(true, success)
 | 
			
		||||
        assertSame(NUMBER_ARTICLES, repository.badgeAll)
 | 
			
		||||
        assertSame(NUMBER_UNREAD, repository.badgeUnread)
 | 
			
		||||
        assertSame(NUMBER_STARRED, repository.badgeStarred)
 | 
			
		||||
        assertEquals(NUMBER_ARTICLES, repository.badgeAll.value)
 | 
			
		||||
        assertEquals(NUMBER_UNREAD, repository.badgeUnread.value)
 | 
			
		||||
        assertEquals(NUMBER_STARRED, repository.badgeStarred.value)
 | 
			
		||||
        coVerify(atLeast = 1) { api.stats() }
 | 
			
		||||
        verify(exactly = 0) { db.itemsQueries.items().executeAsList() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
@@ -318,9 +320,9 @@ class RepositoryTest {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertSame(false, success)
 | 
			
		||||
        assertSame(0, repository.badgeAll)
 | 
			
		||||
        assertSame(0, repository.badgeUnread)
 | 
			
		||||
        assertSame(0, repository.badgeStarred)
 | 
			
		||||
        assertSame(0, repository.badgeAll.value)
 | 
			
		||||
        assertSame(0, repository.badgeUnread.value)
 | 
			
		||||
        assertSame(0, repository.badgeStarred.value)
 | 
			
		||||
        coVerify(atLeast = 1) { api.stats() }
 | 
			
		||||
        verify(exactly = 0) { db.itemsQueries.items().executeAsList() }
 | 
			
		||||
    }
 | 
			
		||||
@@ -338,9 +340,9 @@ class RepositoryTest {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertTrue(success)
 | 
			
		||||
        assertSame(1, repository.badgeAll)
 | 
			
		||||
        assertSame(1, repository.badgeUnread)
 | 
			
		||||
        assertSame(1, repository.badgeStarred)
 | 
			
		||||
        assertEquals(1, repository.badgeAll.value)
 | 
			
		||||
        assertEquals(1, repository.badgeUnread.value)
 | 
			
		||||
        assertEquals(1, repository.badgeStarred.value)
 | 
			
		||||
        coVerify(exactly = 0) { api.stats() }
 | 
			
		||||
        verify(atLeast = 1) { db.itemsQueries.items().executeAsList() }
 | 
			
		||||
    }
 | 
			
		||||
@@ -358,9 +360,9 @@ class RepositoryTest {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertFalse(success)
 | 
			
		||||
        assertSame(0, repository.badgeAll)
 | 
			
		||||
        assertSame(0, repository.badgeUnread)
 | 
			
		||||
        assertSame(0, repository.badgeStarred)
 | 
			
		||||
        assertSame(0, repository.badgeAll.value)
 | 
			
		||||
        assertSame(0, repository.badgeUnread.value)
 | 
			
		||||
        assertSame(0, repository.badgeStarred.value)
 | 
			
		||||
        coVerify(exactly = 0) { api.stats() }
 | 
			
		||||
        verify(exactly = 0) { db.itemsQueries.items().executeAsList() }
 | 
			
		||||
    }
 | 
			
		||||
@@ -376,7 +378,7 @@ class RepositoryTest {
 | 
			
		||||
            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
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
@@ -403,7 +405,7 @@ class RepositoryTest {
 | 
			
		||||
            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
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
@@ -433,7 +435,7 @@ class RepositoryTest {
 | 
			
		||||
            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
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
			
		||||
@@ -460,7 +462,7 @@ class RepositoryTest {
 | 
			
		||||
            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
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
			
		||||
@@ -489,7 +491,7 @@ class RepositoryTest {
 | 
			
		||||
            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
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
@@ -517,7 +519,7 @@ class RepositoryTest {
 | 
			
		||||
            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
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
			
		||||
@@ -544,7 +546,7 @@ class RepositoryTest {
 | 
			
		||||
            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
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
@@ -572,7 +574,7 @@ class RepositoryTest {
 | 
			
		||||
            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
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } 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
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
@@ -681,7 +683,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
        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
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
@@ -738,7 +740,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
			
		||||
        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
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
@@ -792,7 +794,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } 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
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        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
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
@@ -898,7 +900,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
			
		||||
        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
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
@@ -952,7 +954,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
        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
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
@@ -1006,7 +1008,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } 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
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
@@ -1022,7 +1024,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun create_source() {
 | 
			
		||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.SuccessResponse(true)
 | 
			
		||||
                SuccessResponse(true)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1052,7 +1054,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun create_source_but_response_fails() {
 | 
			
		||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.SuccessResponse(false)
 | 
			
		||||
                SuccessResponse(false)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1082,7 +1084,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun create_source_without_connection() {
 | 
			
		||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.SuccessResponse(true)
 | 
			
		||||
                SuccessResponse(true)
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1111,7 +1113,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun delete_source() {
 | 
			
		||||
        coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(true)
 | 
			
		||||
        coEvery { api.deleteSource(any()) } returns SuccessResponse(true)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1125,7 +1127,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun delete_source_but_response_fails() {
 | 
			
		||||
        coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false)
 | 
			
		||||
        coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1139,7 +1141,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun delete_source_without_connection() {
 | 
			
		||||
        coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false)
 | 
			
		||||
        coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1153,7 +1155,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote() {
 | 
			
		||||
        coEvery { api.update() } returns SelfossModel.StatusAndData(
 | 
			
		||||
        coEvery { api.update() } returns StatusAndData(
 | 
			
		||||
            success = true,
 | 
			
		||||
            data = "finished"
 | 
			
		||||
        )
 | 
			
		||||
@@ -1170,7 +1172,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote_but_response_fails() {
 | 
			
		||||
        coEvery { api.update() } returns SelfossModel.StatusAndData(
 | 
			
		||||
        coEvery { api.update() } returns StatusAndData(
 | 
			
		||||
            success = false,
 | 
			
		||||
            data = "unallowed access"
 | 
			
		||||
        )
 | 
			
		||||
@@ -1187,7 +1189,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote_with_unallowed_access() {
 | 
			
		||||
        coEvery { api.update() } returns SelfossModel.StatusAndData(
 | 
			
		||||
        coEvery { api.update() } returns StatusAndData(
 | 
			
		||||
            success = true,
 | 
			
		||||
            data = "unallowed access"
 | 
			
		||||
        )
 | 
			
		||||
@@ -1204,7 +1206,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote_without_connection() {
 | 
			
		||||
        coEvery { api.update() } returns SelfossModel.StatusAndData(
 | 
			
		||||
        coEvery { api.update() } returns StatusAndData(
 | 
			
		||||
            success = true,
 | 
			
		||||
            data = "undocumented..."
 | 
			
		||||
        )
 | 
			
		||||
@@ -1221,7 +1223,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun login() {
 | 
			
		||||
        coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true)
 | 
			
		||||
        coEvery { api.login() } returns SuccessResponse(success = true)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1235,7 +1237,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun login_but_response_fails() {
 | 
			
		||||
        coEvery { api.login() } returns SelfossModel.SuccessResponse(success = false)
 | 
			
		||||
        coEvery { api.login() } returns SuccessResponse(success = false)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1249,7 +1251,7 @@ class RepositoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun login_but_without_connection() {
 | 
			
		||||
        coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true)
 | 
			
		||||
        coEvery { api.login() } returns SuccessResponse(success = true)
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1297,9 +1299,9 @@ class RepositoryTest {
 | 
			
		||||
                any()
 | 
			
		||||
            )
 | 
			
		||||
        } returnsMany listOf(
 | 
			
		||||
            SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
            SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
 | 
			
		||||
            SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -1323,7 +1325,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun cache_items_but_response_fails() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                SelfossModel.StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
                StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.tagFilter = SelfossModel.Tag("Tag", "read", 0)
 | 
			
		||||
@@ -1346,7 +1348,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun cache_items_without_connection() {
 | 
			
		||||
        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))
 | 
			
		||||
        repository.tagFilter = SelfossModel.Tag("Tag", "read", 0)
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ 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")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
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
 | 
			
		||||
@@ -10,4 +11,5 @@ 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() }
 | 
			
		||||
}
 | 
			
		||||
@@ -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("&url=")) {
 | 
			
		||||
                        link.substringAfter("&url=")
 | 
			
		||||
                    } else {
 | 
			
		||||
                        this.link.replace("&", "&")
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.link.replace("&", "&")
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            // 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")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -20,12 +20,6 @@ class SelfossModel {
 | 
			
		||||
        val unread: Int
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @Serializable
 | 
			
		||||
    class SuccessResponse(val success: Boolean) {
 | 
			
		||||
        val isSuccess: Boolean
 | 
			
		||||
            get() = success
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Serializable
 | 
			
		||||
    class Stats(
 | 
			
		||||
        val total: Int,
 | 
			
		||||
@@ -152,16 +146,4 @@ 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)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ 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.*
 | 
			
		||||
@@ -10,6 +11,7 @@ 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) {
 | 
			
		||||
@@ -27,19 +29,19 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
 | 
			
		||||
 | 
			
		||||
    var offlineOverride = false
 | 
			
		||||
 | 
			
		||||
    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 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()
 | 
			
		||||
 | 
			
		||||
    private var fetchedSources = false
 | 
			
		||||
    private var fetchedTags = false
 | 
			
		||||
 | 
			
		||||
    suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
 | 
			
		||||
        // 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
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            fetchedItems = api.getItems(
 | 
			
		||||
@@ -64,7 +66,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
 | 
			
		||||
                if (sourceFilter != null) {
 | 
			
		||||
                    dbItems = dbItems.filter { it.sourcetitle == sourceFilter!!.title }
 | 
			
		||||
                }
 | 
			
		||||
                fetchedItems = SelfossModel.StatusAndData.succes(
 | 
			
		||||
                fetchedItems = StatusAndData.succes(
 | 
			
		||||
                    dbItems.map { it.toView() }
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
@@ -80,7 +82,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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()) {
 | 
			
		||||
            val offset = items.size
 | 
			
		||||
            fetchedItems = api.getItems(
 | 
			
		||||
@@ -125,17 +127,17 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            val response = api.stats()
 | 
			
		||||
            if (response.success && response.data != null) {
 | 
			
		||||
                badgeUnread = response.data.unread
 | 
			
		||||
                badgeAll = response.data.total
 | 
			
		||||
                badgeStarred = response.data.starred
 | 
			
		||||
                _badgeUnread.value = response.data.unread
 | 
			
		||||
                _badgeAll.value = response.data.total
 | 
			
		||||
                _badgeStarred.value = response.data.starred
 | 
			
		||||
                success = true
 | 
			
		||||
            }
 | 
			
		||||
        } else if (appSettingsService.isItemCachingEnabled()) {
 | 
			
		||||
            // TODO: do this differently, because it's not efficient
 | 
			
		||||
            val dbItems = getDBItems()
 | 
			
		||||
            badgeUnread = dbItems.filter { item -> item.unread }.size
 | 
			
		||||
            badgeStarred = dbItems.filter { item -> item.starred }.size
 | 
			
		||||
            badgeAll = dbItems.size
 | 
			
		||||
            _badgeUnread.value = dbItems.filter { item -> item.unread }.size
 | 
			
		||||
            _badgeStarred.value = dbItems.filter { item -> item.starred }.size
 | 
			
		||||
            _badgeAll.value = dbItems.size
 | 
			
		||||
            success = true
 | 
			
		||||
        }
 | 
			
		||||
        return success
 | 
			
		||||
@@ -283,7 +285,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
 | 
			
		||||
    private fun markAsReadLocally(item: SelfossModel.Item) {
 | 
			
		||||
        if (item.unread) {
 | 
			
		||||
            item.unread = false
 | 
			
		||||
            badgeUnread -= 1
 | 
			
		||||
            _badgeUnread.value -= 1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
@@ -294,7 +296,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
 | 
			
		||||
    private fun unmarkAsReadLocally(item: SelfossModel.Item) {
 | 
			
		||||
        if (!item.unread) {
 | 
			
		||||
            item.unread = true
 | 
			
		||||
            badgeUnread += 1
 | 
			
		||||
            _badgeUnread.value += 1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
@@ -305,7 +307,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
 | 
			
		||||
    private fun starrLocally(item: SelfossModel.Item) {
 | 
			
		||||
        if (!item.starred) {
 | 
			
		||||
            item.starred = true
 | 
			
		||||
            badgeStarred += 1
 | 
			
		||||
            _badgeStarred.value += 1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
@@ -316,7 +318,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
 | 
			
		||||
    private fun unstarrLocally(item: SelfossModel.Item) {
 | 
			
		||||
        if (item.starred) {
 | 
			
		||||
            item.starred = false
 | 
			
		||||
            badgeStarred -= 1
 | 
			
		||||
            _badgeStarred.value -= 1
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        })
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
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 io.ktor.client.*
 | 
			
		||||
import io.ktor.client.call.*
 | 
			
		||||
@@ -66,7 +66,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
        client = createHttpClient()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun login(): SelfossModel.SuccessResponse =
 | 
			
		||||
    suspend fun login(): 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
 | 
			
		||||
    ): SelfossModel.StatusAndData<List<SelfossModel.Item>> =
 | 
			
		||||
    ): 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(): SelfossModel.StatusAndData<SelfossModel.Stats> =
 | 
			
		||||
    suspend fun stats(): StatusAndData<SelfossModel.Stats> =
 | 
			
		||||
        bodyOrFailure(client.get(url("/stats")) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
    suspend fun tags(): SelfossModel.StatusAndData<List<SelfossModel.Tag>> =
 | 
			
		||||
    suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
 | 
			
		||||
        bodyOrFailure(client.get(url("/tags")) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
    suspend fun update(): SelfossModel.StatusAndData<String> =
 | 
			
		||||
    suspend fun update(): StatusAndData<String> =
 | 
			
		||||
        bodyOrFailure(client.get(url("/update")) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                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")) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                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")) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
    suspend fun version(): SelfossModel.StatusAndData<SelfossModel.ApiVersion> =
 | 
			
		||||
    suspend fun version(): StatusAndData<SelfossModel.ApiVersion> =
 | 
			
		||||
        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")) {
 | 
			
		||||
            parameter("username", appSettingsService.getUserName())
 | 
			
		||||
            parameter("password", appSettingsService.getPassword())
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse =
 | 
			
		||||
    suspend fun unmarkAsRead(id: String): SuccessResponse =
 | 
			
		||||
        maybeResponse(client.post(url("/unmark/$id")) {
 | 
			
		||||
            parameter("username", appSettingsService.getUserName())
 | 
			
		||||
            parameter("password", appSettingsService.getPassword())
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun starr(id: String): SelfossModel.SuccessResponse =
 | 
			
		||||
    suspend fun starr(id: String): SuccessResponse =
 | 
			
		||||
        maybeResponse(client.post(url("/starr/$id")) {
 | 
			
		||||
            parameter("username", appSettingsService.getUserName())
 | 
			
		||||
            parameter("password", appSettingsService.getPassword())
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun unstarr(id: String): SelfossModel.SuccessResponse =
 | 
			
		||||
    suspend fun unstarr(id: String): SuccessResponse =
 | 
			
		||||
        maybeResponse(client.post(url("/unstarr/$id")) {
 | 
			
		||||
            parameter("username", appSettingsService.getUserName())
 | 
			
		||||
            parameter("password", appSettingsService.getPassword())
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse =
 | 
			
		||||
    suspend fun markAllAsRead(ids: List<String>): 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
 | 
			
		||||
    ): SelfossModel.SuccessResponse =
 | 
			
		||||
    ): SuccessResponse =
 | 
			
		||||
        maybeResponse(
 | 
			
		||||
            if (version > 1) {
 | 
			
		||||
                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")) {
 | 
			
		||||
                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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -35,6 +35,8 @@ class AppSettingsService {
 | 
			
		||||
    private var _fontSize: Int? = null
 | 
			
		||||
    private var _staticBar: Boolean? = null
 | 
			
		||||
    private var _font: String = ""
 | 
			
		||||
    private var _theme: Int? = null
 | 
			
		||||
    private var _enableAnalytics: Boolean? = null
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
@@ -307,6 +309,17 @@ class AppSettingsService {
 | 
			
		||||
        return _staticBar == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshAnalyticsEnabled() {
 | 
			
		||||
        _enableAnalytics = settings.getBoolean(ENABLE_ANALYTICS, true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isAnalyticsEnabled(): Boolean {
 | 
			
		||||
        if (_enableAnalytics != null) {
 | 
			
		||||
            refreshAnalyticsEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _enableAnalytics == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshFont() {
 | 
			
		||||
        _font = settings.getString(READER_FONT, "")
 | 
			
		||||
    }
 | 
			
		||||
@@ -318,6 +331,17 @@ 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()
 | 
			
		||||
@@ -346,6 +370,8 @@ class AppSettingsService {
 | 
			
		||||
        refreshFontSize()
 | 
			
		||||
        refreshFont()
 | 
			
		||||
        refreshStaticBarEnabled()
 | 
			
		||||
        refreshCurrentTheme()
 | 
			
		||||
        refreshAnalyticsEnabled()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun refreshLoginInformation(
 | 
			
		||||
@@ -444,5 +470,9 @@ class AppSettingsService {
 | 
			
		||||
        const val INFINITE_LOADING = "infinite_loading"
 | 
			
		||||
 | 
			
		||||
        const val ITEMS_CACHING = "items_caching"
 | 
			
		||||
 | 
			
		||||
        const val CURRENT_THEME = "currentMode"
 | 
			
		||||
 | 
			
		||||
        const val ENABLE_ANALYTICS = "enable_analytics"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user