Compare commits
	
		
			45 Commits
		
	
	
		
			v123010301
			...
			v123051471
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7f96798f13 | ||
| 
						 | 
					6e5704a45b | ||
| 
						 | 
					495591159f | ||
| 
						 | 
					718fe7c5ee | ||
| 
						 | 
					ecd23213f9 | ||
| 
						 | 
					e6baed8cb4 | ||
| 
						 | 
					c87abec0b9 | ||
| 
						 | 
					0aba41d8bf | ||
| 
						 | 
					2a2d1047b4 | ||
| 
						 | 
					66ef1ccf32 | ||
| 
						 | 
					677ede5bc7 | ||
| 
						 | 
					996a7ed22c | ||
| 
						 | 
					85208c4e5a | ||
| 
						 | 
					5cfec50cba | ||
| 76ad71e1dc | |||
| 0277fb507c | |||
| 8d7d3174aa | |||
| 
						 | 
					00eb3333fe | ||
| 
						 | 
					629ca01d99 | ||
| 
						 | 
					c2d8681ce8 | ||
| 
						 | 
					08f79cb148 | ||
| e21906e70d | |||
| 
						 | 
					9d2cc32bc9 | ||
| 
						 | 
					d9d057c8dc | ||
| 
						 | 
					1f3fa0c4a6 | ||
| 
						 | 
					dea3def385 | ||
| 
						 | 
					f72ef2f5d4 | ||
| 
						 | 
					f28cb759df | ||
| 
						 | 
					b9d69c3e64 | ||
| 
						 | 
					c2a1c9eaac | ||
| 
						 | 
					bf37209a15 | ||
| 
						 | 
					2c558fe6fd | ||
| 
						 | 
					ad88011454 | ||
| 
						 | 
					559c17bc1d | ||
| 
						 | 
					ab9c46f0eb | ||
| 
						 | 
					aa799d2ca8 | ||
| 
						 | 
					177c978474 | ||
| 
						 | 
					39b9991413 | ||
| 
						 | 
					b303f110f1 | ||
| 
						 | 
					f851941a6a | ||
| 
						 | 
					a313552976 | ||
| 
						 | 
					6ac97ed3fe | ||
| 
						 | 
					d583b937b7 | ||
| 
						 | 
					15b9a2d935 | ||
| 
						 | 
					5a8ce15961 | 
							
								
								
									
										28
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								.drone.yml
									
									
									
									
									
								
							@@ -48,13 +48,14 @@ steps:
 | 
			
		||||
    commands:
 | 
			
		||||
      - apt-get update && apt-get install -y git
 | 
			
		||||
      - git fetch --tags -p
 | 
			
		||||
      - PREV=$(git describe --tags --abbrev=0)
 | 
			
		||||
      - ./build.sh --publish --from-ci
 | 
			
		||||
      - git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git
 | 
			
		||||
      - VER=$(git describe --tags --abbrev=0)
 | 
			
		||||
      - CHANGELOG=$(git log $VER..HEAD --pretty="- %s")
 | 
			
		||||
      - CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
 | 
			
		||||
      - echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
 | 
			
		||||
      - git add CHANGELOG.md
 | 
			
		||||
      - git commit -m "Changelog for $VER [CI SKIP]"
 | 
			
		||||
      - ./build.sh --publish --from-ci
 | 
			
		||||
      - git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git
 | 
			
		||||
      - git push pushing master
 | 
			
		||||
      - git push pushing --tags
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -136,6 +137,27 @@ steps:
 | 
			
		||||
        from_secret: giteaAPI
 | 
			
		||||
      base_url: https://gitea.amine-louveau.fr
 | 
			
		||||
      files: signed.apk
 | 
			
		||||
 | 
			
		||||
  - name: notify
 | 
			
		||||
    image: drillster/drone-email
 | 
			
		||||
    failure: ignore
 | 
			
		||||
    settings:
 | 
			
		||||
      host:
 | 
			
		||||
        from_secret: smtpHOST
 | 
			
		||||
      port:
 | 
			
		||||
        from_secret: smtpPORT
 | 
			
		||||
      username:
 | 
			
		||||
        from_secret: smtpUSERNAME
 | 
			
		||||
      password:
 | 
			
		||||
        from_secret: smtpPASSWORD
 | 
			
		||||
      from:
 | 
			
		||||
        from_secret: smtpFROM
 | 
			
		||||
      subject: Mapping file
 | 
			
		||||
      recipients:
 | 
			
		||||
        from_secret: smtpTO
 | 
			
		||||
      recipients_only: true
 | 
			
		||||
      skip_verify: true
 | 
			
		||||
      attachment: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
 | 
			
		||||
trigger:
 | 
			
		||||
  event:
 | 
			
		||||
    - tag
 | 
			
		||||
							
								
								
									
										105
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,3 +1,108 @@
 | 
			
		||||
**v123051331**
 | 
			
		||||
 | 
			
		||||
- fix: illegal input.
 | 
			
		||||
- Changelog for v123051321 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123051321**
 | 
			
		||||
 | 
			
		||||
- debug: Debug null context.
 | 
			
		||||
- Changelog for v123051301 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123051301**
 | 
			
		||||
 | 
			
		||||
- feat: Basic auth from url. Fixes #142 (#143)
 | 
			
		||||
- debug: Debug index out of bound exception.
 | 
			
		||||
- Changelog for v123051211 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123051211**
 | 
			
		||||
 | 
			
		||||
- fix: Sometimes url isn't even defined.
 | 
			
		||||
- Changelog for v123041021 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123041021**
 | 
			
		||||
 | 
			
		||||
- fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master
 | 
			
		||||
- Enable Core Library Desugaring to support older Android versions
 | 
			
		||||
- Changelog for v123030851 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123030851**
 | 
			
		||||
 | 
			
		||||
