Compare commits
22 Commits
7a5e2f1155
...
4e0117877d
Author | SHA1 | Date | |
---|---|---|---|
4e0117877d | |||
80ca79cba3 | |||
6b142df554 | |||
cee82e2a3d | |||
|
9057ee0052 | ||
|
50d0b44315 | ||
|
21b08ed384 | ||
|
993c4d2ee9 | ||
|
57a9d51027 | ||
|
673f0edb8b | ||
|
7f96798f13 | ||
|
6e5704a45b | ||
|
495591159f | ||
|
718fe7c5ee | ||
|
ecd23213f9 | ||
|
e6baed8cb4 | ||
|
c87abec0b9 | ||
|
0aba41d8bf | ||
|
2a2d1047b4 | ||
|
66ef1ccf32 | ||
|
677ede5bc7 | ||
|
996a7ed22c |
48
CHANGELOG.md
48
CHANGELOG.md
@ -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**
|
||||
|
||||
- fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master
|
||||
|
@ -8,7 +8,7 @@ plugins {
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
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 {
|
||||
@ -16,7 +16,7 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
|
||||
project.exec {
|
||||
commandLine = cmd.split(" ")
|
||||
standardOutput = outputStream
|
||||
isIgnoreExitValue = ignore ?: false
|
||||
isIgnoreExitValue = ignore
|
||||
}
|
||||
outputStream.toString()
|
||||
}
|
||||
@ -24,9 +24,8 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
|
||||
}
|
||||
|
||||
fun gitVersion(): String {
|
||||
var process = ""
|
||||
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.")
|
||||
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
|
||||
} else {
|
||||
@ -58,13 +57,13 @@ android {
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
// Flag to enable support for the new language APIs
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
// For Kotlin projects
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
jvmTarget = "17"
|
||||
}
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "33.0.0"
|
||||
@ -171,7 +170,7 @@ dependencies {
|
||||
//PhotoView
|
||||
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")
|
||||
|
||||
|
@ -139,6 +139,8 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
|
||||
showProgress(true)
|
||||
|
||||
appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
|
||||
|
||||
repository.refreshLoginInformation(url, login, password)
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
|
@ -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)
|
||||
|
@ -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.sendSilentlyWithAcraWithName
|
||||
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.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
|
||||
@ -260,21 +260,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()
|
||||
}
|
||||
@ -287,14 +274,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)
|
||||
@ -307,7 +315,7 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
binding.webcontent.webViewClient = object : WebViewClient() {
|
||||
@Deprecated("Deprecated in Java")
|
||||
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 {
|
||||
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
@ -523,7 +531,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 {
|
||||
@ -541,8 +554,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)
|
||||
|
@ -149,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 {
|
||||
|
@ -51,6 +51,13 @@
|
||||
android:maxLines="1"
|
||||
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
|
||||
android:id="@+id/withLogin"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -33,8 +33,8 @@
|
||||
<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_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">"Can't get spouts list. There may ben an api 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">"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="pref_header_links">"Ligazóns"</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_off">A barra inferior pode mostrarse a través do botón flotante</string>
|
||||
<string name="remove_source">Eliminar fonte</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="gdpr_dialog_title">The app does not share any personal data about you.</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="crash_toast_text">A crash occured. Sending the details to the developper.</string>
|
||||
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
|
||||
<string name="menu_home_filter">Filters</string>
|
||||
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
|
||||
<string name="pref_theme_title">Modo Claro/Escuro</string>
|
||||
<string name="mode_dark">Modo escuro</string>
|
||||
<string name="mode_system">Seguir axustes do sistema</string>
|
||||
<string name="mode_light">Modo claro</string>
|
||||
<string name="gdpr_dialog_title">A aplicación non comparte ningún dato persoal seu.</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">Ocurriu un erro. Enviando os detalles o desenvolvedor.</string>
|
||||
<string name="pref_switch_disable_acra">"Deshabilitar o reporte automático de erros. "</string>
|
||||
<string name="menu_home_filter">Filtros</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="update_source">Update source</string>
|
||||
</resources>
|
||||
|
@ -90,7 +90,7 @@
|
||||
<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_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="pref_switch_periodic_refresh">Sync articles</string>
|
||||
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
|
||||
|
@ -125,7 +125,7 @@
|
||||
<string name="crash_toast_text">发生崩溃。请将细节发送给开发人员。</string>
|
||||
<string name="pref_switch_disable_acra">"禁用自动错误报告 "</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="menu_home_sources">Sources</string>
|
||||
<string name="update_source">Update source</string>
|
||||
<string name="application_selfoss_only">此应用只适用于 Selfoss 实例,不适用于其他 RSS 。</string>
|
||||
<string name="menu_home_sources">源</string>
|
||||
<string name="update_source">更新源</string>
|
||||
</resources>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<resources>
|
||||
<string name="app_name">"Reader for Selfoss"</string>
|
||||
<string name="title_activity_login">"Log in"</string>
|
||||
<string name="prompt_password">"Password"</string>
|
||||
@ -6,6 +6,7 @@
|
||||
<string name="error_invalid_password">"Password not long enough"</string>
|
||||
<string name="error_field_required">"Field required"</string>
|
||||
<string name="prompt_url">"Url"</string>
|
||||
<string name="disableSSL">"Disable SSL"</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="prompt_login">"Username"</string>
|
||||
|
@ -7,12 +7,12 @@ buildscript {
|
||||
|
||||
plugins {
|
||||
//trick: for the same plugin versions in all sub-modules
|
||||
id("com.android.application").version("7.4.0").apply(false)
|
||||
id("com.android.library").version("7.4.0").apply(false)
|
||||
kotlin("android").version("1.7.20").apply(false)
|
||||
kotlin("multiplatform").version("1.7.20").apply(false)
|
||||
id("com.android.application").version("8.0.0").apply(false)
|
||||
id("com.android.library").version("8.0.0").apply(false)
|
||||
kotlin("android").version("1.9.0").apply(false)
|
||||
kotlin("multiplatform").version("1.9.0").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 {
|
||||
|
@ -13,22 +13,15 @@
|
||||
#Tue Mar 22 16:50:00 CET 2022
|
||||
#Gradle
|
||||
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||
|
||||
#Kotlin
|
||||
kotlin.code.style=official
|
||||
|
||||
#Android
|
||||
android.useAndroidX=true
|
||||
kotlin.native.enableDependencyPropagation=false
|
||||
#android.nonTransitiveRClass=true
|
||||
android.enableJetifier=true
|
||||
|
||||
|
||||
android.nonTransitiveRClass=false
|
||||
#MPP
|
||||
kotlin.mpp.enableCInteropCommonization=true
|
||||
kotlin.mpp.enableGranularSourceSetsMetadata=true
|
||||
|
||||
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
ignoreGitVersion=false
|
||||
|
6
gradle/wrapper/gradle-wrapper.properties
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Mon Jan 23 20:47:46 CET 2023
|
||||
#Thu Jul 13 11:41:19 CEST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -1,3 +1,5 @@
|
||||
val ktorVersion = "2.3.2"
|
||||
|
||||
object SqlDelight {
|
||||
const val runtime = "com.squareup.sqldelight:runtime:1.5.4"
|
||||
const val android = "com.squareup.sqldelight:android-driver:1.5.4"
|
||||
@ -9,12 +11,12 @@ plugins {
|
||||
kotlin("multiplatform")
|
||||
id("com.android.library")
|
||||
id("com.squareup.sqldelight")
|
||||
kotlin("plugin.serialization") version "1.4.10"
|
||||
id("org.jetbrains.kotlinx.kover") version "0.6.1"
|
||||
kotlin("plugin.serialization") version "1.9.0"
|
||||
id("org.jetbrains.kotlinx.kover")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android()
|
||||
androidTarget()
|
||||
|
||||
listOf(
|
||||
iosX64(),
|
||||
@ -29,13 +31,14 @@ kotlin {
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-core:2.1.1")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:2.1.1")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.1")
|
||||
implementation("io.ktor:ktor-client-logging:2.1.1")
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||
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("io.ktor:ktor-client-auth:2.1.1")
|
||||
implementation("org.jsoup:jsoup:1.14.3")
|
||||
|
||||
implementation("org.jsoup:jsoup:1.15.4")
|
||||
|
||||
//Dependency Injection
|
||||
implementation("org.kodein.di:kodein-di:7.12.0")
|
||||
@ -58,14 +61,15 @@ kotlin {
|
||||
}
|
||||
val androidMain by getting {
|
||||
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")
|
||||
|
||||
// Sql
|
||||
implementation(SqlDelight.android)
|
||||
}
|
||||
}
|
||||
val androidTest by getting {
|
||||
val androidUnitTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-junit"))
|
||||
implementation("junit:junit:4.13.2")
|
||||
@ -98,15 +102,14 @@ kotlin {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 32
|
||||
compileSdk = 33
|
||||
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
targetSdk = 32
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
namespace = "bou.amine.apps.readerforselfossv2"
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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,9 +54,9 @@ 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
|
||||
|
||||
@ -75,7 +75,7 @@ class SelfossModel {
|
||||
data class SourceStats(
|
||||
override val id: Int,
|
||||
override var title: String,
|
||||
override var unread: Int?,
|
||||
override var unread: Int? = null,
|
||||
override var error: String? = null,
|
||||
override var icon: String? = null
|
||||
) : Source
|
||||
@ -86,11 +86,11 @@ class SelfossModel {
|
||||
override var title: String,
|
||||
override var unread: Int? = null,
|
||||
@Serializable(with = TagsListSerializer::class)
|
||||
var tags: List<String>?,
|
||||
var spout: String?,
|
||||
override var error: String?,
|
||||
override var icon: String?,
|
||||
var 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
|
||||
@ -107,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
|
||||
|
@ -4,79 +4,45 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.model.StatusAndData
|
||||
import bou.amine.apps.readerforselfossv2.model.SuccessResponse
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import io.github.aakira.napier.Napier
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.cache.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.plugins.cookies.*
|
||||
import io.ktor.client.plugins.logging.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.auth.providers.BasicAuthCredentials
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.headers
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.Parameters
|
||||
import io.ktor.util.encodeBase64
|
||||
import io.ktor.utils.io.charsets.Charsets
|
||||
import io.ktor.utils.io.core.toByteArray
|
||||
|
||||
expect fun createHttpClient(
|
||||
appSettingsService: AppSettingsService,
|
||||
api: SelfossApi
|
||||
): HttpClient
|
||||
|
||||
class SelfossApi(private val appSettingsService: AppSettingsService) {
|
||||
|
||||
var client = createHttpClient()
|
||||
|
||||
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
|
||||
}
|
||||
var client = createHttpClient(appSettingsService, this)
|
||||
|
||||
fun url(path: String) =
|
||||
"${appSettingsService.getBaseUrl()}$path"
|
||||
|
||||
fun refreshLoginInformation() {
|
||||
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
|
||||
private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
|
||||
private fun hasLoginInfo() =
|
||||
fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
|
||||
fun hasLoginInfo() =
|
||||
appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword()
|
||||
.isNotEmpty()
|
||||
|
||||
@ -96,11 +62,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 +92,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 +131,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 +147,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 +161,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 +175,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 +189,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,6 +203,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 sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
|
||||
@ -189,6 +217,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 sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
|
||||
@ -197,10 +231,23 @@ 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 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")) {
|
||||
@ -208,6 +255,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 =
|
||||
@ -216,6 +269,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 =
|
||||
@ -224,6 +283,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 =
|
||||
@ -232,6 +297,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 =
|
||||
@ -243,6 +314,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()))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
@ -278,6 +357,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()))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -315,6 +402,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()))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -324,5 +419,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()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -8,9 +8,12 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
|
||||
// Api related
|
||||
private var _apiVersion: Int = -1
|
||||
private var _publicAccess: Boolean? = null
|
||||
private var _selfSigned: 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
|
||||
@ -75,6 +78,22 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = 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 {
|
||||
if (_baseUrl.isEmpty()) {
|
||||
refreshBaseUrl()
|
||||
@ -96,6 +115,20 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
|
||||
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 +182,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
|
||||
_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,9 +395,12 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
|
||||
fun refreshApiSettings() {
|
||||
refreshPassword()
|
||||
refreshUsername()
|
||||
refreshBasicUsername()
|
||||
refreshBasicPassword()
|
||||
refreshBaseUrl()
|
||||
refreshApiVersion()
|
||||
refreshPublicAccess()
|
||||
refreshSelfSigned()
|
||||
}
|
||||
|
||||
fun refreshUserSettings() {
|
||||
@ -387,7 +431,17 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
|
||||
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 +451,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
|
||||
settings.remove(BASE_URL)
|
||||
settings.remove(LOGIN)
|
||||
settings.remove(PASSWORD)
|
||||
settings.remove(BASIC_LOGIN)
|
||||
settings.remove(BASIC_PASSWORD)
|
||||
refreshApiSettings()
|
||||
}
|
||||
|
||||
@ -430,6 +486,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
|
||||
|
||||
const val API_PUBLIC_ACCESS = "apiPublicAccess"
|
||||
|
||||
const val API_SELF_SIGNED = "apiSelfSigned"
|
||||
|
||||
const val API_ITEMS_NUMBER = "prefer_api_items_number"
|
||||
|
||||
const val API_TIMEOUT = "api_timeout"
|
||||
@ -440,6 +498,10 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
|
||||
|
||||
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 +532,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
|
||||
|
||||
const val PERIODIC_REFRESH_MINUTES = "periodic_refresh_minutes"
|
||||
|
||||
|
||||
const val INFINITE_LOADING = "infinite_loading"
|
||||
|
||||
const val ITEMS_CACHING = "items_caching"
|
||||
|
Loading…
Reference in New Issue
Block a user