Compare commits

...

22 Commits

Author SHA1 Message Date
4e0117877d Add a login switch to disable SSL verification 2023-07-13 14:52:25 +02:00
80ca79cba3 Upgrade to gradle 8 2023-07-13 14:51:17 +02:00
6b142df554 Add a switch in the login screen to disable SSL 2023-06-30 11:35:19 +02:00
cee82e2a3d Tentative self signed ssl support 2023-06-30 11:35:15 +02:00
aminecmi
9057ee0052 Changelog for v123061651 [CI SKIP] 2023-06-14 18:23:59 +00:00
aminecmi
50d0b44315 i18n: Translation update. 2023-06-13 20:52:42 +02:00
aminecmi
21b08ed384 i18n: Translation update. 2023-06-13 20:26:36 +02:00
aminecmi
993c4d2ee9 i18n: Translation update. 2023-06-12 20:50:26 +02:00
aminecmi
57a9d51027 fix: avoid trying to open invalid image urls. 2023-06-12 20:34:35 +02:00
aminecmi
673f0edb8b Changelog for v123051471 [CI SKIP] 2023-05-27 19:25:35 +00:00
aminecmi
7f96798f13 fix: images could be null. 2023-05-27 21:06:56 +02:00
aminecmi
6e5704a45b fix: Check if color is not empty before parsing it. 2023-05-27 21:02:25 +02:00
aminecmi
495591159f chore: Removed unused log. 2023-05-27 21:01:54 +02:00
aminecmi
718fe7c5ee Changelog for v123051331 [CI SKIP] 2023-05-13 20:24:57 +00:00
aminecmi
ecd23213f9 fix: illegal input. 2023-05-13 22:14:25 +02:00
aminecmi
e6baed8cb4 Changelog for v123051321 [CI SKIP] 2023-05-12 19:19:35 +00:00
aminecmi
c87abec0b9 debug: Debug null context. 2023-05-12 20:31:40 +02:00
Amine Louveau
0aba41d8bf Changelog for v123051301 [CI SKIP] 2023-05-10 19:36:31 +00:00
Amine Louveau
2a2d1047b4 feat: Basic auth from url. Fixes #142 (#143)
Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/143
2023-05-10 19:19:11 +00:00
aminecmi
66ef1ccf32 debug: Debug index out of bound exception. 2023-05-10 20:50:13 +02:00
aminecmi
677ede5bc7 Changelog for v123051211 [CI SKIP] 2023-05-01 18:26:12 +00:00
aminecmi
996a7ed22c fix: Sometimes url isn't even defined. 2023-04-30 18:41:15 +02:00
20 changed files with 514 additions and 180 deletions

View File

@ -1,3 +1,51 @@
**v123061651**
- i18n: Translation update.
- i18n: Translation update.
- i18n: Translation update.
- fix: avoid trying to open invalid image urls.
- Changelog for v123051471 [CI SKIP]
--------------------------------------------------------------------
**v123051471**
- fix: images could be null.
- fix: Check if color is not empty before parsing it.
- chore: Removed unused log.
- Changelog for v123051331 [CI SKIP]
--------------------------------------------------------------------
**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** **v123041021**
- fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master - fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master

View File