- chore: replace textDrawable library (#136)
 | 
			
		||||
- refactor: Remove slow login check. Closes #135.
 | 
			
		||||
- ci: send the mapping file after a release.
 | 
			
		||||
- Changelog for v123030751 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123030751**
 | 
			
		||||
 | 
			
		||||
- debug: added a lot to pinpoint the url issue.
 | 
			
		||||
- feat: Use /sources/stats in the home (#133)
 | 
			
		||||
- Changelog for v123030681 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123030681**
 | 
			
		||||
 | 
			
		||||
- fix: Unread and starred can be null.
 | 
			
		||||
- Fixed version number issue.
 | 
			
		||||
- Changelog for v123030621 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123030621**
 | 
			
		||||
 | 
			
		||||
- fix: url required issue.
 | 
			
		||||
- fix: Canvas reused issue.
 | 
			
		||||
- Changelog for v123020572 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123020572**
 | 
			
		||||
 | 
			
		||||
- fix: requirecontext issues ?
 | 
			
		||||
- debug: activity not found exception.
 | 
			
		||||
- Changelog for v123020571 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123020571**
 | 
			
		||||
 | 
			
		||||
- chore: remove errors logging.
 | 
			
		||||
- fix: quickfix for url param not provided for some sources.
 | 
			
		||||
- Update 'CHANGELOG.md'
 | 
			
		||||
- Changelog for v123020523 [CI SKIP]
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123020523**
 | 
			
		||||
 | 
			
		||||
- fix: Git changelog.
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123020491**
 | 
			
		||||
 | 
			
		||||
- fix: Fixed acra bug reporting.
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123010301**
 | 
			
		||||
 | 
			
		||||
- Chore: acra config.
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
**v123010281**
 | 
			
		||||
 | 
			
		||||
- improvement: Improve right to left support (#130) Co-authored-by: davidoskky <davidoskky@hidden.hidden> Co-committed-by: davidoskky <davidoskky@hidden.hidden>
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ fun gitVersion(): String {
 | 
			
		||||
    val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
 | 
			
		||||
    process = if (maybeTagOfCurrentCommit.isEmpty()) {
 | 
			
		||||
        println("No tag on current commit. Will take the latest one.")
 | 
			
		||||
        execWithOutput("git -C ../ for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1")
 | 
			
		||||
        execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
 | 
			
		||||
    } else {
 | 
			
		||||
        println("Tag found on current commit")
 | 
			
		||||
        execWithOutput("git -C ../ describe --contains HEAD")
 | 
			
		||||
@@ -56,6 +56,7 @@ fun versionNameFromGit(): String {
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
    compileOptions {
 | 
			
		||||
        isCoreLibraryDesugaringEnabled = true
 | 
			
		||||
        // Flag to enable support for the new language APIs
 | 
			
		||||
        sourceCompatibility = JavaVersion.VERSION_11
 | 
			
		||||
        targetCompatibility = JavaVersion.VERSION_11
 | 
			
		||||
@@ -112,6 +113,8 @@ android {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
 | 
			
		||||
 | 
			
		||||
    implementation(project(":shared"))
 | 
			
		||||
    implementation("com.google.android.material:material:1.5.0")
 | 
			
		||||
    implementation("androidx.appcompat:appcompat:1.4.1")
 | 
			
		||||
@@ -142,11 +145,10 @@ dependencies {
 | 
			
		||||
 | 
			
		||||
    // Material-ish things
 | 
			
		||||
    implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
 | 
			
		||||
    implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1")
 | 
			
		||||
 | 
			
		||||
    // glide
 | 
			
		||||
    kapt("com.github.bumptech.glide:compiler:4.14.2")
 | 
			
		||||
    implementation("com.github.bumptech.glide:okhttp3-integration:4.14.2")
 | 
			
		||||
    kapt("com.github.bumptech.glide:compiler:4.15.0")
 | 
			
		||||
    implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0")
 | 
			
		||||
 | 
			
		||||
    // Themes
 | 
			
		||||
    implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
 | 
			
		||||
@@ -174,7 +176,7 @@ dependencies {
 | 
			
		||||
    implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
 | 
			
		||||
 | 
			
		||||
    // Network information
 | 
			
		||||
     implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
 | 
			
		||||
    implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
 | 
			
		||||
 | 
			
		||||
    // SQLDELIGHT
 | 
			
		||||
    implementation("com.squareup.sqldelight:android-driver:1.5.4")
 | 
			
		||||
 
 | 
			
		||||
@@ -57,29 +57,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
 | 
			
		||||
        if (appSettingsService.getBaseUrl().isNotEmpty()) {
 | 
			
		||||
            showProgress(true)
 | 
			
		||||
            // This should be reverted when "old" users connected with a non-selfoss rss
 | 
			
		||||
            // are handled. Revert to "simple" way.
 | 
			
		||||
            CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
                try {
 | 
			
		||||
                    val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance()
 | 
			
		||||
                    if (!errorFetching && !displaySelfossOnly) {
 | 
			
		||||
                        goToMain()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        showProgress(false)
 | 
			
		||||
                        if (displaySelfossOnly) {
 | 
			
		||||
                            Toast.makeText(
 | 
			
		||||
                                applicationContext,
 | 
			
		||||
                                R.string.application_selfoss_only,
 | 
			
		||||
                                Toast.LENGTH_LONG
 | 
			
		||||
                            ).show()
 | 
			
		||||
                        }
 | 
			
		||||
                        repository.logout()
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (e: Throwable) {
 | 
			
		||||
                    repository.logout()
 | 
			
		||||
                    showProgress(false)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            goToMain()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        handleActions()
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ import org.kodein.di.*
 | 
			
		||||
class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
 | 
			
		||||
    override val di by DI.lazy {
 | 
			
		||||
        bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) }
 | 
			
		||||
        import(networkModule)
 | 
			
		||||
        bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
 | 
			
		||||
        bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
 | 
			
		||||
@@ -130,8 +131,8 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
            httpSender {
 | 
			
		||||
                uri =
 | 
			
		||||
                    "https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
 | 
			
		||||
                basicAuthLogin = "LMTlLZuazADohTCm"
 | 
			
		||||
                basicAuthPassword = "he6ghHp83F0PYPfh"
 | 
			
		||||
                basicAuthLogin = "qMEscjj89Gwt6cPR"
 | 
			
		||||
                basicAuthPassword = "Yo58QFlGzFaWlBzP"
 | 
			
		||||
                httpMethod = HttpSender.Method.POST
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,11 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
            finish()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        readItem(allItems[currentItem])
 | 
			
		||||
        try {
 | 
			
		||||
            readItem(allItems[currentItem])
 | 
			
		||||
        } catch (e: IndexOutOfBoundsException) {
 | 
			
		||||
            finish()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.pager.adapter = ScreenSlidePagerAdapter(this)
 | 
			
		||||
        binding.pager.setCurrentItem(currentItem, false)
 | 
			
		||||
 
 | 
			
		||||
@@ -49,13 +49,13 @@ class SourcesActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        val mLayoutManager = LinearLayoutManager(this)
 | 
			
		||||
 | 
			
		||||
        var items: ArrayList<SelfossModel.Source>
 | 
			
		||||
        var items: ArrayList<SelfossModel.SourceDetail>
 | 
			
		||||
 | 
			
		||||
        binding.recyclerView.setHasFixedSize(true)
 | 
			
		||||
        binding.recyclerView.layoutManager = mLayoutManager
 | 
			
		||||
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
            val response = repository.getSources()
 | 
			
		||||
            val response = repository.getSourcesDetails()
 | 
			
		||||
            if (response.isNotEmpty()) {
 | 
			
		||||
                items = response
 | 
			
		||||
                val mAdapter = SourcesListAdapter(
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ import org.kodein.di.instance
 | 
			
		||||
 | 
			
		||||
class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
 | 
			
		||||
    private var existingSource: SelfossModel.Source? = null
 | 
			
		||||
    private var existingSource: SelfossModel.SourceDetail? = null
 | 
			
		||||
    private var mSpoutsValue: String? = null
 | 
			
		||||
 | 
			
		||||
    private lateinit var binding: ActivityUpsertSourceBinding
 | 
			
		||||
@@ -68,7 +68,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
 | 
			
		||||
    private fun initFields(items: Map<String, SelfossModel.Spout>) {
 | 
			
		||||
        binding.nameInput.setText(existingSource!!.title)
 | 
			
		||||
        binding.tags.setText(existingSource!!.tags.joinToString(", "))
 | 
			
		||||
        binding.tags.setText(existingSource!!.tags?.joinToString(", "))
 | 
			
		||||
        binding.sourceUri.setText(existingSource!!.params?.url)
 | 
			
		||||
        binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout))
 | 
			
		||||
        binding.progress.visibility = View.GONE
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,9 @@ import android.widget.ImageView.ScaleType
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
 | 
			
		||||
@@ -22,8 +21,6 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
 | 
			
		||||
import com.amulyakhare.textdrawable.TextDrawable
 | 
			
		||||
import com.amulyakhare.textdrawable.util.ColorGenerator
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
@@ -38,7 +35,6 @@ class ItemCardAdapter(
 | 
			
		||||
    override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
 | 
			
		||||
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
    private val generator: ColorGenerator = ColorGenerator.MATERIAL
 | 
			
		||||
    private val imageMaxHeight: Int =
 | 
			
		||||
        c.resources.getDimension(R.dimen.card_image_max_height).toInt()
 | 
			
		||||
 | 
			
		||||
@@ -83,16 +79,9 @@ class ItemCardAdapter(
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (itm.getIcon(repository.baseUrl).isEmpty()) {
 | 
			
		||||
                val color = generator.getColor(itm.title.getHtmlDecoded())
 | 
			
		||||
 | 
			
		||||
                val drawable =
 | 
			
		||||
                    TextDrawable
 | 
			
		||||
                        .builder()
 | 
			
		||||
                        .round()
 | 
			
		||||
                        .build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
 | 
			
		||||
                binding.sourceImage.setImageDrawable(drawable)
 | 
			
		||||
                binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
 | 
			
		||||
            } else {
 | 
			
		||||
                c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
 | 
			
		||||
                c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,10 +7,8 @@ import android.view.ViewGroup
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
			
		||||
@@ -18,8 +16,6 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
 | 
			
		||||
import com.amulyakhare.textdrawable.TextDrawable
 | 
			
		||||
import com.amulyakhare.textdrawable.util.ColorGenerator
 | 
			
		||||
import org.kodein.di.DI
 | 
			
		||||
import org.kodein.di.android.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
@@ -29,7 +25,6 @@ class ItemListAdapter(
 | 
			
		||||
    override var items: ArrayList<SelfossModel.Item>,
 | 
			
		||||
    override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
 | 
			
		||||
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
 | 
			
		||||
    private val generator: ColorGenerator = ColorGenerator.MATERIAL
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
 | 
			
		||||
    override val di: DI by closestDI(app)
 | 
			
		||||
@@ -56,20 +51,12 @@ class ItemListAdapter(
 | 
			
		||||
            if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
 | 
			
		||||
 | 
			
		||||
                if (itm.getIcon(repository.baseUrl).isEmpty()) {
 | 
			
		||||
                    val color = generator.getColor(itm.title.getHtmlDecoded())
 | 
			
		||||
 | 
			
		||||
                    val drawable =
 | 
			
		||||
                            TextDrawable
 | 
			
		||||
                                    .builder()
 | 
			
		||||
                                    .round()
 | 
			
		||||
                                    .build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
 | 
			
		||||
 | 
			
		||||
                    binding.itemImage.setImageDrawable(drawable)
 | 
			
		||||
                    binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
 | 
			
		||||
                } else {
 | 
			
		||||
                    c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
 | 
			
		||||
                    c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
 | 
			
		||||
                c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,17 +10,14 @@ import android.widget.Button
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.constraintlayout.widget.ConstraintLayout
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
 | 
			
		||||
import com.amulyakhare.textdrawable.TextDrawable
 | 
			
		||||
import com.amulyakhare.textdrawable.util.ColorGenerator
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
@@ -31,10 +28,9 @@ import org.kodein.di.instance
 | 
			
		||||
 | 
			
		||||
class SourcesListAdapter(
 | 
			
		||||
    private val app: Activity,
 | 
			
		||||
    private val items: ArrayList<SelfossModel.Source>
 | 
			
		||||
    private val items: ArrayList<SelfossModel.SourceDetail>
 | 
			
		||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
    private val generator: ColorGenerator = ColorGenerator.MATERIAL
 | 
			
		||||
    private lateinit var binding: SourceListItemBinding
 | 
			
		||||
 | 
			
		||||
    override val di: DI by closestDI(app)
 | 
			
		||||
@@ -49,19 +45,12 @@ class SourcesListAdapter(
 | 
			
		||||
        val itm = items[position]
 | 
			
		||||
 | 
			
		||||
        if (itm.getIcon(repository.baseUrl).isEmpty()) {
 | 
			
		||||
            val color = generator.getColor(itm.title.getHtmlDecoded())
 | 
			
		||||
 | 
			
		||||
            val drawable =
 | 
			
		||||
                TextDrawable
 | 
			
		||||
                    .builder()
 | 
			
		||||
                    .round()
 | 
			
		||||
                    .build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
 | 
			
		||||
            binding.itemImage.setImageDrawable(drawable)
 | 
			
		||||
            binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
 | 
			
		||||
        } else {
 | 
			
		||||
            c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
 | 
			
		||||
            c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (itm.error.isNotBlank()) {
 | 
			
		||||
        if (!itm.error.isNullOrBlank()) {
 | 
			
		||||
            binding.errorText.visibility = View.VISIBLE
 | 
			
		||||
            binding.errorText.text = itm.error
 | 
			
		||||
        } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android.fragments
 | 
			
		||||
 | 
			
		||||
import android.content.ActivityNotFoundException
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.res.ColorStateList
 | 
			
		||||
import android.content.res.TypedArray
 | 
			
		||||
@@ -30,7 +31,6 @@ import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.MercuryModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.StatusAndData
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
@@ -150,17 +150,19 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
 | 
			
		||||
        } catch (e: InflateException) {
 | 
			
		||||
            e.sendSilentlyWithAcraWithName("webview not available")
 | 
			
		||||
            AlertDialog.Builder(requireContext())
 | 
			
		||||
                .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
 | 
			
		||||
                .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
 | 
			
		||||
                .setPositiveButton(
 | 
			
		||||
                    android.R.string.ok
 | 
			
		||||
                ) { _, _ ->
 | 
			
		||||
                    appSettingsService.disableArticleViewer()
 | 
			
		||||
                    requireActivity().finish()
 | 
			
		||||
                }
 | 
			
		||||
                .create()
 | 
			
		||||
                .show()
 | 
			
		||||
            if (context != null) {
 | 
			
		||||
                AlertDialog.Builder(requireContext())
 | 
			
		||||
                    .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
 | 
			
		||||
                    .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
 | 
			
		||||
                    .setPositiveButton(
 | 
			
		||||
                        android.R.string.ok
 | 
			
		||||
                    ) { _, _ ->
 | 
			
		||||
                        appSettingsService.disableArticleViewer()
 | 
			
		||||
                        requireActivity().finish()
 | 
			
		||||
                    }
 | 
			
		||||
                    .create()
 | 
			
		||||
                    .show()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return binding.root
 | 
			
		||||
@@ -257,21 +259,8 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
            try {
 | 
			
		||||
                val response = mercuryApi.query(url)
 | 
			
		||||
                if (response.success && response.data != null && !response.data?.content.isNullOrEmpty()) {
 | 
			
		||||
                    binding.titleView.text = response.data!!.title.orEmpty()
 | 
			
		||||
                    if (typeface != null) {
 | 
			
		||||
                        binding.titleView.typeface = typeface
 | 
			
		||||
                    }
 | 
			
		||||
                    URL(response.data!!.url)
 | 
			
		||||
                    url = response.data!!.url
 | 
			
		||||
 | 
			
		||||
                    contentText = response.data!!.content.orEmpty()
 | 
			
		||||
                    htmlToWebview()
 | 
			
		||||
 | 
			
		||||
                    handleLeadImage(response)
 | 
			
		||||
 | 
			
		||||
                    binding.nestedScrollView.scrollTo(0, 0)
 | 
			
		||||
                    binding.progressBar.visibility = View.GONE
 | 
			
		||||
                if (response.success && response.data != null) {
 | 
			
		||||
                    handleMercuryData(response.data!!)
 | 
			
		||||
                } else {
 | 
			
		||||
                    openInBrowserAfterFailing()
 | 
			
		||||
                }
 | 
			
		||||
@@ -284,14 +273,35 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleLeadImage(response: StatusAndData<MercuryModel.ParsedContent>) {
 | 
			
		||||
        if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) {
 | 
			
		||||
    private fun handleMercuryData(data: MercuryModel.ParsedContent) {
 | 
			
		||||
        if (data.error == true || data.failed == true) {
 | 
			
		||||
            openInBrowserAfterFailing()
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.titleView.text = data.title.orEmpty()
 | 
			
		||||
            if (typeface != null) {
 | 
			
		||||
                binding.titleView.typeface = typeface
 | 
			
		||||
            }
 | 
			
		||||
            URL(data.url)
 | 
			
		||||
            url = data.url!!
 | 
			
		||||
 | 
			
		||||
            contentText = data.content.orEmpty()
 | 
			
		||||
            htmlToWebview()
 | 
			
		||||
 | 
			
		||||
            handleLeadImage(data?.lead_image_url)
 | 
			
		||||
 | 
			
		||||
            binding.nestedScrollView.scrollTo(0, 0)
 | 
			
		||||
            binding.progressBar.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleLeadImage(lead_image_url: String?) {
 | 
			
		||||
        if (!lead_image_url.isNullOrEmpty() && context != null) {
 | 
			
		||||
            binding.imageView.visibility = View.VISIBLE
 | 
			
		||||
            Glide
 | 
			
		||||
                .with(requireContext())
 | 
			
		||||
                .asBitmap()
 | 
			
		||||
                .load(
 | 
			
		||||
                    response.data!!.lead_image_url.orEmpty()
 | 
			
		||||
                    lead_image_url
 | 
			
		||||
                )
 | 
			
		||||
                .apply(RequestOptions.fitCenterTransform())
 | 
			
		||||
                .into(binding.imageView)
 | 
			
		||||
@@ -304,10 +314,16 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
        binding.webcontent.webViewClient = object : WebViewClient() {
 | 
			
		||||
            @Deprecated("Deprecated in Java")
 | 
			
		||||
            override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
 | 
			
		||||
                if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
 | 
			
		||||
                    requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
 | 
			
		||||
                return if (context != null && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
 | 
			
		||||
                    try {
 | 
			
		||||
                        requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
 | 
			
		||||
                    } catch (e: ActivityNotFoundException) {
 | 
			
		||||
                        e.sendSilentlyWithAcraWithName("activityNotFound > $url")
 | 
			
		||||
                    }
 | 
			
		||||
                    true
 | 
			
		||||
                } else {
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Deprecated("Deprecated in Java")
 | 
			
		||||
@@ -325,7 +341,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                            getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)
 | 
			
		||||
                        )
 | 
			
		||||
                    } catch (e: ExecutionException) {
 | 
			
		||||
                        e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url")
 | 
			
		||||
                        // Do nothing
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (url.lowercase(Locale.US).contains(".png")) {
 | 
			
		||||
                    try {
 | 
			
		||||
@@ -337,7 +353,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                            getBitmapInputStream(image, Bitmap.CompressFormat.PNG)
 | 
			
		||||
                        )
 | 
			
		||||
                    } catch (e: ExecutionException) {
 | 
			
		||||
                        e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png > $url")
 | 
			
		||||
                        // Do nothing
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (url.lowercase(Locale.US).contains(".webp")) {
 | 
			
		||||
                    try {
 | 
			
		||||
@@ -349,7 +365,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                            getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)
 | 
			
		||||
                        )
 | 
			
		||||
                    } catch (e: ExecutionException) {
 | 
			
		||||
                        e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp > $url")
 | 
			
		||||
                        // Do nothing
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -359,74 +375,74 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun htmlToWebview() {
 | 
			
		||||
 | 
			
		||||
        val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
 | 
			
		||||
        val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
 | 
			
		||||
        if (context != null) {
 | 
			
		||||
            val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
 | 
			
		||||
            val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        binding.webcontent.settings.standardFontFamily = a.getString(0)
 | 
			
		||||
        binding.webcontent.visibility = View.VISIBLE
 | 
			
		||||
            binding.webcontent.settings.standardFontFamily = a.getString(0)
 | 
			
		||||
            binding.webcontent.visibility = View.VISIBLE
 | 
			
		||||
 | 
			
		||||
        val colorOnSurface = TypedValue()
 | 
			
		||||
        requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
 | 
			
		||||
            val colorOnSurface = TypedValue()
 | 
			
		||||
            requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
 | 
			
		||||
 | 
			
		||||
        val colorSurface = TypedValue()
 | 
			
		||||
        requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
 | 
			
		||||
            val colorSurface = TypedValue()
 | 
			
		||||
            requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
 | 
			
		||||
 | 
			
		||||
        binding.webcontent.settings.useWideViewPort = true
 | 
			
		||||
        binding.webcontent.settings.loadWithOverviewMode = true
 | 
			
		||||
        binding.webcontent.settings.javaScriptEnabled = false
 | 
			
		||||
            binding.webcontent.settings.useWideViewPort = true
 | 
			
		||||
            binding.webcontent.settings.loadWithOverviewMode = true
 | 
			
		||||
            binding.webcontent.settings.javaScriptEnabled = false
 | 
			
		||||
 | 
			
		||||
        handleImageLoading()
 | 
			
		||||
            handleImageLoading()
 | 
			
		||||
 | 
			
		||||
        val gestureDetector =
 | 
			
		||||
            GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
 | 
			
		||||
                override fun onSingleTapUp(e: MotionEvent): Boolean {
 | 
			
		||||
                    return performClick()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            val gestureDetector =
 | 
			
		||||
                GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
 | 
			
		||||
                    override fun onSingleTapUp(e: MotionEvent): Boolean {
 | 
			
		||||
                        return performClick()
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
 | 
			
		||||
            binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
 | 
			
		||||
 | 
			
		||||
        binding.webcontent.settings.layoutAlgorithm =
 | 
			
		||||
            WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
 | 
			
		||||
            binding.webcontent.settings.layoutAlgorithm =
 | 
			
		||||
                WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
 | 
			
		||||
 | 
			
		||||
        var baseUrl: String? = null
 | 
			
		||||
            var baseUrl: String? = null
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val itemUrl = URL(url)
 | 
			
		||||
            baseUrl = itemUrl.protocol + "://" + itemUrl.host
 | 
			
		||||
        } catch (e: MalformedURLException) {
 | 
			
		||||
            e.sendSilentlyWithAcraWithName("htmlToWebview > item url")
 | 
			
		||||
        }
 | 
			
		||||
            try {
 | 
			
		||||
                val itemUrl = URL(url)
 | 
			
		||||
                baseUrl = itemUrl.protocol + "://" + itemUrl.host
 | 
			
		||||
            } catch (e: MalformedURLException) {
 | 
			
		||||
                e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        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 -> ""
 | 
			
		||||
        }
 | 
			
		||||
            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 -> ""
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        val fontLinkAndStyle = if (font.isNotEmpty()) {
 | 
			
		||||
            """<link href="https://fonts.googleapis.com/css?family=${
 | 
			
		||||
                fontName.replace(
 | 
			
		||||
                    " ",
 | 
			
		||||
                    "+"
 | 
			
		||||
                )
 | 
			
		||||
            }" rel="stylesheet">
 | 
			
		||||
            val fontLinkAndStyle = if (font.isNotEmpty()) {
 | 
			
		||||
                """<link href="https://fonts.googleapis.com/css?family=${
 | 
			
		||||
                    fontName.replace(
 | 
			
		||||
                        " ",
 | 
			
		||||
                        "+"
 | 
			
		||||
                    )
 | 
			
		||||
                }" rel="stylesheet">
 | 
			
		||||
                |<style>
 | 
			
		||||
                |   * {
 | 
			
		||||
                |       font-family: '$fontName';
 | 
			
		||||
                |   }
 | 
			
		||||
                |</style>
 | 
			
		||||
            """.trimMargin()
 | 
			
		||||
        } else {
 | 
			
		||||
            ""
 | 
			
		||||
        }
 | 
			
		||||
            } else {
 | 
			
		||||
                ""
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        binding.webcontent.loadDataWithBaseURL(
 | 
			
		||||
            baseUrl,
 | 
			
		||||
            """<html>
 | 
			
		||||
            binding.webcontent.loadDataWithBaseURL(
 | 
			
		||||
                baseUrl,
 | 
			
		||||
                """<html>
 | 
			
		||||
                |<head>
 | 
			
		||||
                |   <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
                |   <style>
 | 
			
		||||
@@ -438,11 +454,11 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                |      }
 | 
			
		||||
                |      a {
 | 
			
		||||
                |        color: ${
 | 
			
		||||
                String.format(
 | 
			
		||||
                    "#%06X",
 | 
			
		||||
                    0xFFFFFF and resources.getColor(R.color.colorAccent)
 | 
			
		||||
                )
 | 
			
		||||
            } !important;
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and resources.getColor(R.color.colorAccent)
 | 
			
		||||
                    )
 | 
			
		||||
                } !important;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      *:not(a) {
 | 
			
		||||
                |        color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)};
 | 
			
		||||
@@ -454,25 +470,25 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                |        overflow:hidden;
 | 
			
		||||
                |        line-height: 1.5em;
 | 
			
		||||
                |        background-color: ${
 | 
			
		||||
                String.format(
 | 
			
		||||
                    "#%06X",
 | 
			
		||||
                    0xFFFFFF and colorSurface.data
 | 
			
		||||
                )
 | 
			
		||||
            };
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and colorSurface.data
 | 
			
		||||
                    )
 | 
			
		||||
                };
 | 
			
		||||
                |      }
 | 
			
		||||
                |      body, html {
 | 
			
		||||
                |        background-color: ${
 | 
			
		||||
                String.format(
 | 
			
		||||
                    "#%06X",
 | 
			
		||||
                    0xFFFFFF and colorSurface.data
 | 
			
		||||
                )
 | 
			
		||||
            } !important;
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and colorSurface.data
 | 
			
		||||
                    )
 | 
			
		||||
                } !important;
 | 
			
		||||
                |        border-color: ${
 | 
			
		||||
                String.format(
 | 
			
		||||
                    "#%06X",
 | 
			
		||||
                    0xFFFFFF and colorSurface.data
 | 
			
		||||
                )
 | 
			
		||||
            }  !important;
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and colorSurface.data
 | 
			
		||||
                    )
 | 
			
		||||
                }  !important;
 | 
			
		||||
                |        padding: 0 !important;
 | 
			
		||||
                |        margin: 0 !important;
 | 
			
		||||
                |      }
 | 
			
		||||
@@ -483,11 +499,11 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                |        white-space: pre-wrap;
 | 
			
		||||
                |        width:100%;
 | 
			
		||||
                |        background-color: ${
 | 
			
		||||
                String.format(
 | 
			
		||||
                    "#%06X",
 | 
			
		||||
                    0xFFFFFF and colorSurface.data
 | 
			
		||||
                )
 | 
			
		||||
            };
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and colorSurface.data
 | 
			
		||||
                    )
 | 
			
		||||
                };
 | 
			
		||||
                |      }
 | 
			
		||||
                |   </style>
 | 
			
		||||
                |   $fontLinkAndStyle
 | 
			
		||||
@@ -495,10 +511,11 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                |<body>
 | 
			
		||||
                |   $contentText
 | 
			
		||||
                |</body>""".trimMargin(),
 | 
			
		||||
            "text/html",
 | 
			
		||||
            "utf-8",
 | 
			
		||||
            null
 | 
			
		||||
        )
 | 
			
		||||
                "text/html",
 | 
			
		||||
                "utf-8",
 | 
			
		||||
                null
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun scrollDown() {
 | 
			
		||||
@@ -513,7 +530,12 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
 | 
			
		||||
    private fun openInBrowserAfterFailing() {
 | 
			
		||||
        binding.progressBar.visibility = View.GONE
 | 
			
		||||
        requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
 | 
			
		||||
        if (context != null) {
 | 
			
		||||
            requireContext().openInBrowserAsNewTask(this@ArticleFragment.item)
 | 
			
		||||
        } else {
 | 
			
		||||
            Exception("openInBrowserAfterFailing context is null").sendSilentlyWithAcraWithName("openInBrowserAfterFailing > $context")
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
@@ -531,8 +553,8 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun performClick(): Boolean {
 | 
			
		||||
        if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
 | 
			
		||||
            binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
 | 
			
		||||
        if (allImages != null && (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
 | 
			
		||||
            binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE)
 | 
			
		||||
        ) {
 | 
			
		||||
 | 
			
		||||
            val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
 | 
			
		||||
 
 | 
			
		||||
@@ -20,10 +20,8 @@ import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.load.DataSource
 | 
			
		||||
import com.bumptech.glide.load.engine.GlideException
 | 
			
		||||
import com.bumptech.glide.request.RequestListener
 | 
			
		||||
import com.bumptech.glide.request.target.Target
 | 
			
		||||
import com.bumptech.glide.request.target.ViewTarget
 | 
			
		||||
import com.bumptech.glide.request.transition.Transition
 | 
			
		||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 | 
			
		||||
import com.google.android.material.chip.Chip
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
@@ -84,37 +82,25 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
			
		||||
    ) {
 | 
			
		||||
        val sourceGroup = binding.sourcesGroup
 | 
			
		||||
 | 
			
		||||
        repository.getSources().forEach { source ->
 | 
			
		||||
        repository.getSourcesDetailsOrStats().forEach { source ->
 | 
			
		||||
            val c = Chip(context)
 | 
			
		||||
            c.ellipsize = TextUtils.TruncateAt.END
 | 
			
		||||
 | 
			
		||||
            Glide.with(context)
 | 
			
		||||
                .load(source.getIcon(repository.baseUrl))
 | 
			
		||||
                .listener(object : RequestListener<Drawable?> {
 | 
			
		||||
                    override fun onLoadFailed(
 | 
			
		||||
                        e: GlideException?,
 | 
			
		||||
                        model: Any?,
 | 
			
		||||
                        target: Target<Drawable?>?,
 | 
			
		||||
                        isFirstResource: Boolean
 | 
			
		||||
                    ): Boolean {
 | 
			
		||||
                        return false
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                .into(object : ViewTarget<Chip?, Drawable?>(c) {
 | 
			
		||||
                    override fun onResourceReady(
 | 
			
		||||
                        resource: Drawable?,
 | 
			
		||||
                        model: Any?,
 | 
			
		||||
                        target: Target<Drawable?>?,
 | 
			
		||||
                        dataSource: DataSource?,
 | 
			
		||||
                        isFirstResource: Boolean
 | 
			
		||||
                    ): Boolean {
 | 
			
		||||
                        resource: Drawable,
 | 
			
		||||
                        transition: Transition<in Drawable?>?
 | 
			
		||||
                    ) {
 | 
			
		||||
                        try {
 | 
			
		||||
                            c.chipIcon = resource
 | 
			
		||||
                        } catch (e: Exception) {
 | 
			
		||||
                            e.sendSilentlyWithAcraWithName("sources > onResourceReady")
 | 
			
		||||
                        }
 | 
			
		||||
                        return false
 | 
			
		||||
                    }
 | 
			
		||||
                }).preload()
 | 
			
		||||
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
            c.text = source.title.getHtmlDecoded()
 | 
			
		||||
 | 
			
		||||
@@ -141,9 +127,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
			
		||||
                selectedChip = c
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            c.isEnabled = source.error.isBlank()
 | 
			
		||||
            c.isEnabled = source.error.isNullOrBlank()
 | 
			
		||||
 | 
			
		||||
            if (source.error.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 | 
			
		||||
            if (!source.error.isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 | 
			
		||||
                c.tooltipText = source.error
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -163,21 +149,23 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
			
		||||
            c.ellipsize = TextUtils.TruncateAt.END
 | 
			
		||||
            c.text = tag.tag
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                val gd = GradientDrawable()
 | 
			
		||||
                val gdColor = try {
 | 
			
		||||
                    Color.parseColor(tag.color)
 | 
			
		||||
                } catch (e: IllegalArgumentException) {
 | 
			
		||||
                    e.sendSilentlyWithAcraWithName("color issue " + tag.color)
 | 
			
		||||
                    resources.getColor(R.color.colorPrimary)
 | 
			
		||||
            if (tag.color.isNotEmpty()) {
 | 
			
		||||
                try {
 | 
			
		||||
                    val gd = GradientDrawable()
 | 
			
		||||
                    val gdColor = try {
 | 
			
		||||
                        Color.parseColor(tag.color)
 | 
			
		||||
                    } catch (e: IllegalArgumentException) {
 | 
			
		||||
                        e.sendSilentlyWithAcraWithName("color issue " + tag.color)
 | 
			
		||||
                        resources.getColor(R.color.colorPrimary)
 | 
			
		||||
                    }
 | 
			
		||||
                    gd.setColor(gdColor)
 | 
			
		||||
                    gd.shape = GradientDrawable.RECTANGLE
 | 
			
		||||
                    gd.setSize(30, 30)
 | 
			
		||||
                    gd.cornerRadius = 30F
 | 
			
		||||
                    c.chipIcon = gd
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
 | 
			
		||||
                }
 | 
			
		||||
                gd.setColor(gdColor)
 | 
			
		||||
                gd.shape = GradientDrawable.RECTANGLE
 | 
			
		||||
                gd.setSize(30, 30)
 | 
			
		||||
                gd.cornerRadius = 30F
 | 
			
		||||
                c.chipIcon = gd
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            c.setOnCloseIconClickListener {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,62 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android.utils
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.drawable.GradientDrawable
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.RelativeLayout
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
 | 
			
		||||
import com.google.android.material.imageview.ShapeableImageView
 | 
			
		||||
import kotlin.math.abs
 | 
			
		||||
 | 
			
		||||
class CircleImageView @JvmOverloads constructor(
 | 
			
		||||
    context: Context,
 | 
			
		||||
    attrs: AttributeSet? = null,
 | 
			
		||||
    defStyleAttr: Int = 0
 | 
			
		||||
) : RelativeLayout(context, attrs, defStyleAttr) {
 | 
			
		||||
    val view: View
 | 
			
		||||
    val imageView: ShapeableImageView
 | 
			
		||||
    val textView: TextView
 | 
			
		||||
 | 
			
		||||
    private val colorScheme = listOf(
 | 
			
		||||
    -0x1a8c8d,
 | 
			
		||||
    -0xf9d6e,
 | 
			
		||||
    -0x459738,
 | 
			
		||||
    -0x6a8a33,
 | 
			
		||||
    -0x867935,
 | 
			
		||||
    -0x9b4a0a,
 | 
			
		||||
    -0xb03c09,
 | 
			
		||||
    -0xb22f1f,
 | 
			
		||||
    -0xb24954,
 | 
			
		||||
    -0x7e387c,
 | 
			
		||||
    -0x512a7f,
 | 
			
		||||
    -0x759b,
 | 
			
		||||
    -0x2b1ea9,
 | 
			
		||||
    -0x2ab1,
 | 
			
		||||
    -0x48b3,
 | 
			
		||||
    -0x5e7781,
 | 
			
		||||
    -0x6f5b52
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        view = LayoutInflater.from(context).inflate(R.layout.circle_image_view, this, true)
 | 
			
		||||
        imageView = view.findViewById(R.id.circleImage)
 | 
			
		||||
        textView = view.findViewById(R.id.circleText)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setBackgroundAndText(text: String) {
 | 
			
		||||
        val circleDrawable = GradientDrawable()
 | 
			
		||||
        val color = colorFromIdentifier(text)
 | 
			
		||||
        circleDrawable.setColor(color)
 | 
			
		||||
        imageView.setImageDrawable(circleDrawable)
 | 
			
		||||
 | 
			
		||||
        textView.text = text.toTextDrawableString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun colorFromIdentifier(key: String): Int {
 | 
			
		||||
        return colorScheme[abs(key.hashCode()) % colorScheme.size]
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -3,10 +3,9 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.request.RequestOptions
 | 
			
		||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
 | 
			
		||||
import java.io.ByteArrayInputStream
 | 
			
		||||
import java.io.ByteArrayOutputStream
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
@@ -18,21 +17,13 @@ fun Context.bitmapCenterCrop(url: String, iv: ImageView) =
 | 
			
		||||
        .apply(RequestOptions.centerCropTransform())
 | 
			
		||||
        .into(iv)
 | 
			
		||||
 | 
			
		||||
fun Context.circularBitmapDrawable(url: String, iv: ImageView) =
 | 
			
		||||
fun Context.circularDrawable(url: String, view: CircleImageView) {
 | 
			
		||||
    view.textView.text =""
 | 
			
		||||
 | 
			
		||||
    Glide.with(this)
 | 
			
		||||
        .asBitmap()
 | 
			
		||||
        .load(url)
 | 
			
		||||
        .apply(RequestOptions.centerCropTransform())
 | 
			
		||||
        .into(object : BitmapImageViewTarget(iv) {
 | 
			
		||||
            override fun setResource(resource: Bitmap?) {
 | 
			
		||||
                val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(
 | 
			
		||||
                    resources,
 | 
			
		||||
                    resource
 | 
			
		||||
                )
 | 
			
		||||
                circularBitmapDrawable.isCircular = true
 | 
			
		||||
                iv.setImageDrawable(circularBitmapDrawable)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        .into(view.imageView)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream {
 | 
			
		||||
    val byteArrayOutputStream = ByteArrayOutputStream()
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@
 | 
			
		||||
            app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
            app:layout_constraintTop_toBottomOf="@+id/itemImage">
 | 
			
		||||
 | 
			
		||||
            <ImageView
 | 
			
		||||
            <bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
 | 
			
		||||
                android:id="@+id/sourceImage"
 | 
			
		||||
                android:layout_width="40dp"
 | 
			
		||||
                android:layout_height="40dp"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								androidApp/src/main/res/layout/circle_image_view.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								androidApp/src/main/res/layout/circle_image_view.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    android:layout_width="wrap_content"
 | 
			
		||||
    android:layout_height="wrap_content">
 | 
			
		||||
 | 
			
		||||
    <com.google.android.material.imageview.ShapeableImageView
 | 
			
		||||
        android:id="@+id/circleImage"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:scaleType="centerCrop"
 | 
			
		||||
        app:shapeAppearanceOverlay="@style/circleImageView"
 | 
			
		||||
        app:srcCompat="@drawable/background_splash" />
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
        android:id="@+id/circleText"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:ellipsize="none"
 | 
			
		||||
        android:gravity="center"
 | 
			
		||||
        android:singleLine="true"
 | 
			
		||||
        android:textColor="@color/white"
 | 
			
		||||
        android:textIsSelectable="false"
 | 
			
		||||
        android:textSize="20sp"
 | 
			
		||||
        android:typeface="normal" />
 | 
			
		||||
</RelativeLayout>
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content">
 | 
			
		||||
 | 
			
		||||
    <ImageView
 | 
			
		||||
    <bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
 | 
			
		||||
        android:id="@+id/itemImage"
 | 
			
		||||
        android:layout_width="46dp"
 | 
			
		||||
        android:layout_height="46dp"
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent"
 | 
			
		||||
        app:layout_constraintVertical_bias="0.0" />
 | 
			
		||||
 | 
			
		||||
    <ImageView
 | 
			
		||||
    <bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
 | 
			
		||||
        android:id="@+id/itemImage"
 | 
			
		||||
        android:layout_width="36dp"
 | 
			
		||||
        android:layout_height="36dp"
 | 
			
		||||
 
 | 
			
		||||
@@ -32,4 +32,10 @@
 | 
			
		||||
        <item name="android:colorBackgroundCacheHint">@null</item>
 | 
			
		||||
        <item name="android:windowIsTranslucent">true</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="circleImageView" parent="">
 | 
			
		||||
        <item name="cornerFamily">rounded</item>
 | 
			
		||||
        <item name="cornerSize">50%</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -300,9 +300,10 @@ class RepositoryTest {
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        repository.setSourceFilter(SelfossModel.Source(
 | 
			
		||||
        repository.setSourceFilter(SelfossModel.SourceDetail(
 | 
			
		||||
            1,
 | 
			
		||||
            "Test",
 | 
			
		||||
            null,
 | 
			
		||||
            listOf("tags"),
 | 
			
		||||
            SPOUT,
 | 
			
		||||
            "",
 | 
			
		||||
@@ -609,30 +610,32 @@ class RepositoryTest {
 | 
			
		||||
    fun get_sources() {
 | 
			
		||||
        val (sources, sourcesDB) = prepareSources()
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
        var testSources: List<SelfossModel.Source>
 | 
			
		||||
        runBlocking {
 | 
			
		||||
            testSources = repository.getSources()
 | 
			
		||||
            testSources = repository.getSourcesDetails()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertSame(sources, testSources)
 | 
			
		||||
        assertEquals(sources, testSources)
 | 
			
		||||
        assertNotEquals(sourcesDB.map { it.toView() }, testSources)
 | 
			
		||||
        coVerify(exactly = 1) { api.sources() }
 | 
			
		||||
        coVerify(exactly = 1) { api.sourcesDetailed() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun prepareSources(): Pair<ArrayList<SelfossModel.Source>, List<SOURCE>> {
 | 
			
		||||
    private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> {
 | 
			
		||||
        val sources = arrayListOf(
 | 
			
		||||
            SelfossModel.Source(
 | 
			
		||||
            SelfossModel.SourceDetail(
 | 
			
		||||
                1,
 | 
			
		||||
                "First source",
 | 
			
		||||
                null,
 | 
			
		||||
                listOf("Test", "second"),
 | 
			
		||||
                SPOUT,
 | 
			
		||||
                "",
 | 
			
		||||
                IMAGE_URL_2,
 | 
			
		||||
                SelfossModel.SourceParams("url")
 | 
			
		||||
            ),
 | 
			
		||||
            SelfossModel.Source(
 | 
			
		||||
            SelfossModel.SourceDetail(
 | 
			
		||||
                2,
 | 
			
		||||
                "Second source",
 | 
			
		||||
                null,
 | 
			
		||||
                listOf("second"),
 | 
			
		||||
                SPOUT,
 | 
			
		||||
                "",
 | 
			
		||||
@@ -661,7 +664,7 @@ class RepositoryTest {
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
 | 
			
		||||
        coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources)
 | 
			
		||||
        every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
 | 
			
		||||
        return Pair(sources, sourcesDB)
 | 
			
		||||
    }
 | 
			
		||||
@@ -675,13 +678,13 @@ class RepositoryTest {
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
        runBlocking {
 | 
			
		||||
            testSources = repository.getSources()
 | 
			
		||||
            testSources = repository.getSourcesDetails()
 | 
			
		||||
            // Sources will be fetched from the database on the second call, thus testSources != sources
 | 
			
		||||
            testSources = repository.getSources()
 | 
			
		||||
            testSources = repository.getSourcesDetails()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        coVerify(exactly = 1) { api.sources() }
 | 
			
		||||
        assertNotSame(sources, testSources)
 | 
			
		||||
        coVerify(exactly = 1) { api.sourcesDetailed() }
 | 
			
		||||
        assertNotEquals(sources, testSources)
 | 
			
		||||
        assertEquals(sourcesDB.map { it.toView() }, testSources)
 | 
			
		||||
        verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
 | 
			
		||||
    }
 | 
			
		||||
@@ -693,13 +696,13 @@ class RepositoryTest {
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
        var testSources: List<SelfossModel.Source>
 | 
			
		||||
        runBlocking {
 | 
			
		||||
            testSources = repository.getSources()
 | 
			
		||||
            testSources = repository.getSourcesDetails()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertSame(sources, testSources)
 | 
			
		||||
        coVerify(exactly = 1) { api.sources() }
 | 
			
		||||
        assertEquals(sources, testSources)
 | 
			
		||||
        coVerify(exactly = 1) { api.sourcesDetailed() }
 | 
			
		||||
        verify(exactly = 0) { db.sourcesQueries }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -710,13 +713,13 @@ class RepositoryTest {
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
        var testSources: List<SelfossModel.Source>
 | 
			
		||||
        runBlocking {
 | 
			
		||||
            testSources = repository.getSources()
 | 
			
		||||
            testSources = repository.getSourcesDetails()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertSame(sources, testSources)
 | 
			
		||||
        coVerify(exactly = 1) { api.sources() }
 | 
			
		||||
        assertEquals(sources, testSources)
 | 
			
		||||
        coVerify(exactly = 1) { api.sourcesDetailed() }
 | 
			
		||||
        verify(atLeast = 1) { db.sourcesQueries }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -724,13 +727,13 @@ class RepositoryTest {
 | 
			
		||||
    fun get_sources_without_connection() {
 | 
			
		||||
        val (_, sourcesDB) = prepareSources()
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
        var testSources: List<SelfossModel.Source>
 | 
			
		||||
        runBlocking {
 | 
			
		||||
            testSources = repository.getSources()
 | 
			
		||||
            testSources = repository.getSourcesDetails()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertEquals(sourcesDB.map { it.toView() }, testSources)
 | 
			
		||||
        coVerify(exactly = 0) { api.sources() }
 | 
			
		||||
        coVerify(exactly = 0) { api.sourcesDetailed() }
 | 
			
		||||
        verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -741,13 +744,13 @@ class RepositoryTest {
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
        var testSources: List<SelfossModel.Source>
 | 
			
		||||
        runBlocking {
 | 
			
		||||
            testSources = repository.getSources()
 | 
			
		||||
            testSources = repository.getSourcesDetails()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertEquals(emptyList<SelfossModel.Source>(), testSources)
 | 
			
		||||
        coVerify(exactly = 0) { api.sources() }
 | 
			
		||||
        coVerify(exactly = 0) { api.sourcesDetailed() }
 | 
			
		||||
        verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -758,13 +761,13 @@ class RepositoryTest {
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
        var testSources: List<SelfossModel.Source>
 | 
			
		||||
        runBlocking {
 | 
			
		||||
            testSources = repository.getSources()
 | 
			
		||||
            testSources = repository.getSourcesDetails()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertEquals(sourcesDB.map { it.toView() }, testSources)
 | 
			
		||||
        coVerify(exactly = 0) { api.sources() }
 | 
			
		||||
        coVerify(exactly = 0) { api.sourcesDetailed() }
 | 
			
		||||
        verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -775,13 +778,13 @@ class RepositoryTest {
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var testSources: List<SelfossModel.Source>?
 | 
			
		||||
        var testSources: List<SelfossModel.Source>
 | 
			
		||||
        runBlocking {
 | 
			
		||||
            testSources = repository.getSources()
 | 
			
		||||
            testSources = repository.getSourcesDetails()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertEquals(sourcesDB.map { it.toView() }, testSources)
 | 
			
		||||
        coVerify(exactly = 0) { api.sources() }
 | 
			
		||||
        coVerify(exactly = 0) { api.sourcesDetailed() }
 | 
			
		||||
        verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -1102,9 +1105,10 @@ class RepositoryTest {
 | 
			
		||||
    private fun prepareSearch() {
 | 
			
		||||
        repository.setTagFilter(SelfossModel.Tag("Tag", "read", 0))
 | 
			
		||||
        repository.setSourceFilter(
 | 
			
		||||
            SelfossModel.Source(
 | 
			
		||||
            SelfossModel.SourceDetail(
 | 
			
		||||
                1,
 | 
			
		||||
                "First source",
 | 
			
		||||
                5,
 | 
			
		||||
                listOf("Test", "second"),
 | 
			
		||||
                SPOUT,
 | 
			
		||||
                "",
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ import org.kodein.di.instance
 | 
			
		||||
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() }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,9 +6,12 @@ class MercuryModel {
 | 
			
		||||
 | 
			
		||||
    @Serializable
 | 
			
		||||
    class ParsedContent(
 | 
			
		||||
        val title: String?,
 | 
			
		||||
        val content: String?,
 | 
			
		||||
        val lead_image_url: String?, // NOSONAR
 | 
			
		||||
        val url: String
 | 
			
		||||
        val title: String? = null,
 | 
			
		||||
        val content: String? = null,
 | 
			
		||||
        val lead_image_url: String? = null, // NOSONAR
 | 
			
		||||
        val url: String? = null,
 | 
			
		||||
        val error: Boolean? = null,
 | 
			
		||||
        val message: String? = null,
 | 
			
		||||
        val failed: Boolean? = null
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,8 +24,8 @@ class SelfossModel {
 | 
			
		||||
    @Serializable
 | 
			
		||||
    class Stats(
 | 
			
		||||
        val total: Int,
 | 
			
		||||
        val unread: Int,
 | 
			
		||||
        val starred: Int
 | 
			
		||||
        val unread: Int? = null,
 | 
			
		||||
        val starred: Int? = null
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @Serializable
 | 
			
		||||
@@ -36,9 +36,9 @@ class SelfossModel {
 | 
			
		||||
 | 
			
		||||
    @Serializable
 | 
			
		||||
    data class ApiInformation(
 | 
			
		||||
        val version: String?,
 | 
			
		||||
        val apiversion: String?,
 | 
			
		||||
        val configuration: ApiConfiguration?
 | 
			
		||||
        val version: String? = null,
 | 
			
		||||
        val apiversion: String? = null,
 | 
			
		||||
        val configuration: ApiConfiguration? = null
 | 
			
		||||
    ) {
 | 
			
		||||
        fun getApiMajorVersion(): Int {
 | 
			
		||||
            var versionNumber = 0
 | 
			
		||||
@@ -54,29 +54,48 @@ class SelfossModel {
 | 
			
		||||
    @Serializable
 | 
			
		||||
    data class ApiConfiguration(
 | 
			
		||||
        @Serializable(with = BooleanSerializer::class)
 | 
			
		||||
        val publicMode: Boolean?,
 | 
			
		||||
        val publicMode: Boolean? = null,
 | 
			
		||||
        @Serializable(with = BooleanSerializer::class)
 | 
			
		||||
        val authEnabled: Boolean?
 | 
			
		||||
        val authEnabled: Boolean? = null
 | 
			
		||||
    ) {
 | 
			
		||||
        fun isAuthEnabled() = authEnabled ?: true
 | 
			
		||||
 | 
			
		||||
        fun isPublicModeEnabled() = publicMode ?: false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface Source {
 | 
			
		||||
        val id: Int
 | 
			
		||||
        var title: String
 | 
			
		||||
        var unread: Int?
 | 
			
		||||
        var error: String?
 | 
			
		||||
        var icon: String?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Serializable
 | 
			
		||||
    data class Source(
 | 
			
		||||
        val id: Int,
 | 
			
		||||
        val title: String,
 | 
			
		||||
    data class SourceStats(
 | 
			
		||||
        override val id: Int,
 | 
			
		||||
        override var title: String,
 | 
			
		||||
        override var unread: Int? = null,
 | 
			
		||||
        override var error: String? = null,
 | 
			
		||||
        override var icon: String? = null
 | 
			
		||||
        ) : Source
 | 
			
		||||
 | 
			
		||||
    @Serializable
 | 
			
		||||
    data class SourceDetail(
 | 
			
		||||
        override val id: Int,
 | 
			
		||||
        override var title: String,
 | 
			
		||||
        override var unread: Int? = null,
 | 
			
		||||
        @Serializable(with = TagsListSerializer::class)
 | 
			
		||||
        val tags: List<String>,
 | 
			
		||||
        val spout: String,
 | 
			
		||||
        val error: String,
 | 
			
		||||
        val icon: String?,
 | 
			
		||||
        val params: SourceParams?
 | 
			
		||||
    )
 | 
			
		||||
        var tags: List<String>? = null,
 | 
			
		||||
        var spout: String? = null,
 | 
			
		||||
        override var error: String? = null,
 | 
			
		||||
        override var icon: String? = null,
 | 
			
		||||
        var params: SourceParams? = null
 | 
			
		||||
    ) : Source
 | 
			
		||||
 | 
			
		||||
    @Serializable
 | 
			
		||||
    data class SourceParams(
 | 
			
		||||
        val url: String
 | 
			
		||||
        val url: String? = null
 | 
			
		||||
    )
 | 
			
		||||
    @Serializable
 | 
			
		||||
    data class Item(
 | 
			
		||||
@@ -88,13 +107,13 @@ class SelfossModel {
 | 
			
		||||
        var unread: Boolean,
 | 
			
		||||
        @Serializable(with = BooleanSerializer::class)
 | 
			
		||||
        var starred: Boolean,
 | 
			
		||||
        val thumbnail: String?,
 | 
			
		||||
        val icon: String?,
 | 
			
		||||
        val thumbnail: String? = null,
 | 
			
		||||
        val icon: String? = null,
 | 
			
		||||
        val link: String,
 | 
			
		||||
        val sourcetitle: String,
 | 
			
		||||
        @Serializable(with = TagsListSerializer::class)
 | 
			
		||||
        val tags: List<String>,
 | 
			
		||||
        val author: String?
 | 
			
		||||
        val author: String? = null
 | 
			
		||||
    ) {
 | 
			
		||||
        fun getLinkDecoded(): String {
 | 
			
		||||
            var stringUrl: String
 | 
			
		||||
 
 | 
			
		||||
@@ -44,11 +44,11 @@ class Repository(
 | 
			
		||||
    private val _badgeStarred = MutableStateFlow(0)
 | 
			
		||||
    val badgeStarred = _badgeStarred.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    private var fetchedSources = false
 | 
			
		||||
    private var fetchedTags = false
 | 
			
		||||
    private var fetchedSources = false
 | 
			
		||||
 | 
			
		||||
    private var _readerItems = ArrayList<SelfossModel.Item>()
 | 
			
		||||
    private var _selectedSource: SelfossModel.Source? = null
 | 
			
		||||
    private var _selectedSource: SelfossModel.SourceDetail? = null
 | 
			
		||||
 | 
			
		||||
    suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
 | 
			
		||||
        var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
 | 
			
		||||
@@ -132,9 +132,9 @@ class Repository(
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            val response = api.stats()
 | 
			
		||||
            if (response.success && response.data != null) {
 | 
			
		||||
                _badgeUnread.value = response.data.unread
 | 
			
		||||
                _badgeUnread.value = response.data.unread ?: 0
 | 
			
		||||
                _badgeAll.value = response.data.total
 | 
			
		||||
                _badgeStarred.value = response.data.starred
 | 
			
		||||
                _badgeStarred.value = response.data.starred ?: 0
 | 
			
		||||
                success = true
 | 
			
		||||
            }
 | 
			
		||||
        } else if (appSettingsService.isItemCachingEnabled()) {
 | 
			
		||||
@@ -180,23 +180,46 @@ class Repository(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getSources(): ArrayList<SelfossModel.Source> {
 | 
			
		||||
    suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> {
 | 
			
		||||
        var sources = ArrayList<SelfossModel.Source>()
 | 
			
		||||
        val isDatabaseEnabled =
 | 
			
		||||
            appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
 | 
			
		||||
        return if (isNetworkAvailable() && !fetchedSources) {
 | 
			
		||||
            val apiSources = api.sources()
 | 
			
		||||
            if (apiSources.success && apiSources.data != null && isDatabaseEnabled) {
 | 
			
		||||
                resetDBSourcesWithData(apiSources.data)
 | 
			
		||||
                if (!appSettingsService.isUpdateSourcesEnabled()) {
 | 
			
		||||
        val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
 | 
			
		||||
        if (shouldFetch && isNetworkAvailable()) {
 | 
			
		||||
            if (appSettingsService.getPublicAccess()) {
 | 
			
		||||
                val apiSources = api.sourcesStats()
 | 
			
		||||
                if (apiSources.success && apiSources.data != null) {
 | 
			
		||||
                    fetchedSources = true
 | 
			
		||||
                    sources = apiSources.data as ArrayList<SelfossModel.Source>
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                sources = getSourcesDetails() as ArrayList<SelfossModel.Source>
 | 
			
		||||
            }
 | 
			
		||||
        } else if (isDatabaseEnabled) {
 | 
			
		||||
            sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.Source>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return sources
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getSourcesDetails(): ArrayList<SelfossModel.SourceDetail> {
 | 
			
		||||
        var sources = ArrayList<SelfossModel.SourceDetail>()
 | 
			
		||||
        val isDatabaseEnabled =
 | 
			
		||||
            appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
 | 
			
		||||
        val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
 | 
			
		||||
        if (shouldFetch && isNetworkAvailable()) {
 | 
			
		||||
            val apiSources = api.sourcesDetailed()
 | 
			
		||||
            if (apiSources.success && apiSources.data != null) {
 | 
			
		||||
                fetchedSources = true
 | 
			
		||||
                sources = apiSources.data
 | 
			
		||||
                if (isDatabaseEnabled) {
 | 
			
		||||
                    resetDBSourcesWithData(sources)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            apiSources.data ?: ArrayList()
 | 
			
		||||
        } else if (isDatabaseEnabled) {
 | 
			
		||||
            ArrayList(getDBSources().map { it.toView() })
 | 
			
		||||
        } else {
 | 
			
		||||
            ArrayList()
 | 
			
		||||
            sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.SourceDetail>
 | 
			
		||||
        }
 | 
			
		||||
        return sources
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun markAsRead(item: SelfossModel.Item): Boolean {
 | 
			
		||||
@@ -482,7 +505,7 @@ class Repository(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun resetDBSourcesWithData(sources: List<SelfossModel.Source>) {
 | 
			
		||||
    private fun resetDBSourcesWithData(sources: List<SelfossModel.SourceDetail>) {
 | 
			
		||||
        db.sourcesQueries.deleteAllSources()
 | 
			
		||||
 | 
			
		||||
        db.sourcesQueries.transaction {
 | 
			
		||||
@@ -592,7 +615,7 @@ class Repository(
 | 
			
		||||
        ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setSelectedSource(source: SelfossModel.Source) {
 | 
			
		||||
    fun setSelectedSource(source: SelfossModel.SourceDetail) {
 | 
			
		||||
        _selectedSource = source
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -600,7 +623,7 @@ class Repository(
 | 
			
		||||
        _selectedSource = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getSelectedSource(): SelfossModel.Source? {
 | 
			
		||||
    fun getSelectedSource(): SelfossModel.SourceDetail? {
 | 
			
		||||
        return _selectedSource
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,6 +7,7 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
import io.github.aakira.napier.Napier
 | 
			
		||||
import io.ktor.client.*
 | 
			
		||||
import io.ktor.client.plugins.*
 | 
			
		||||
import io.ktor.client.plugins.auth.providers.*
 | 
			
		||||
import io.ktor.client.plugins.cache.*
 | 
			
		||||
import io.ktor.client.plugins.contentnegotiation.*
 | 
			
		||||
import io.ktor.client.plugins.cookies.*
 | 
			
		||||
@@ -15,6 +16,9 @@ import io.ktor.client.request.*
 | 
			
		||||
import io.ktor.client.statement.*
 | 
			
		||||
import io.ktor.http.*
 | 
			
		||||
import io.ktor.serialization.kotlinx.json.*
 | 
			
		||||
import io.ktor.util.*
 | 
			
		||||
import io.ktor.utils.io.charsets.*
 | 
			
		||||
import io.ktor.utils.io.core.*
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
@@ -32,6 +36,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    prettyPrint = true
 | 
			
		||||
                    isLenient = true
 | 
			
		||||
                    ignoreUnknownKeys = true
 | 
			
		||||
                    explicitNulls = false
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
            install(Logging) {
 | 
			
		||||
@@ -63,6 +68,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
            expectSuccess = false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return client
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -74,6 +80,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
        client = createHttpClient()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun constructBasicAuthValue(credentials: BasicAuthCredentials): String {
 | 
			
		||||
        val authString = "${credentials.username}:${credentials.password}"
 | 
			
		||||
        val authBuf = authString.toByteArray(Charsets.UTF_8).encodeBase64()
 | 
			
		||||
 | 
			
		||||
        return "Basic $authBuf"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Api version was introduces after the POST login, so when there is a version, it should be available
 | 
			
		||||
    private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
 | 
			
		||||
    private fun hasLoginInfo() =
 | 
			
		||||
@@ -96,11 +109,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
    private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) {
 | 
			
		||||
        parameter("username", appSettingsService.getUserName())
 | 
			
		||||
        parameter("password", appSettingsService.getPassword())
 | 
			
		||||
        if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
            headers {
 | 
			
		||||
                append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    private suspend fun postLogin() = maybeResponse(client.tryToPost(url("/login")) {
 | 
			
		||||
        parameter("username", appSettingsService.getUserName())
 | 
			
		||||
        parameter("password", appSettingsService.getPassword())
 | 
			
		||||
        if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
            headers {
 | 
			
		||||
                append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    private fun shouldHaveNewLogout() =
 | 
			
		||||
@@ -114,9 +139,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private suspend fun maybeLogoutIfAvailable() =
 | 
			
		||||
        responseOrSuccessIf404(client.tryToGet(url("/logout")))
 | 
			
		||||
        responseOrSuccessIf404(client.tryToGet(url("/logout")) {
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current")))
 | 
			
		||||
    private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current")) {
 | 
			
		||||
        if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
            headers {
 | 
			
		||||
                append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    suspend fun getItems(
 | 
			
		||||
        type: String,
 | 
			
		||||
@@ -139,6 +178,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
            parameter("updatedsince", updatedSince)
 | 
			
		||||
            parameter("items", items ?: appSettingsService.getItemsNumber())
 | 
			
		||||
            parameter("offset", offset)
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> =
 | 
			
		||||
@@ -149,6 +194,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
            }
 | 
			
		||||
            parameter("type", "all")
 | 
			
		||||
            parameter("items", 1)
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun stats(): StatusAndData<SelfossModel.Stats> =
 | 
			
		||||
@@ -157,6 +208,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
 | 
			
		||||
@@ -165,6 +222,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun update(): StatusAndData<String> =
 | 
			
		||||
@@ -173,6 +236,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
 | 
			
		||||
@@ -181,18 +250,51 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> =
 | 
			
		||||
    suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
 | 
			
		||||
        bodyOrFailure(client.tryToGet(url("/sources/stats")) {
 | 
			
		||||
            if (!shouldHavePostLogin()) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
 | 
			
		||||
        bodyOrFailure(client.tryToGet(url("/sources/list")) {
 | 
			
		||||
            if (!shouldHavePostLogin()) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
 | 
			
		||||
        bodyOrFailure(client.tryToGet(url("/api/about")))
 | 
			
		||||
        bodyOrFailure(client.tryToGet(url("/api/about")) {
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun markAsRead(id: String): SuccessResponse =
 | 
			
		||||
        maybeResponse(client.tryToPost(url("/mark/$id")) {
 | 
			
		||||
@@ -200,6 +302,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun unmarkAsRead(id: String): SuccessResponse =
 | 
			
		||||
@@ -208,6 +316,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun starr(id: String): SuccessResponse =
 | 
			
		||||
@@ -216,6 +330,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun unstarr(id: String): SuccessResponse =
 | 
			
		||||
@@ -224,6 +344,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
 | 
			
		||||
@@ -235,6 +361,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    append("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                ids.map { append("ids[]", it) }
 | 
			
		||||
            },
 | 
			
		||||
            block = {
 | 
			
		||||
                if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        ))
 | 
			
		||||
 | 
			
		||||
@@ -270,6 +404,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                append("url", url)
 | 
			
		||||
                append("spout", spout)
 | 
			
		||||
                append(tagsParamName, tags)
 | 
			
		||||
            },
 | 
			
		||||
            block = {
 | 
			
		||||
                if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -307,6 +449,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                append("url", url)
 | 
			
		||||
                append("spout", spout)
 | 
			
		||||
                append(tagsParamName, tags)
 | 
			
		||||
            },
 | 
			
		||||
            block = {
 | 
			
		||||
                if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -316,5 +466,18 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
            }
 | 
			
		||||
            if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
 | 
			
		||||
                headers {
 | 
			
		||||
                    append(
 | 
			
		||||
                        HttpHeaders.Authorization,
 | 
			
		||||
                        constructBasicAuthValue(
 | 
			
		||||
                            BasicAuthCredentials(
 | 
			
		||||
                                username = appSettingsService.getBasicUserName(),
 | 
			
		||||
                                password = appSettingsService.getBasicPassword()
 | 
			
		||||
                            )
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,70 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.service
 | 
			
		||||
 | 
			
		||||
import com.russhwolf.settings.Settings
 | 
			
		||||
 | 
			
		||||
// This will be used in ACRA process. For now, it does nothing.
 | 
			
		||||
// This is to fix ACRA not sending reports anymore.
 | 
			
		||||
// See https://www.acra.ch/docs/Troubleshooting-Guide#applicationoncreate
 | 
			
		||||
class ACRASettings : Settings {
 | 
			
		||||
    override val keys: Set<String> = emptySet()
 | 
			
		||||
    override val size: Int = 0
 | 
			
		||||
 | 
			
		||||
    override fun clear() {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getBoolean(key: String, defaultValue: Boolean): Boolean = false
 | 
			
		||||
 | 
			
		||||
    override fun getBooleanOrNull(key: String): Boolean? = null
 | 
			
		||||
 | 
			
		||||
    override fun getDouble(key: String, defaultValue: Double): Double = 0.0
 | 
			
		||||
 | 
			
		||||
    override fun getDoubleOrNull(key: String): Double? = null
 | 
			
		||||
 | 
			
		||||
    override fun getFloat(key: String, defaultValue: Float): Float = 0.0F
 | 
			
		||||
 | 
			
		||||
    override fun getFloatOrNull(key: String): Float? = null
 | 
			
		||||
 | 
			
		||||
    override fun getInt(key: String, defaultValue: Int): Int = 0
 | 
			
		||||
 | 
			
		||||
    override fun getIntOrNull(key: String): Int? = null
 | 
			
		||||
 | 
			
		||||
    override fun getLong(key: String, defaultValue: Long): Long = 0
 | 
			
		||||
 | 
			
		||||
    override fun getLongOrNull(key: String): Long? = null
 | 
			
		||||
 | 
			
		||||
    override fun getString(key: String, defaultValue: String): String = "0"
 | 
			
		||||
 | 
			
		||||
    override fun getStringOrNull(key: String): String? = null
 | 
			
		||||
 | 
			
		||||
    override fun hasKey(key: String): Boolean = false
 | 
			
		||||
 | 
			
		||||
    override fun putBoolean(key: String, value: Boolean) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putDouble(key: String, value: Double) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putFloat(key: String, value: Float) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putInt(key: String, value: Int) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putLong(key: String, value: Long) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putString(key: String, value: String) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun remove(key: String) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -2,15 +2,17 @@ package bou.amine.apps.readerforselfossv2.service
 | 
			
		||||
 | 
			
		||||
import com.russhwolf.settings.Settings
 | 
			
		||||
 | 
			
		||||
class AppSettingsService {
 | 
			
		||||
    val settings: Settings = Settings()
 | 
			
		||||
class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
 | 
			
		||||
    val settings: Settings = if (acraSenderServiceProcess) { ACRASettings() } else { Settings() }
 | 
			
		||||
 | 
			
		||||
    // Api related
 | 
			
		||||
    private var _apiVersion: Int = -1
 | 
			
		||||
    private var _publicAccess: Boolean? = null
 | 
			
		||||
    private var _baseUrl: String = ""
 | 
			
		||||
    private var _userName: String = ""
 | 
			
		||||
    private var _basicUserName: String = ""
 | 
			
		||||
    private var _password: String = ""
 | 
			
		||||
    private var _basicPassword: String = ""
 | 
			
		||||
 | 
			
		||||
    // User settings related
 | 
			
		||||
    private var _itemsCaching: Boolean? = null
 | 
			
		||||
@@ -96,6 +98,20 @@ class AppSettingsService {
 | 
			
		||||
        return _password
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getBasicUserName(): String {
 | 
			
		||||
        if (_basicUserName.isEmpty()) {
 | 
			
		||||
            refreshBasicUsername()
 | 
			
		||||
        }
 | 
			
		||||
        return _basicUserName
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getBasicPassword(): String {
 | 
			
		||||
        if (_basicPassword.isEmpty()) {
 | 
			
		||||
            refreshBasicPassword()
 | 
			
		||||
        }
 | 
			
		||||
        return _basicPassword
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getItemsNumber(): Int {
 | 
			
		||||
        if (_itemsNumber == null) {
 | 
			
		||||
            refreshItemsNumber()
 | 
			
		||||
@@ -149,6 +165,14 @@ class AppSettingsService {
 | 
			
		||||
        _password = settings.getString(PASSWORD, "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshBasicUsername() {
 | 
			
		||||
        _basicUserName = settings.getString(BASIC_LOGIN, "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshBasicPassword() {
 | 
			
		||||
        _basicPassword = settings.getString(BASIC_PASSWORD, "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshArticleViewerEnabled() {
 | 
			
		||||
        _articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
 | 
			
		||||
    }
 | 
			
		||||
@@ -354,6 +378,8 @@ class AppSettingsService {
 | 
			
		||||
    fun refreshApiSettings() {
 | 
			
		||||
        refreshPassword()
 | 
			
		||||
        refreshUsername()
 | 
			
		||||
        refreshBasicUsername()
 | 
			
		||||
        refreshBasicPassword()
 | 
			
		||||
        refreshBaseUrl()
 | 
			
		||||
        refreshApiVersion()
 | 
			
		||||
        refreshPublicAccess()
 | 
			
		||||
@@ -387,7 +413,17 @@ class AppSettingsService {
 | 
			
		||||
        login: String,
 | 
			
		||||
        password: String
 | 
			
		||||
    ) {
 | 
			
		||||
        settings.putString(BASE_URL, url)
 | 
			
		||||
        val regex = """\/\/(\D+):(\D+)@""".toRegex()
 | 
			
		||||
        val matchResult = regex.find(url)
 | 
			
		||||
        if (matchResult != null) {
 | 
			
		||||
            val (basicLogin, basicPassword) = matchResult.destructured
 | 
			
		||||
            settings.putString(BASIC_LOGIN, basicLogin)
 | 
			
		||||
            settings.putString(BASIC_PASSWORD, basicPassword)
 | 
			
		||||
            val urlWithoutBasicAuth = url.replace(regex, "//")
 | 
			
		||||
            settings.putString(BASE_URL, urlWithoutBasicAuth)
 | 
			
		||||
        } else {
 | 
			
		||||
            settings.putString(BASE_URL, url)
 | 
			
		||||
        }
 | 
			
		||||
        settings.putString(LOGIN, login)
 | 
			
		||||
        settings.putString(PASSWORD, password)
 | 
			
		||||
        refreshApiSettings()
 | 
			
		||||
@@ -397,6 +433,8 @@ class AppSettingsService {
 | 
			
		||||
        settings.remove(BASE_URL)
 | 
			
		||||
        settings.remove(LOGIN)
 | 
			
		||||
        settings.remove(PASSWORD)
 | 
			
		||||
        settings.remove(BASIC_LOGIN)
 | 
			
		||||
        settings.remove(BASIC_PASSWORD)
 | 
			
		||||
        refreshApiSettings()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -440,6 +478,10 @@ class AppSettingsService {
 | 
			
		||||
 | 
			
		||||
        const val PASSWORD = "password"
 | 
			
		||||
 | 
			
		||||
        const val BASIC_LOGIN = "basic_login"
 | 
			
		||||
 | 
			
		||||
        const val BASIC_PASSWORD = "basic_password"
 | 
			
		||||
 | 
			
		||||
        const val PREFER_ARTICLE_VIEWER = "prefer_article_viewer"
 | 
			
		||||
 | 
			
		||||
        const val CARD_VIEW_ACTIVE = "card_view_active"
 | 
			
		||||
@@ -470,7 +512,6 @@ class AppSettingsService {
 | 
			
		||||
 | 
			
		||||
        const val PERIODIC_REFRESH_MINUTES = "periodic_refresh_minutes"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        const val INFINITE_LOADING = "infinite_loading"
 | 
			
		||||
 | 
			
		||||
        const val ITEMS_CACHING = "items_caching"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,24 +12,25 @@ fun TAG.toView(): SelfossModel.Tag =
 | 
			
		||||
        this.unread.toInt()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
fun SOURCE.toView(): SelfossModel.Source =
 | 
			
		||||
    SelfossModel.Source(
 | 
			
		||||
fun SOURCE.toView(): SelfossModel.SourceDetail =
 | 
			
		||||
    SelfossModel.SourceDetail(
 | 
			
		||||
        this.id.toInt(),
 | 
			
		||||
        this.title,
 | 
			
		||||
        this.tags.split(","),
 | 
			
		||||
        null,
 | 
			
		||||
        this.tags?.split(","),
 | 
			
		||||
        this.spout,
 | 
			
		||||
        this.error,
 | 
			
		||||
        this.icon,
 | 
			
		||||
        if (this.url != null) SelfossModel.SourceParams(this.url) else null
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
fun SelfossModel.Source.toEntity(): SOURCE =
 | 
			
		||||
fun SelfossModel.SourceDetail.toEntity(): SOURCE =
 | 
			
		||||
    SOURCE(
 | 
			
		||||
        this.id.toString(),
 | 
			
		||||
        this.title.getHtmlDecoded(),
 | 
			
		||||
        this.tags.joinToString(","),
 | 
			
		||||
        this.spout,
 | 
			
		||||
        this.error,
 | 
			
		||||
        this.tags?.joinToString(",").orEmpty(),
 | 
			
		||||
        this.spout.orEmpty(),
 | 
			
		||||
        this.error.orEmpty(),
 | 
			
		||||
        this.icon.orEmpty(),
 | 
			
		||||
        this.params?.url
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user