@ -8,7 +8,7 @@ plugins {
kotlin("android") kotlin("android")
kotlin("kapt") kotlin("kapt")
id("com.mikepenz.aboutlibraries.plugin") id("com.mikepenz.aboutlibraries.plugin")
id("org.jetbrains.kotlinx.kover") version "0.6.1" id("org.jetbrains.kotlinx.kover")
} }
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String { fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
@ -16,7 +16,7 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
project.exec { project.exec {
commandLine = cmd.split(" ") commandLine = cmd.split(" ")
standardOutput = outputStream standardOutput = outputStream
isIgnoreExitValue = ignore ?: false isIgnoreExitValue = ignore
} }
outputStream.toString() outputStream.toString()
} }
@ -24,9 +24,8 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
} }
fun gitVersion(): String { fun gitVersion(): String {
var process = ""
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true) val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
process = if (maybeTagOfCurrentCommit.isEmpty()) { var process = if (maybeTagOfCurrentCommit.isEmpty()) {
println("No tag on current commit. Will take the latest one.") println("No tag on current commit. Will take the latest one.")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1") execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
} else { } else {
@ -58,13 +57,13 @@ android {
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
// Flag to enable support for the new language APIs // Flag to enable support for the new language APIs
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_17
} }
// For Kotlin projects // For Kotlin projects
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "17"
} }
compileSdk = 33 compileSdk = 33
buildToolsVersion = "33.0.0" buildToolsVersion = "33.0.0"
@ -171,7 +170,7 @@ dependencies {
//PhotoView //PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.8.0") implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")

View File

@ -139,6 +139,8 @@ class LoginActivity : AppCompatActivity(), DIAware {
showProgress(true) showProgress(true)
appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
repository.refreshLoginInformation(url, login, password) repository.refreshLoginInformation(url, login, password)
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {

View File

@ -71,7 +71,11 @@ class ReaderActivity : AppCompatActivity(), DIAware {
finish() finish()
} }
try {
readItem(allItems[currentItem]) readItem(allItems[currentItem])
} catch (e: IndexOutOfBoundsException) {
finish()
}
binding.pager.adapter = ScreenSlidePagerAdapter(this) binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(currentItem, false) binding.pager.setCurrentItem(currentItem, false)

View File

@ -27,11 +27,11 @@ import bou.amine.apps.readerforselfossv2.android.model.toModel
import bou.amine.apps.readerforselfossv2.android.model.toParcelable import bou.amine.apps.readerforselfossv2.android.model.toParcelable
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.shareLink import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.MercuryModel import bou.amine.apps.readerforselfossv2.model.MercuryModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.MercuryApi import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@ -260,21 +260,8 @@ class ArticleFragment : Fragment(), DIAware {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try { try {
val response = mercuryApi.query(url) val response = mercuryApi.query(url)
if (response.success && response.data != null && !response.data?.content.isNullOrEmpty()) { if (response.success && response.data != null) {
binding.titleView.text = response.data!!.title.orEmpty() handleMercuryData(response.data!!)
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
} else { } else {
openInBrowserAfterFailing() openInBrowserAfterFailing()
} }
@ -287,14 +274,35 @@ class ArticleFragment : Fragment(), DIAware {
} }
} }
private fun handleLeadImage(response: StatusAndData<MercuryModel.ParsedContent>) { private fun handleMercuryData(data: MercuryModel.ParsedContent) {
if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) { 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 binding.imageView.visibility = View.VISIBLE
Glide Glide
.with(requireContext()) .with(requireContext())
.asBitmap() .asBitmap()
.load( .load(
response.data!!.lead_image_url.orEmpty() lead_image_url
) )
.apply(RequestOptions.fitCenterTransform()) .apply(RequestOptions.fitCenterTransform())
.into(binding.imageView) .into(binding.imageView)
@ -307,7 +315,7 @@ class ArticleFragment : Fragment(), DIAware {
binding.webcontent.webViewClient = object : WebViewClient() { binding.webcontent.webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean { override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
return if (context != null && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
try { try {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
@ -523,7 +531,12 @@ class ArticleFragment : Fragment(), DIAware {
private fun openInBrowserAfterFailing() { private fun openInBrowserAfterFailing() {
binding.progressBar.visibility = View.GONE 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 { companion object {
@ -541,8 +554,8 @@ class ArticleFragment : Fragment(), DIAware {
} }
fun performClick(): Boolean { fun performClick(): Boolean {
if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || if (allImages != null && (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE)
) { ) {
val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra) val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)

View File

@ -149,6 +149,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
c.ellipsize = TextUtils.TruncateAt.END c.ellipsize = TextUtils.TruncateAt.END
c.text = tag.tag c.text = tag.tag
if (tag.color.isNotEmpty()) {
try { try {
val gd = GradientDrawable() val gd = GradientDrawable()
val gdColor = try { val gdColor = try {
@ -165,6 +166,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcraWithName("tags > GradientDrawable") e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
} }
}
c.setOnCloseIconClickListener { c.setOnCloseIconClickListener {
(it as Chip).isCloseIconVisible = false (it as Chip).isCloseIconVisible = false

View File

@ -51,6 +51,13 @@
android:maxLines="1" android:maxLines="1"
android:minHeight="48dp" /> android:minHeight="48dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/selfSigned"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/disableSSL"
android:textAlignment="viewStart" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/withLogin" android:id="@+id/withLogin"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -33,8 +33,8 @@
<string name="addStringNoUrl">"Accede pra engadir fontes."</string> <string name="addStringNoUrl">"Accede pra engadir fontes."</string>
<string name="cant_get_sources">"Non se pode obter a lista de fontes."</string> <string name="cant_get_sources">"Non se pode obter a lista de fontes."</string>
<string name="cant_create_source">"Non se pode crear unha fonte."</string> <string name="cant_create_source">"Non se pode crear unha fonte."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string> <string name="cant_get_spouts_no_network">"Non se pode obter a lista de spouts por mor dun erro de rede."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"Non se pode obter a lista de spoits. Pode que haxa algún problema coa api."</string>
<string name="form_not_complete">"O formulario non está completo"</string> <string name="form_not_complete">"O formulario non está completo"</string>
<string name="pref_header_links">"Ligazóns"</string> <string name="pref_header_links">"Ligazóns"</string>
<string name="issue_tracker_link">"Rastrexador de Incidencias"</string> <string name="issue_tracker_link">"Rastrexador de Incidencias"</string>
@ -116,16 +116,16 @@
<string name="reader_static_bar_on">A barra inferior mostrarase sempre</string> <string name="reader_static_bar_on">A barra inferior mostrarase sempre</string>
<string name="reader_static_bar_off">A barra inferior pode mostrarse a través do botón flotante</string> <string name="reader_static_bar_off">A barra inferior pode mostrarse a través do botón flotante</string>
<string name="remove_source">Eliminar fonte</string> <string name="remove_source">Eliminar fonte</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Modo Claro/Escuro</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Modo escuro</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Seguir axustes do sistema</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Modo claro</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">A aplicación non comparte ningún dato persoal seu.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[O envío de informes de erros está habilitado. Pode deshabilitarse dende a páxina de axustes. Ten en conta que os informes de erros son esenciais para o desenvolvemento da aplicación.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">Ocurriu un erro. Enviando os detalles o desenvolvedor.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Deshabilitar o reporte automático de erros. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filtros</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">Esta aplicación só funciona cunha instancia de Selfoss, e con ningún outro filtro RSS.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
</resources> </resources>

View File

@ -90,7 +90,7 @@
<string name="pref_switch_items_caching">Save items for offline use</string> <string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string> <string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string> <string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="network_connectivity_lost">"Network connection lost"</string> <string name="network_connectivity_lost">"Koneksi jaringan hilang"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string> <string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string> <string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string> <string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -125,7 +125,7 @@
<string name="crash_toast_text">发生崩溃。请将细节发送给开发人员。</string> <string name="crash_toast_text">发生崩溃。请将细节发送给开发人员。</string>
<string name="pref_switch_disable_acra">"禁用自动错误报告 "</string> <string name="pref_switch_disable_acra">"禁用自动错误报告 "</string>
<string name="menu_home_filter">筛选器</string> <string name="menu_home_filter">筛选器</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">此应用只适用于 Selfoss 实例,不适用于其他 RSS 。</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources"></string>
<string name="update_source">Update source</string> <string name="update_source">更新源</string>
</resources> </resources>

View File

@ -1,4 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Reader for Selfoss"</string> <string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"Log in"</string> <string name="title_activity_login">"Log in"</string>
<string name="prompt_password">"Password"</string> <string name="prompt_password">"Password"</string>
@ -6,6 +6,7 @@
<string name="error_invalid_password">"Password not long enough"</string> <string name="error_invalid_password">"Password not long enough"</string>
<string name="error_field_required">"Field required"</string> <string name="error_field_required">"Field required"</string>
<string name="prompt_url">"Url"</string> <string name="prompt_url">"Url"</string>
<string name="disableSSL">"Disable SSL"</string>
<string name="withLoginSwitch">"Login required ?"</string> <string name="withLoginSwitch">"Login required ?"</string>
<string name="login_url_problem">"Oops. You may need to add a \"/\" at the end of the url."</string> <string name="login_url_problem">"Oops. You may need to add a \"/\" at the end of the url."</string>
<string name="prompt_login">"Username"</string> <string name="prompt_login">"Username"</string>

View File

@ -7,12 +7,12 @@ buildscript {
plugins { plugins {
//trick: for the same plugin versions in all sub-modules //trick: for the same plugin versions in all sub-modules
id("com.android.application").version("7.4.0").apply(false) id("com.android.application").version("8.0.0").apply(false)
id("com.android.library").version("7.4.0").apply(false) id("com.android.library").version("8.0.0").apply(false)
kotlin("android").version("1.7.20").apply(false) kotlin("android").version("1.9.0").apply(false)
kotlin("multiplatform").version("1.7.20").apply(false) kotlin("multiplatform").version("1.9.0").apply(false)
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false) id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
id("org.jetbrains.kotlinx.kover") version "0.6.1" id("org.jetbrains.kotlinx.kover").version("0.6.1").apply(true)
} }
allprojects { allprojects {

View File

@ -13,22 +13,15 @@
#Tue Mar 22 16:50:00 CET 2022 #Tue Mar 22 16:50:00 CET 2022
#Gradle #Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
#Kotlin #Kotlin
kotlin.code.style=official kotlin.code.style=official
#Android #Android
android.useAndroidX=true android.useAndroidX=true
kotlin.native.enableDependencyPropagation=false
#android.nonTransitiveRClass=true #android.nonTransitiveRClass=true
android.enableJetifier=true android.enableJetifier=true
android.nonTransitiveRClass=false
#MPP #MPP
kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.enableCInteropCommonization=true
kotlin.mpp.enableGranularSourceSetsMetadata=true
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
ignoreGitVersion=false ignoreGitVersion=false

View File

@ -1,6 +1,6 @@
#Mon Jan 23 20:47:46 CET 2023 #Thu Jul 13 11:41:19 CEST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,3 +1,5 @@
val ktorVersion = "2.3.2"
object SqlDelight { object SqlDelight {
const val runtime = "com.squareup.sqldelight:runtime:1.5.4" const val runtime = "com.squareup.sqldelight:runtime:1.5.4"
const val android = "com.squareup.sqldelight:android-driver:1.5.4" const val android = "com.squareup.sqldelight:android-driver:1.5.4"
@ -9,12 +11,12 @@ plugins {
kotlin("multiplatform") kotlin("multiplatform")
id("com.android.library") id("com.android.library")
id("com.squareup.sqldelight") id("com.squareup.sqldelight")
kotlin("plugin.serialization") version "1.4.10" kotlin("plugin.serialization") version "1.9.0"
id("org.jetbrains.kotlinx.kover") version "0.6.1" id("org.jetbrains.kotlinx.kover")
} }
kotlin { kotlin {
android() androidTarget()
listOf( listOf(
iosX64(), iosX64(),
@ -29,13 +31,14 @@ kotlin {
sourceSets { sourceSets {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation("io.ktor:ktor-client-core:2.1.1") implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:2.1.1") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.1") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-client-logging:2.1.1") implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-auth:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("io.ktor:ktor-client-auth:2.1.1")
implementation("org.jsoup:jsoup:1.14.3") implementation("org.jsoup:jsoup:1.15.4")
//Dependency Injection //Dependency Injection
implementation("org.kodein.di:kodein-di:7.12.0") implementation("org.kodein.di:kodein-di:7.12.0")
@ -58,14 +61,15 @@ kotlin {
} }
val androidMain by getting { val androidMain by getting {
dependencies { dependencies {
implementation("io.ktor:ktor-client-okhttp:2.1.1") implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("io.ktor:ktor-client-okhttp:2.2.4")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
// Sql // Sql
implementation(SqlDelight.android) implementation(SqlDelight.android)
} }
} }
val androidTest by getting { val androidUnitTest by getting {
dependencies { dependencies {
implementation(kotlin("test-junit")) implementation(kotlin("test-junit"))
implementation("junit:junit:4.13.2") implementation("junit:junit:4.13.2")
@ -98,15 +102,14 @@ kotlin {
} }
android { android {
compileSdk = 32 compileSdk = 33
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
targetSdk = 32
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_17
} }
namespace = "bou.amine.apps.readerforselfossv2" namespace = "bou.amine.apps.readerforselfossv2"
} }

View File

@ -0,0 +1,90 @@
package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.github.aakira.napier.Napier
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.cookies.HttpCookies
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.apache.http.conn.ssl.AllowAllHostnameVerifier
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
class NaiveTrustManager : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
}
actual fun createHttpClient(
appSettingsService: AppSettingsService,
api: SelfossApi
) =
HttpClient(OkHttp) {
if (appSettingsService.getSelfSigned()) {
engine {
val trustManager = NaiveTrustManager()
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, arrayOf(trustManager), null)
}
preconfigured = OkHttpClient().newBuilder()
.sslSocketFactory(
sslSocketFactory = sslContext.socketFactory,
trustManager = trustManager
)
.hostnameVerifier(AllowAllHostnameVerifier())
.build()
}
}
install(ContentNegotiation) {
install(HttpCache)
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Napier.d(message, tag = "LogApiCalls")
}
}
level = LogLevel.INFO
}
install(HttpTimeout) {
requestTimeoutMillis = appSettingsService.getApiTimeout()
}
install(HttpCookies)
install(HttpRequestRetry) {
maxRetries = 2
retryIf { _, response ->
response.status == HttpStatusCode.Forbidden && api.shouldHavePostLogin() && api.hasLoginInfo()
}
modifyRequest {
Napier.i("Will modify", tag = "HttpSend")
CoroutineScope(Dispatchers.Main).launch {
Napier.i("Will login", tag = "HttpSend")
api.login()
Napier.i("Did login", tag = "HttpSend")
}
}
}
expectSuccess = false
}

View File

@ -6,9 +6,12 @@ class MercuryModel {
@Serializable @Serializable
class ParsedContent( class ParsedContent(
val title: String?, val title: String? = null,
val content: String?, val content: String? = null,
val lead_image_url: String?, // NOSONAR val lead_image_url: String? = null, // NOSONAR
val url: String val url: String? = null,
val error: Boolean? = null,
val message: String? = null,
val failed: Boolean? = null
) )
} }

View File

@ -24,8 +24,8 @@ class SelfossModel {
@Serializable @Serializable
class Stats( class Stats(
val total: Int, val total: Int,
val unread: Int?, val unread: Int? = null,
val starred: Int? val starred: Int? = null
) )
@Serializable @Serializable
@ -36,9 +36,9 @@ class SelfossModel {
@Serializable @Serializable
data class ApiInformation( data class ApiInformation(
val version: String?, val version: String? = null,
val apiversion: String?, val apiversion: String? = null,
val configuration: ApiConfiguration? val configuration: ApiConfiguration? = null
) { ) {
fun getApiMajorVersion(): Int { fun getApiMajorVersion(): Int {
var versionNumber = 0 var versionNumber = 0
@ -54,9 +54,9 @@ class SelfossModel {
@Serializable @Serializable
data class ApiConfiguration( data class ApiConfiguration(
@Serializable(with = BooleanSerializer::class) @Serializable(with = BooleanSerializer::class)
val publicMode: Boolean?, val publicMode: Boolean? = null,
@Serializable(with = BooleanSerializer::class) @Serializable(with = BooleanSerializer::class)
val authEnabled: Boolean? val authEnabled: Boolean? = null
) { ) {
fun isAuthEnabled() = authEnabled ?: true fun isAuthEnabled() = authEnabled ?: true
@ -75,7 +75,7 @@ class SelfossModel {
data class SourceStats( data class SourceStats(
override val id: Int, override val id: Int,
override var title: String, override var title: String,
override var unread: Int?, override var unread: Int? = null,
override var error: String? = null, override var error: String? = null,
override var icon: String? = null override var icon: String? = null
) : Source ) : Source
@ -86,11 +86,11 @@ class SelfossModel {
override var title: String, override var title: String,
override var unread: Int? = null, override var unread: Int? = null,
@Serializable(with = TagsListSerializer::class) @Serializable(with = TagsListSerializer::class)
var tags: List<String>?, var tags: List<String>? = null,
var spout: String?, var spout: String? = null,
override var error: String?, override var error: String? = null,
override var icon: String?, override var icon: String? = null,
var params: SourceParams? var params: SourceParams? = null
) : Source ) : Source
@Serializable @Serializable
@ -107,13 +107,13 @@ class SelfossModel {
var unread: Boolean, var unread: Boolean,
@Serializable(with = BooleanSerializer::class) @Serializable(with = BooleanSerializer::class)
var starred: Boolean, var starred: Boolean,
val thumbnail: String?, val thumbnail: String? = null,
val icon: String?, val icon: String? = null,
val link: String, val link: String,
val sourcetitle: String, val sourcetitle: String,
@Serializable(with = TagsListSerializer::class) @Serializable(with = TagsListSerializer::class)
val tags: List<String>, val tags: List<String>,
val author: String? val author: String? = null
) { ) {
fun getLinkDecoded(): String { fun getLinkDecoded(): String {
var stringUrl: String var stringUrl: String

View File

@ -4,79 +4,45 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.model.SuccessResponse import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.github.aakira.napier.Napier import io.ktor.client.HttpClient
import io.ktor.client.* import io.ktor.client.plugins.auth.providers.BasicAuthCredentials
import io.ktor.client.plugins.* import io.ktor.client.request.get
import io.ktor.client.plugins.cache.* import io.ktor.client.request.headers
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.parameter
import io.ktor.client.plugins.cookies.* import io.ktor.client.statement.HttpResponse
import io.ktor.client.plugins.logging.* import io.ktor.http.HttpHeaders
import io.ktor.client.request.* import io.ktor.http.Parameters
import io.ktor.client.statement.* import io.ktor.util.encodeBase64
import io.ktor.http.* import io.ktor.utils.io.charsets.Charsets
import io.ktor.serialization.kotlinx.json.* import io.ktor.utils.io.core.toByteArray
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers expect fun createHttpClient(
import kotlinx.coroutines.launch appSettingsService: AppSettingsService,
import kotlinx.serialization.json.Json api: SelfossApi
): HttpClient
class SelfossApi(private val appSettingsService: AppSettingsService) { class SelfossApi(private val appSettingsService: AppSettingsService) {
var client = createHttpClient() var client = createHttpClient(appSettingsService, this)
private fun createHttpClient(): HttpClient {
val client = HttpClient {
install(ContentNegotiation) {
install(HttpCache)
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Napier.d(message, tag = "LogApiCalls")
}
}
level = LogLevel.INFO
}
install(HttpTimeout) {
requestTimeoutMillis = appSettingsService.getApiTimeout()
}
install(HttpCookies)
install(HttpRequestRetry) {
maxRetries = 2
retryIf { _, response ->
response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo()
}
modifyRequest {
Napier.i("Will modify", tag = "HttpSend")
CoroutineScope(Dispatchers.Main).launch {
Napier.i("Will login", tag = "HttpSend")
this@SelfossApi.login()
Napier.i("Did login", tag = "HttpSend")
}
}
}
expectSuccess = false
}
return client
}
fun url(path: String) = fun url(path: String) =
"${appSettingsService.getBaseUrl()}$path" "${appSettingsService.getBaseUrl()}$path"
fun refreshLoginInformation() { fun refreshLoginInformation() {
appSettingsService.refreshApiSettings() appSettingsService.refreshApiSettings()
client = createHttpClient() client = createHttpClient(appSettingsService, this)
}
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 // Api version was introduces after the POST login, so when there is a version, it should be available
private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1 fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
private fun hasLoginInfo() = fun hasLoginInfo() =
appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword() appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword()
.isNotEmpty() .isNotEmpty()
@ -96,11 +62,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) { private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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")) { private suspend fun postLogin() = maybeResponse(client.tryToPost(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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() = private fun shouldHaveNewLogout() =
@ -114,9 +92,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
private suspend fun maybeLogoutIfAvailable() = 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( suspend fun getItems(
type: String, type: String,
@ -139,6 +131,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("updatedsince", updatedSince) parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber()) parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset) 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>> = suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> =
@ -149,6 +147,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
parameter("type", "all") parameter("type", "all")
parameter("items", 1) 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> = suspend fun stats(): StatusAndData<SelfossModel.Stats> =
@ -157,6 +161,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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>> = suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
@ -165,6 +175,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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> = suspend fun update(): StatusAndData<String> =
@ -173,6 +189,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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>> = suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
@ -181,6 +203,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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 sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> = suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
@ -189,6 +217,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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>> = suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
@ -197,10 +231,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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> = 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 = suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse(client.tryToPost(url("/mark/$id")) { maybeResponse(client.tryToPost(url("/mark/$id")) {
@ -208,6 +255,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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 = suspend fun unmarkAsRead(id: String): SuccessResponse =
@ -216,6 +269,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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 = suspend fun starr(id: String): SuccessResponse =
@ -224,6 +283,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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 = suspend fun unstarr(id: String): SuccessResponse =
@ -232,6 +297,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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 = suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
@ -243,6 +314,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append("password", appSettingsService.getPassword()) append("password", appSettingsService.getPassword())
} }
ids.map { append("ids[]", it) } 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()))
)
}
}
} }
)) ))
@ -278,6 +357,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append("url", url) append("url", url)
append("spout", spout) append("spout", spout)
append(tagsParamName, tags) append(tagsParamName, tags)
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
} }
) )
@ -315,6 +402,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append("url", url) append("url", url)
append("spout", spout) append("spout", spout)
append(tagsParamName, tags) append(tagsParamName, tags)
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
} }
) )
@ -324,5 +419,18 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword()
)
)
)
}
}
}) })
} }

View File

@ -8,9 +8,12 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
// Api related // Api related
private var _apiVersion: Int = -1 private var _apiVersion: Int = -1
private var _publicAccess: Boolean? = null private var _publicAccess: Boolean? = null
private var _selfSigned: Boolean? = null
private var _baseUrl: String = "" private var _baseUrl: String = ""
private var _userName: String = "" private var _userName: String = ""
private var _basicUserName: String = ""
private var _password: String = "" private var _password: String = ""
private var _basicPassword: String = ""
// User settings related // User settings related
private var _itemsCaching: Boolean? = null private var _itemsCaching: Boolean? = null
@ -75,6 +78,22 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
_publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false) _publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false)
} }
fun getSelfSigned(): Boolean {
if (_selfSigned == null) {
refreshSelfSigned()
}
return _selfSigned!!
}
fun updateSelfSigned(selfSigned: Boolean) {
settings.putBoolean(API_SELF_SIGNED, selfSigned)
refreshSelfSigned()
}
private fun refreshSelfSigned() {
_selfSigned = settings.getBoolean(API_SELF_SIGNED, false)
}
fun getBaseUrl(): String { fun getBaseUrl(): String {
if (_baseUrl.isEmpty()) { if (_baseUrl.isEmpty()) {
refreshBaseUrl() refreshBaseUrl()
@ -96,6 +115,20 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
return _password return _password
} }
fun getBasicUserName(): String {
if (_basicUserName.isEmpty()) {
refreshBasicUsername()
}
return _basicUserName
}
fun getBasicPassword(): String {
if (_basicPassword.isEmpty()) {
refreshBasicPassword()
}
return _basicPassword
}
fun getItemsNumber(): Int { fun getItemsNumber(): Int {
if (_itemsNumber == null) { if (_itemsNumber == null) {
refreshItemsNumber() refreshItemsNumber()
@ -149,6 +182,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
_password = settings.getString(PASSWORD, "") _password = settings.getString(PASSWORD, "")
} }
private fun refreshBasicUsername() {
_basicUserName = settings.getString(BASIC_LOGIN, "")
}
private fun refreshBasicPassword() {
_basicPassword = settings.getString(BASIC_PASSWORD, "")
}
private fun refreshArticleViewerEnabled() { private fun refreshArticleViewerEnabled() {
_articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true) _articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
} }
@ -354,9 +395,12 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
fun refreshApiSettings() { fun refreshApiSettings() {
refreshPassword() refreshPassword()
refreshUsername() refreshUsername()
refreshBasicUsername()
refreshBasicPassword()
refreshBaseUrl() refreshBaseUrl()
refreshApiVersion() refreshApiVersion()
refreshPublicAccess() refreshPublicAccess()
refreshSelfSigned()
} }
fun refreshUserSettings() { fun refreshUserSettings() {
@ -387,7 +431,17 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
login: String, login: String,
password: String password: String
) { ) {
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(BASE_URL, url)
}
settings.putString(LOGIN, login) settings.putString(LOGIN, login)
settings.putString(PASSWORD, password) settings.putString(PASSWORD, password)
refreshApiSettings() refreshApiSettings()
@ -397,6 +451,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
settings.remove(BASE_URL) settings.remove(BASE_URL)
settings.remove(LOGIN) settings.remove(LOGIN)
settings.remove(PASSWORD) settings.remove(PASSWORD)
settings.remove(BASIC_LOGIN)
settings.remove(BASIC_PASSWORD)
refreshApiSettings() refreshApiSettings()
} }
@ -430,6 +486,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
const val API_PUBLIC_ACCESS = "apiPublicAccess" const val API_PUBLIC_ACCESS = "apiPublicAccess"
const val API_SELF_SIGNED = "apiSelfSigned"
const val API_ITEMS_NUMBER = "prefer_api_items_number" const val API_ITEMS_NUMBER = "prefer_api_items_number"
const val API_TIMEOUT = "api_timeout" const val API_TIMEOUT = "api_timeout"
@ -440,6 +498,10 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
const val PASSWORD = "password" 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 PREFER_ARTICLE_VIEWER = "prefer_article_viewer"
const val CARD_VIEW_ACTIVE = "card_view_active" const val CARD_VIEW_ACTIVE = "card_view_active"
@ -470,7 +532,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
const val PERIODIC_REFRESH_MINUTES = "periodic_refresh_minutes" const val PERIODIC_REFRESH_MINUTES = "periodic_refresh_minutes"
const val INFINITE_LOADING = "infinite_loading" const val INFINITE_LOADING = "infinite_loading"
const val ITEMS_CACHING = "items_caching" const val ITEMS_CACHING = "items_caching"