Compare commits
	
		
			32 Commits
		
	
	
		
			v123020571
			...
			7f96798f13
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7f96798f13 | ||
|  | 6e5704a45b | ||
|  | 495591159f | ||
|  | 718fe7c5ee | ||
|  | ecd23213f9 | ||
|  | e6baed8cb4 | ||
|  | c87abec0b9 | ||
|  | 0aba41d8bf | ||
|  | 2a2d1047b4 | ||
|  | 66ef1ccf32 | ||
|  | 677ede5bc7 | ||
|  | 996a7ed22c | ||
|  | 85208c4e5a | ||
|  | 5cfec50cba | ||
| 76ad71e1dc | |||
| 0277fb507c | |||
| 8d7d3174aa | |||
|  | 00eb3333fe | ||
|  | 629ca01d99 | ||
|  | c2d8681ce8 | ||
|  | 08f79cb148 | ||
| e21906e70d | |||
|  | 9d2cc32bc9 | ||
|  | d9d057c8dc | ||
|  | 1f3fa0c4a6 | ||
|  | dea3def385 | ||
|  | f72ef2f5d4 | ||
|  | f28cb759df | ||
|  | b9d69c3e64 | ||
|  | c2a1c9eaac | ||
|  | bf37209a15 | ||
|  | 2c558fe6fd | 
							
								
								
									
										21
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								.drone.yml
									
									
									
									
									
								
							| @@ -137,6 +137,27 @@ steps: | |||||||
|         from_secret: giteaAPI |         from_secret: giteaAPI | ||||||
|       base_url: https://gitea.amine-louveau.fr |       base_url: https://gitea.amine-louveau.fr | ||||||
|       files: signed.apk |       files: signed.apk | ||||||
|  |  | ||||||
|  |   - name: notify | ||||||
|  |     image: drillster/drone-email | ||||||
|  |     failure: ignore | ||||||
|  |     settings: | ||||||
|  |       host: | ||||||
|  |         from_secret: smtpHOST | ||||||
|  |       port: | ||||||
|  |         from_secret: smtpPORT | ||||||
|  |       username: | ||||||
|  |         from_secret: smtpUSERNAME | ||||||
|  |       password: | ||||||
|  |         from_secret: smtpPASSWORD | ||||||
|  |       from: | ||||||
|  |         from_secret: smtpFROM | ||||||
|  |       subject: Mapping file | ||||||
|  |       recipients: | ||||||
|  |         from_secret: smtpTO | ||||||
|  |       recipients_only: true | ||||||
|  |       skip_verify: true | ||||||
|  |       attachment: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt | ||||||
| trigger: | trigger: | ||||||
|   event: |   event: | ||||||
|     - tag |     - tag | ||||||
							
								
								
									
										87
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,90 @@ | |||||||
|  | **v123051331** | ||||||
|  |  | ||||||
|  | - fix: illegal input. | ||||||
|  | - Changelog for v123051321 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123051321** | ||||||
|  |  | ||||||
|  | - debug: Debug null context. | ||||||
|  | - Changelog for v123051301 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123051301** | ||||||
|  |  | ||||||
|  | - feat: Basic auth from url. Fixes #142 (#143) | ||||||
|  | - debug: Debug index out of bound exception. | ||||||
|  | - Changelog for v123051211 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123051211** | ||||||
|  |  | ||||||
|  | - fix: Sometimes url isn't even defined. | ||||||
|  | - Changelog for v123041021 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123041021** | ||||||
|  |  | ||||||
|  | - fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master | ||||||
|  | - Enable Core Library Desugaring to support older Android versions | ||||||
|  | - Changelog for v123030851 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123030851** | ||||||
|  |  | ||||||
|  | - chore: replace textDrawable library (#136) | ||||||
|  | - refactor: Remove slow login check. Closes #135. | ||||||
|  | - ci: send the mapping file after a release. | ||||||
|  | - Changelog for v123030751 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123030751** | ||||||
|  |  | ||||||
|  | - debug: added a lot to pinpoint the url issue. | ||||||
|  | - feat: Use /sources/stats in the home (#133) | ||||||
|  | - Changelog for v123030681 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123030681** | ||||||
|  |  | ||||||
|  | - fix: Unread and starred can be null. | ||||||
|  | - Fixed version number issue. | ||||||
|  | - Changelog for v123030621 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123030621** | ||||||
|  |  | ||||||
|  | - fix: url required issue. | ||||||
|  | - fix: Canvas reused issue. | ||||||
|  | - Changelog for v123020572 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123020572** | ||||||
|  |  | ||||||
|  | - fix: requirecontext issues ? | ||||||
|  | - debug: activity not found exception. | ||||||
|  | - Changelog for v123020571 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | **v123020571** | ||||||
|  |  | ||||||
|  | - chore: remove errors logging. | ||||||
|  | - fix: quickfix for url param not provided for some sources. | ||||||
|  | - Update 'CHANGELOG.md' | ||||||
|  | - Changelog for v123020523 [CI SKIP] | ||||||
|  |  | ||||||
|  | -------------------------------------------------------------------- | ||||||
|  |  | ||||||
| **v123020523** | **v123020523** | ||||||
|  |  | ||||||
| - fix: Git changelog. | - fix: Git changelog. | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ fun gitVersion(): String { | |||||||
|     val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true) |     val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true) | ||||||
|     process = if (maybeTagOfCurrentCommit.isEmpty()) { |     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=-authordate --format='%(refname:short)' --count=1") |         execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1") | ||||||
|     } else { |     } else { | ||||||
|         println("Tag found on current commit") |         println("Tag found on current commit") | ||||||
|         execWithOutput("git -C ../ describe --contains HEAD") |         execWithOutput("git -C ../ describe --contains HEAD") | ||||||
| @@ -56,6 +56,7 @@ fun versionNameFromGit(): String { | |||||||
|  |  | ||||||
| android { | android { | ||||||
|     compileOptions { |     compileOptions { | ||||||
|  |         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_11 | ||||||
|         targetCompatibility = JavaVersion.VERSION_11 |         targetCompatibility = JavaVersion.VERSION_11 | ||||||
| @@ -112,6 +113,8 @@ android { | |||||||
| } | } | ||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|  |     coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") | ||||||
|  |  | ||||||
|     implementation(project(":shared")) |     implementation(project(":shared")) | ||||||
|     implementation("com.google.android.material:material:1.5.0") |     implementation("com.google.android.material:material:1.5.0") | ||||||
|     implementation("androidx.appcompat:appcompat:1.4.1") |     implementation("androidx.appcompat:appcompat:1.4.1") | ||||||
| @@ -142,11 +145,10 @@ dependencies { | |||||||
|  |  | ||||||
|     // Material-ish things |     // Material-ish things | ||||||
|     implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") |     implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") | ||||||
|     implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1") |  | ||||||
|  |  | ||||||
|     // glide |     // glide | ||||||
|     kapt("com.github.bumptech.glide:compiler:4.14.2") |     kapt("com.github.bumptech.glide:compiler:4.15.0") | ||||||
|     implementation("com.github.bumptech.glide:okhttp3-integration:4.14.2") |     implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0") | ||||||
|  |  | ||||||
|     // Themes |     // Themes | ||||||
|     implementation("com.github.rubensousa:floatingtoolbar:1.5.1") |     implementation("com.github.rubensousa:floatingtoolbar:1.5.1") | ||||||
|   | |||||||
| @@ -57,29 +57,7 @@ class LoginActivity : AppCompatActivity(), DIAware { | |||||||
|  |  | ||||||
|         if (appSettingsService.getBaseUrl().isNotEmpty()) { |         if (appSettingsService.getBaseUrl().isNotEmpty()) { | ||||||
|             showProgress(true) |             showProgress(true) | ||||||
|             // This should be reverted when "old" users connected with a non-selfoss rss |  | ||||||
|             // are handled. Revert to "simple" way. |  | ||||||
|             CoroutineScope(Dispatchers.Main).launch { |  | ||||||
|                 try { |  | ||||||
|                     val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance() |  | ||||||
|                     if (!errorFetching && !displaySelfossOnly) { |  | ||||||
|             goToMain() |             goToMain() | ||||||
|                     } else { |  | ||||||
|                         showProgress(false) |  | ||||||
|                         if (displaySelfossOnly) { |  | ||||||
|                             Toast.makeText( |  | ||||||
|                                 applicationContext, |  | ||||||
|                                 R.string.application_selfoss_only, |  | ||||||
|                                 Toast.LENGTH_LONG |  | ||||||
|                             ).show() |  | ||||||
|                         } |  | ||||||
|                         repository.logout() |  | ||||||
|                     } |  | ||||||
|                 } catch (e: Throwable) { |  | ||||||
|                     repository.logout() |  | ||||||
|                     showProgress(false) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         handleActions() |         handleActions() | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -49,13 +49,13 @@ class SourcesActivity : AppCompatActivity(), DIAware { | |||||||
|         super.onResume() |         super.onResume() | ||||||
|         val mLayoutManager = LinearLayoutManager(this) |         val mLayoutManager = LinearLayoutManager(this) | ||||||
|  |  | ||||||
|         var items: ArrayList<SelfossModel.Source> |         var items: ArrayList<SelfossModel.SourceDetail> | ||||||
|  |  | ||||||
|         binding.recyclerView.setHasFixedSize(true) |         binding.recyclerView.setHasFixedSize(true) | ||||||
|         binding.recyclerView.layoutManager = mLayoutManager |         binding.recyclerView.layoutManager = mLayoutManager | ||||||
|  |  | ||||||
|         CoroutineScope(Dispatchers.Main).launch { |         CoroutineScope(Dispatchers.Main).launch { | ||||||
|             val response = repository.getSources() |             val response = repository.getSourcesDetails() | ||||||
|             if (response.isNotEmpty()) { |             if (response.isNotEmpty()) { | ||||||
|                 items = response |                 items = response | ||||||
|                 val mAdapter = SourcesListAdapter( |                 val mAdapter = SourcesListAdapter( | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ import org.kodein.di.instance | |||||||
|  |  | ||||||
| class UpsertSourceActivity : AppCompatActivity(), DIAware { | class UpsertSourceActivity : AppCompatActivity(), DIAware { | ||||||
|  |  | ||||||
|     private var existingSource: SelfossModel.Source? = null |     private var existingSource: SelfossModel.SourceDetail? = null | ||||||
|     private var mSpoutsValue: String? = null |     private var mSpoutsValue: String? = null | ||||||
|  |  | ||||||
|     private lateinit var binding: ActivityUpsertSourceBinding |     private lateinit var binding: ActivityUpsertSourceBinding | ||||||
| @@ -68,7 +68,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware { | |||||||
|  |  | ||||||
|     private fun initFields(items: Map<String, SelfossModel.Spout>) { |     private fun initFields(items: Map<String, SelfossModel.Spout>) { | ||||||
|         binding.nameInput.setText(existingSource!!.title) |         binding.nameInput.setText(existingSource!!.title) | ||||||
|         binding.tags.setText(existingSource!!.tags.joinToString(", ")) |         binding.tags.setText(existingSource!!.tags?.joinToString(", ")) | ||||||
|         binding.sourceUri.setText(existingSource!!.params?.url) |         binding.sourceUri.setText(existingSource!!.params?.url) | ||||||
|         binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout)) |         binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout)) | ||||||
|         binding.progress.visibility = View.GONE |         binding.progress.visibility = View.GONE | ||||||
|   | |||||||
| @@ -9,10 +9,9 @@ import android.widget.ImageView.ScaleType | |||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import bou.amine.apps.readerforselfossv2.android.R | import bou.amine.apps.readerforselfossv2.android.R | ||||||
| import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding | import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding | ||||||
| import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString |  | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener | import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop | import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable | import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask | import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl | import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.shareLink | import bou.amine.apps.readerforselfossv2.android.utils.shareLink | ||||||
| @@ -22,8 +21,6 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService | |||||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||||
| import bou.amine.apps.readerforselfossv2.utils.getThumbnail | import bou.amine.apps.readerforselfossv2.utils.getThumbnail | ||||||
| import com.amulyakhare.textdrawable.TextDrawable |  | ||||||
| import com.amulyakhare.textdrawable.util.ColorGenerator |  | ||||||
| import com.bumptech.glide.Glide | import com.bumptech.glide.Glide | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
| @@ -38,7 +35,6 @@ class ItemCardAdapter( | |||||||
|     override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit |     override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit | ||||||
| ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { | ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { | ||||||
|     private val c: Context = app.baseContext |     private val c: Context = app.baseContext | ||||||
|     private val generator: ColorGenerator = ColorGenerator.MATERIAL |  | ||||||
|     private val imageMaxHeight: Int = |     private val imageMaxHeight: Int = | ||||||
|         c.resources.getDimension(R.dimen.card_image_max_height).toInt() |         c.resources.getDimension(R.dimen.card_image_max_height).toInt() | ||||||
|  |  | ||||||
| @@ -83,16 +79,9 @@ class ItemCardAdapter( | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (itm.getIcon(repository.baseUrl).isEmpty()) { |             if (itm.getIcon(repository.baseUrl).isEmpty()) { | ||||||
|                 val color = generator.getColor(itm.title.getHtmlDecoded()) |                 binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) | ||||||
|  |  | ||||||
|                 val drawable = |  | ||||||
|                     TextDrawable |  | ||||||
|                         .builder() |  | ||||||
|                         .round() |  | ||||||
|                         .build(itm.title.getHtmlDecoded().toTextDrawableString(), color) |  | ||||||
|                 binding.sourceImage.setImageDrawable(drawable) |  | ||||||
|             } else { |             } else { | ||||||
|                 c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage) |                 c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -7,10 +7,8 @@ import android.view.ViewGroup | |||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import bou.amine.apps.readerforselfossv2.android.R | import bou.amine.apps.readerforselfossv2.android.R | ||||||
| import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding | import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding | ||||||
| import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString |  | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener | import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop | import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable |  | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl | import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl | ||||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | import bou.amine.apps.readerforselfossv2.repository.Repository | ||||||
| @@ -18,8 +16,6 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService | |||||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||||
| import bou.amine.apps.readerforselfossv2.utils.getThumbnail | import bou.amine.apps.readerforselfossv2.utils.getThumbnail | ||||||
| import com.amulyakhare.textdrawable.TextDrawable |  | ||||||
| import com.amulyakhare.textdrawable.util.ColorGenerator |  | ||||||
| import org.kodein.di.DI | import org.kodein.di.DI | ||||||
| import org.kodein.di.android.closestDI | import org.kodein.di.android.closestDI | ||||||
| import org.kodein.di.instance | import org.kodein.di.instance | ||||||
| @@ -29,7 +25,6 @@ class ItemListAdapter( | |||||||
|     override var items: ArrayList<SelfossModel.Item>, |     override var items: ArrayList<SelfossModel.Item>, | ||||||
|     override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit |     override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit | ||||||
| ) : ItemsAdapter<ItemListAdapter.ViewHolder>() { | ) : ItemsAdapter<ItemListAdapter.ViewHolder>() { | ||||||
|     private val generator: ColorGenerator = ColorGenerator.MATERIAL |  | ||||||
|     private val c: Context = app.baseContext |     private val c: Context = app.baseContext | ||||||
|  |  | ||||||
|     override val di: DI by closestDI(app) |     override val di: DI by closestDI(app) | ||||||
| @@ -56,20 +51,12 @@ class ItemListAdapter( | |||||||
|             if (itm.getThumbnail(repository.baseUrl).isEmpty()) { |             if (itm.getThumbnail(repository.baseUrl).isEmpty()) { | ||||||
|  |  | ||||||
|                 if (itm.getIcon(repository.baseUrl).isEmpty()) { |                 if (itm.getIcon(repository.baseUrl).isEmpty()) { | ||||||
|                     val color = generator.getColor(itm.title.getHtmlDecoded()) |                     binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) | ||||||
|  |  | ||||||
|                     val drawable = |  | ||||||
|                             TextDrawable |  | ||||||
|                                     .builder() |  | ||||||
|                                     .round() |  | ||||||
|                                     .build(itm.title.getHtmlDecoded().toTextDrawableString(), color) |  | ||||||
|  |  | ||||||
|                     binding.itemImage.setImageDrawable(drawable) |  | ||||||
|                 } else { |                 } else { | ||||||
|                     c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) |                     c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                 c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage) |                 c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -10,17 +10,14 @@ import android.widget.Button | |||||||
| import android.widget.Toast | import android.widget.Toast | ||||||
| import androidx.constraintlayout.widget.ConstraintLayout | import androidx.constraintlayout.widget.ConstraintLayout | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity |  | ||||||
| import bou.amine.apps.readerforselfossv2.android.R | import bou.amine.apps.readerforselfossv2.android.R | ||||||
|  | import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity | ||||||
| import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding | import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding | ||||||
| import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString | import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable | ||||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable |  | ||||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | import bou.amine.apps.readerforselfossv2.repository.Repository | ||||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||||
| import com.amulyakhare.textdrawable.TextDrawable |  | ||||||
| import com.amulyakhare.textdrawable.util.ColorGenerator |  | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| @@ -31,10 +28,9 @@ import org.kodein.di.instance | |||||||
|  |  | ||||||
| class SourcesListAdapter( | class SourcesListAdapter( | ||||||
|     private val app: Activity, |     private val app: Activity, | ||||||
|     private val items: ArrayList<SelfossModel.Source> |     private val items: ArrayList<SelfossModel.SourceDetail> | ||||||
| ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware { | ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware { | ||||||
|     private val c: Context = app.baseContext |     private val c: Context = app.baseContext | ||||||
|     private val generator: ColorGenerator = ColorGenerator.MATERIAL |  | ||||||
|     private lateinit var binding: SourceListItemBinding |     private lateinit var binding: SourceListItemBinding | ||||||
|  |  | ||||||
|     override val di: DI by closestDI(app) |     override val di: DI by closestDI(app) | ||||||
| @@ -49,19 +45,12 @@ class SourcesListAdapter( | |||||||
|         val itm = items[position] |         val itm = items[position] | ||||||
|  |  | ||||||
|         if (itm.getIcon(repository.baseUrl).isEmpty()) { |         if (itm.getIcon(repository.baseUrl).isEmpty()) { | ||||||
|             val color = generator.getColor(itm.title.getHtmlDecoded()) |             binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded()) | ||||||
|  |  | ||||||
|             val drawable = |  | ||||||
|                 TextDrawable |  | ||||||
|                     .builder() |  | ||||||
|                     .round() |  | ||||||
|                     .build(itm.title.getHtmlDecoded().toTextDrawableString(), color) |  | ||||||
|             binding.itemImage.setImageDrawable(drawable) |  | ||||||
|         } else { |         } else { | ||||||
|             c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) |             c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (itm.error.isNotBlank()) { |         if (!itm.error.isNullOrBlank()) { | ||||||
|             binding.errorText.visibility = View.VISIBLE |             binding.errorText.visibility = View.VISIBLE | ||||||
|             binding.errorText.text = itm.error |             binding.errorText.text = itm.error | ||||||
|         } else { |         } else { | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| package bou.amine.apps.readerforselfossv2.android.fragments | package bou.amine.apps.readerforselfossv2.android.fragments | ||||||
|  |  | ||||||
|  | import android.content.ActivityNotFoundException | ||||||
| import android.content.Intent | import android.content.Intent | ||||||
| import android.content.res.ColorStateList | import android.content.res.ColorStateList | ||||||
| import android.content.res.TypedArray | import android.content.res.TypedArray | ||||||
| @@ -30,7 +31,6 @@ import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask | |||||||
| import bou.amine.apps.readerforselfossv2.android.utils.shareLink | import bou.amine.apps.readerforselfossv2.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 | ||||||
| @@ -150,6 +150,7 @@ class ArticleFragment : Fragment(), DIAware { | |||||||
|  |  | ||||||
|         } catch (e: InflateException) { |         } catch (e: InflateException) { | ||||||
|             e.sendSilentlyWithAcraWithName("webview not available") |             e.sendSilentlyWithAcraWithName("webview not available") | ||||||
|  |             if (context != null) { | ||||||
|                 AlertDialog.Builder(requireContext()) |                 AlertDialog.Builder(requireContext()) | ||||||
|                     .setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) |                     .setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) | ||||||
|                     .setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) |                     .setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) | ||||||
| @@ -162,6 +163,7 @@ class ArticleFragment : Fragment(), DIAware { | |||||||
|                     .create() |                     .create() | ||||||
|                     .show() |                     .show() | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return binding.root |         return binding.root | ||||||
|     } |     } | ||||||
| @@ -257,21 +259,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() | ||||||
|                 } |                 } | ||||||
| @@ -284,14 +273,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) | ||||||
| @@ -304,10 +314,16 @@ 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 { | ||||||
|                 if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { |                 return if (context != null && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { | ||||||
|  |                     try { | ||||||
|                         requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) |                         requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) | ||||||
|  |                     } catch (e: ActivityNotFoundException) { | ||||||
|  |                         e.sendSilentlyWithAcraWithName("activityNotFound > $url") | ||||||
|  |                     } | ||||||
|  |                     true | ||||||
|  |                 } else { | ||||||
|  |                     false | ||||||
|                 } |                 } | ||||||
|                 return true |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             @Deprecated("Deprecated in Java") |             @Deprecated("Deprecated in Java") | ||||||
| @@ -359,7 +375,7 @@ class ArticleFragment : Fragment(), DIAware { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun htmlToWebview() { |     private fun htmlToWebview() { | ||||||
|  |         if (context != null) { | ||||||
|             val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) |             val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) | ||||||
|             val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) |             val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) | ||||||
|  |  | ||||||
| @@ -397,7 +413,7 @@ class ArticleFragment : Fragment(), DIAware { | |||||||
|                 val itemUrl = URL(url) |                 val itemUrl = URL(url) | ||||||
|                 baseUrl = itemUrl.protocol + "://" + itemUrl.host |                 baseUrl = itemUrl.protocol + "://" + itemUrl.host | ||||||
|             } catch (e: MalformedURLException) { |             } catch (e: MalformedURLException) { | ||||||
|             e.sendSilentlyWithAcraWithName("htmlToWebview > item url") |                 e.sendSilentlyWithAcraWithName("htmlToWebview > $url") | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             val fontName = when (font) { |             val fontName = when (font) { | ||||||
| @@ -500,6 +516,7 @@ class ArticleFragment : Fragment(), DIAware { | |||||||
|                 null |                 null | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     fun scrollDown() { |     fun scrollDown() { | ||||||
|         val height = binding.nestedScrollView.measuredHeight |         val height = binding.nestedScrollView.measuredHeight | ||||||
| @@ -513,7 +530,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 { | ||||||
| @@ -531,8 +553,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) | ||||||
|   | |||||||
| @@ -20,10 +20,8 @@ import bou.amine.apps.readerforselfossv2.repository.Repository | |||||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||||
| import com.bumptech.glide.Glide | import com.bumptech.glide.Glide | ||||||
| import com.bumptech.glide.load.DataSource | import com.bumptech.glide.request.target.ViewTarget | ||||||
| import com.bumptech.glide.load.engine.GlideException | import com.bumptech.glide.request.transition.Transition | ||||||
| import com.bumptech.glide.request.RequestListener |  | ||||||
| import com.bumptech.glide.request.target.Target |  | ||||||
| import com.google.android.material.bottomsheet.BottomSheetDialogFragment | import com.google.android.material.bottomsheet.BottomSheetDialogFragment | ||||||
| import com.google.android.material.chip.Chip | import com.google.android.material.chip.Chip | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| @@ -84,37 +82,25 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | |||||||
|     ) { |     ) { | ||||||
|         val sourceGroup = binding.sourcesGroup |         val sourceGroup = binding.sourcesGroup | ||||||
|  |  | ||||||
|         repository.getSources().forEach { source -> |         repository.getSourcesDetailsOrStats().forEach { source -> | ||||||
|             val c = Chip(context) |             val c = Chip(context) | ||||||
|             c.ellipsize = TextUtils.TruncateAt.END |             c.ellipsize = TextUtils.TruncateAt.END | ||||||
|  |  | ||||||
|             Glide.with(context) |             Glide.with(context) | ||||||
|                 .load(source.getIcon(repository.baseUrl)) |                 .load(source.getIcon(repository.baseUrl)) | ||||||
|                 .listener(object : RequestListener<Drawable?> { |                 .into(object : ViewTarget<Chip?, Drawable?>(c) { | ||||||
|                     override fun onLoadFailed( |  | ||||||
|                         e: GlideException?, |  | ||||||
|                         model: Any?, |  | ||||||
|                         target: Target<Drawable?>?, |  | ||||||
|                         isFirstResource: Boolean |  | ||||||
|                     ): Boolean { |  | ||||||
|                         return false |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     override fun onResourceReady( |                     override fun onResourceReady( | ||||||
|                         resource: Drawable?, |                         resource: Drawable, | ||||||
|                         model: Any?, |                         transition: Transition<in Drawable?>? | ||||||
|                         target: Target<Drawable?>?, |                     ) { | ||||||
|                         dataSource: DataSource?, |  | ||||||
|                         isFirstResource: Boolean |  | ||||||
|                     ): Boolean { |  | ||||||
|                         try { |                         try { | ||||||
|                             c.chipIcon = resource |                             c.chipIcon = resource | ||||||
|                         } catch (e: Exception) { |                         } catch (e: Exception) { | ||||||
|                             e.sendSilentlyWithAcraWithName("sources > onResourceReady") |                             e.sendSilentlyWithAcraWithName("sources > onResourceReady") | ||||||
|                         } |                         } | ||||||
|                         return false |  | ||||||
|                     } |                     } | ||||||
|                 }).preload() |  | ||||||
|  |                 }) | ||||||
|  |  | ||||||
|             c.text = source.title.getHtmlDecoded() |             c.text = source.title.getHtmlDecoded() | ||||||
|  |  | ||||||
| @@ -141,9 +127,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | |||||||
|                 selectedChip = c |                 selectedChip = c | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             c.isEnabled = source.error.isBlank() |             c.isEnabled = source.error.isNullOrBlank() | ||||||
|  |  | ||||||
|             if (source.error.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |             if (!source.error.isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||||
|                 c.tooltipText = source.error |                 c.tooltipText = source.error | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -163,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 { | ||||||
| @@ -179,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 | ||||||
|   | |||||||
| @@ -0,0 +1,62 @@ | |||||||
|  | package bou.amine.apps.readerforselfossv2.android.utils | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.graphics.drawable.GradientDrawable | ||||||
|  | import android.util.AttributeSet | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.widget.RelativeLayout | ||||||
|  | import android.widget.TextView | ||||||
|  | import bou.amine.apps.readerforselfossv2.android.R | ||||||
|  | import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString | ||||||
|  | import com.google.android.material.imageview.ShapeableImageView | ||||||
|  | import kotlin.math.abs | ||||||
|  |  | ||||||
|  | class CircleImageView @JvmOverloads constructor( | ||||||
|  |     context: Context, | ||||||
|  |     attrs: AttributeSet? = null, | ||||||
|  |     defStyleAttr: Int = 0 | ||||||
|  | ) : RelativeLayout(context, attrs, defStyleAttr) { | ||||||
|  |     val view: View | ||||||
|  |     val imageView: ShapeableImageView | ||||||
|  |     val textView: TextView | ||||||
|  |  | ||||||
|  |     private val colorScheme = listOf( | ||||||
|  |     -0x1a8c8d, | ||||||
|  |     -0xf9d6e, | ||||||
|  |     -0x459738, | ||||||
|  |     -0x6a8a33, | ||||||
|  |     -0x867935, | ||||||
|  |     -0x9b4a0a, | ||||||
|  |     -0xb03c09, | ||||||
|  |     -0xb22f1f, | ||||||
|  |     -0xb24954, | ||||||
|  |     -0x7e387c, | ||||||
|  |     -0x512a7f, | ||||||
|  |     -0x759b, | ||||||
|  |     -0x2b1ea9, | ||||||
|  |     -0x2ab1, | ||||||
|  |     -0x48b3, | ||||||
|  |     -0x5e7781, | ||||||
|  |     -0x6f5b52 | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         view = LayoutInflater.from(context).inflate(R.layout.circle_image_view, this, true) | ||||||
|  |         imageView = view.findViewById(R.id.circleImage) | ||||||
|  |         textView = view.findViewById(R.id.circleText) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun setBackgroundAndText(text: String) { | ||||||
|  |         val circleDrawable = GradientDrawable() | ||||||
|  |         val color = colorFromIdentifier(text) | ||||||
|  |         circleDrawable.setColor(color) | ||||||
|  |         imageView.setImageDrawable(circleDrawable) | ||||||
|  |  | ||||||
|  |         textView.text = text.toTextDrawableString() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun colorFromIdentifier(key: String): Int { | ||||||
|  |         return colorScheme[abs(key.hashCode()) % colorScheme.size] | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -3,10 +3,9 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide | |||||||
| import android.content.Context | import android.content.Context | ||||||
| import android.graphics.Bitmap | import android.graphics.Bitmap | ||||||
| import android.widget.ImageView | import android.widget.ImageView | ||||||
| import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory | import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView | ||||||
| import com.bumptech.glide.Glide | import com.bumptech.glide.Glide | ||||||
| import com.bumptech.glide.request.RequestOptions | import com.bumptech.glide.request.RequestOptions | ||||||
| import com.bumptech.glide.request.target.BitmapImageViewTarget |  | ||||||
| import java.io.ByteArrayInputStream | import java.io.ByteArrayInputStream | ||||||
| import java.io.ByteArrayOutputStream | import java.io.ByteArrayOutputStream | ||||||
| import java.io.InputStream | import java.io.InputStream | ||||||
| @@ -18,21 +17,13 @@ fun Context.bitmapCenterCrop(url: String, iv: ImageView) = | |||||||
|         .apply(RequestOptions.centerCropTransform()) |         .apply(RequestOptions.centerCropTransform()) | ||||||
|         .into(iv) |         .into(iv) | ||||||
|  |  | ||||||
| fun Context.circularBitmapDrawable(url: String, iv: ImageView) = | fun Context.circularDrawable(url: String, view: CircleImageView) { | ||||||
|  |     view.textView.text ="" | ||||||
|  |  | ||||||
|     Glide.with(this) |     Glide.with(this) | ||||||
|         .asBitmap() |  | ||||||
|         .load(url) |         .load(url) | ||||||
|         .apply(RequestOptions.centerCropTransform()) |         .into(view.imageView) | ||||||
|         .into(object : BitmapImageViewTarget(iv) { | } | ||||||
|             override fun setResource(resource: Bitmap?) { |  | ||||||
|                 val circularBitmapDrawable = RoundedBitmapDrawableFactory.create( |  | ||||||
|                     resources, |  | ||||||
|                     resource |  | ||||||
|                 ) |  | ||||||
|                 circularBitmapDrawable.isCircular = true |  | ||||||
|                 iv.setImageDrawable(circularBitmapDrawable) |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|  |  | ||||||
| fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream { | fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream { | ||||||
|     val byteArrayOutputStream = ByteArrayOutputStream() |     val byteArrayOutputStream = ByteArrayOutputStream() | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ | |||||||
|             app:layout_constraintStart_toStartOf="parent" |             app:layout_constraintStart_toStartOf="parent" | ||||||
|             app:layout_constraintTop_toBottomOf="@+id/itemImage"> |             app:layout_constraintTop_toBottomOf="@+id/itemImage"> | ||||||
|  |  | ||||||
|             <ImageView |             <bou.amine.apps.readerforselfossv2.android.utils.CircleImageView | ||||||
|                 android:id="@+id/sourceImage" |                 android:id="@+id/sourceImage" | ||||||
|                 android:layout_width="40dp" |                 android:layout_width="40dp" | ||||||
|                 android:layout_height="40dp" |                 android:layout_height="40dp" | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								androidApp/src/main/res/layout/circle_image_view.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								androidApp/src/main/res/layout/circle_image_view.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     android:layout_width="wrap_content" | ||||||
|  |     android:layout_height="wrap_content"> | ||||||
|  |  | ||||||
|  |     <com.google.android.material.imageview.ShapeableImageView | ||||||
|  |         android:id="@+id/circleImage" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:scaleType="centerCrop" | ||||||
|  |         app:shapeAppearanceOverlay="@style/circleImageView" | ||||||
|  |         app:srcCompat="@drawable/background_splash" /> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/circleText" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:ellipsize="none" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:singleLine="true" | ||||||
|  |         android:textColor="@color/white" | ||||||
|  |         android:textIsSelectable="false" | ||||||
|  |         android:textSize="20sp" | ||||||
|  |         android:typeface="normal" /> | ||||||
|  | </RelativeLayout> | ||||||
| @@ -5,7 +5,7 @@ | |||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="wrap_content"> |     android:layout_height="wrap_content"> | ||||||
|  |  | ||||||
|     <ImageView |     <bou.amine.apps.readerforselfossv2.android.utils.CircleImageView | ||||||
|         android:id="@+id/itemImage" |         android:id="@+id/itemImage" | ||||||
|         android:layout_width="46dp" |         android:layout_width="46dp" | ||||||
|         android:layout_height="46dp" |         android:layout_height="46dp" | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ | |||||||
|         app:layout_constraintTop_toTopOf="parent" |         app:layout_constraintTop_toTopOf="parent" | ||||||
|         app:layout_constraintVertical_bias="0.0" /> |         app:layout_constraintVertical_bias="0.0" /> | ||||||
|  |  | ||||||
|     <ImageView |     <bou.amine.apps.readerforselfossv2.android.utils.CircleImageView | ||||||
|         android:id="@+id/itemImage" |         android:id="@+id/itemImage" | ||||||
|         android:layout_width="36dp" |         android:layout_width="36dp" | ||||||
|         android:layout_height="36dp" |         android:layout_height="36dp" | ||||||
|   | |||||||
| @@ -32,4 +32,10 @@ | |||||||
|         <item name="android:colorBackgroundCacheHint">@null</item> |         <item name="android:colorBackgroundCacheHint">@null</item> | ||||||
|         <item name="android:windowIsTranslucent">true</item> |         <item name="android:windowIsTranslucent">true</item> | ||||||
|     </style> |     </style> | ||||||
|  |  | ||||||
|  |     <style name="circleImageView" parent=""> | ||||||
|  |         <item name="cornerFamily">rounded</item> | ||||||
|  |         <item name="cornerSize">50%</item> | ||||||
|  |     </style> | ||||||
|  |  | ||||||
| </resources> | </resources> | ||||||
|   | |||||||
| @@ -300,9 +300,10 @@ class RepositoryTest { | |||||||
|         every { appSettingsService.isItemCachingEnabled() } returns true |         every { appSettingsService.isItemCachingEnabled() } returns true | ||||||
|  |  | ||||||
|         initializeRepository(MutableStateFlow(false)) |         initializeRepository(MutableStateFlow(false)) | ||||||
|         repository.setSourceFilter(SelfossModel.Source( |         repository.setSourceFilter(SelfossModel.SourceDetail( | ||||||
|             1, |             1, | ||||||
|             "Test", |             "Test", | ||||||
|  |             null, | ||||||
|             listOf("tags"), |             listOf("tags"), | ||||||
|             SPOUT, |             SPOUT, | ||||||
|             "", |             "", | ||||||
| @@ -609,30 +610,32 @@ class RepositoryTest { | |||||||
|     fun get_sources() { |     fun get_sources() { | ||||||
|         val (sources, sourcesDB) = prepareSources() |         val (sources, sourcesDB) = prepareSources() | ||||||
|         initializeRepository() |         initializeRepository() | ||||||
|         var testSources: List<SelfossModel.Source>? |         var testSources: List<SelfossModel.Source> | ||||||
|         runBlocking { |         runBlocking { | ||||||
|             testSources = repository.getSources() |             testSources = repository.getSourcesDetails() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         assertSame(sources, testSources) |         assertEquals(sources, testSources) | ||||||
|         assertNotEquals(sourcesDB.map { it.toView() }, testSources) |         assertNotEquals(sourcesDB.map { it.toView() }, testSources) | ||||||
|         coVerify(exactly = 1) { api.sources() } |         coVerify(exactly = 1) { api.sourcesDetailed() } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun prepareSources(): Pair<ArrayList<SelfossModel.Source>, List<SOURCE>> { |     private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> { | ||||||
|         val sources = arrayListOf( |         val sources = arrayListOf( | ||||||
|             SelfossModel.Source( |             SelfossModel.SourceDetail( | ||||||
|                 1, |                 1, | ||||||
|                 "First source", |                 "First source", | ||||||
|  |                 null, | ||||||
|                 listOf("Test", "second"), |                 listOf("Test", "second"), | ||||||
|                 SPOUT, |                 SPOUT, | ||||||
|                 "", |                 "", | ||||||
|                 IMAGE_URL_2, |                 IMAGE_URL_2, | ||||||
|                 SelfossModel.SourceParams("url") |                 SelfossModel.SourceParams("url") | ||||||
|             ), |             ), | ||||||
|             SelfossModel.Source( |             SelfossModel.SourceDetail( | ||||||
|                 2, |                 2, | ||||||
|                 "Second source", |                 "Second source", | ||||||
|  |                 null, | ||||||
|                 listOf("second"), |                 listOf("second"), | ||||||
|                 SPOUT, |                 SPOUT, | ||||||
|                 "", |                 "", | ||||||
| @@ -661,7 +664,7 @@ class RepositoryTest { | |||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) |         coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources) | ||||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB |         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||||
|         return Pair(sources, sourcesDB) |         return Pair(sources, sourcesDB) | ||||||
|     } |     } | ||||||
| @@ -675,13 +678,13 @@ class RepositoryTest { | |||||||
|         initializeRepository() |         initializeRepository() | ||||||
|         var testSources: List<SelfossModel.Source>? |         var testSources: List<SelfossModel.Source>? | ||||||
|         runBlocking { |         runBlocking { | ||||||
|             testSources = repository.getSources() |             testSources = repository.getSourcesDetails() | ||||||
|             // Sources will be fetched from the database on the second call, thus testSources != sources |             // Sources will be fetched from the database on the second call, thus testSources != sources | ||||||
|             testSources = repository.getSources() |             testSources = repository.getSourcesDetails() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         coVerify(exactly = 1) { api.sources() } |         coVerify(exactly = 1) { api.sourcesDetailed() } | ||||||
|         assertNotSame(sources, testSources) |         assertNotEquals(sources, testSources) | ||||||
|         assertEquals(sourcesDB.map { it.toView() }, testSources) |         assertEquals(sourcesDB.map { it.toView() }, testSources) | ||||||
|         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } |         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } | ||||||
|     } |     } | ||||||
| @@ -693,13 +696,13 @@ class RepositoryTest { | |||||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true |         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||||
|         every { appSettingsService.isItemCachingEnabled() } returns false |         every { appSettingsService.isItemCachingEnabled() } returns false | ||||||
|         initializeRepository() |         initializeRepository() | ||||||
|         var testSources: List<SelfossModel.Source>? |         var testSources: List<SelfossModel.Source> | ||||||
|         runBlocking { |         runBlocking { | ||||||
|             testSources = repository.getSources() |             testSources = repository.getSourcesDetails() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         assertSame(sources, testSources) |         assertEquals(sources, testSources) | ||||||
|         coVerify(exactly = 1) { api.sources() } |         coVerify(exactly = 1) { api.sourcesDetailed() } | ||||||
|         verify(exactly = 0) { db.sourcesQueries } |         verify(exactly = 0) { db.sourcesQueries } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -710,13 +713,13 @@ class RepositoryTest { | |||||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false |         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||||
|         every { appSettingsService.isItemCachingEnabled() } returns false |         every { appSettingsService.isItemCachingEnabled() } returns false | ||||||
|         initializeRepository() |         initializeRepository() | ||||||
|         var testSources: List<SelfossModel.Source>? |         var testSources: List<SelfossModel.Source> | ||||||
|         runBlocking { |         runBlocking { | ||||||
|             testSources = repository.getSources() |             testSources = repository.getSourcesDetails() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         assertSame(sources, testSources) |         assertEquals(sources, testSources) | ||||||
|         coVerify(exactly = 1) { api.sources() } |         coVerify(exactly = 1) { api.sourcesDetailed() } | ||||||
|         verify(atLeast = 1) { db.sourcesQueries } |         verify(atLeast = 1) { db.sourcesQueries } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -724,13 +727,13 @@ class RepositoryTest { | |||||||
|     fun get_sources_without_connection() { |     fun get_sources_without_connection() { | ||||||
|         val (_, sourcesDB) = prepareSources() |         val (_, sourcesDB) = prepareSources() | ||||||
|         initializeRepository(MutableStateFlow(false)) |         initializeRepository(MutableStateFlow(false)) | ||||||
|         var testSources: List<SelfossModel.Source>? |         var testSources: List<SelfossModel.Source> | ||||||
|         runBlocking { |         runBlocking { | ||||||
|             testSources = repository.getSources() |             testSources = repository.getSourcesDetails() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         assertEquals(sourcesDB.map { it.toView() }, testSources) |         assertEquals(sourcesDB.map { it.toView() }, testSources) | ||||||
|         coVerify(exactly = 0) { api.sources() } |         coVerify(exactly = 0) { api.sourcesDetailed() } | ||||||
|         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } |         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -741,13 +744,13 @@ class RepositoryTest { | |||||||
|         every { appSettingsService.isItemCachingEnabled() } returns false |         every { appSettingsService.isItemCachingEnabled() } returns false | ||||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true |         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||||
|         initializeRepository(MutableStateFlow(false)) |         initializeRepository(MutableStateFlow(false)) | ||||||
|         var testSources: List<SelfossModel.Source>? |         var testSources: List<SelfossModel.Source> | ||||||
|         runBlocking { |         runBlocking { | ||||||
|             testSources = repository.getSources() |             testSources = repository.getSourcesDetails() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         assertEquals(emptyList<SelfossModel.Source>(), testSources) |         assertEquals(emptyList<SelfossModel.Source>(), testSources) | ||||||
|         coVerify(exactly = 0) { api.sources() } |         coVerify(exactly = 0) { api.sourcesDetailed() } | ||||||
|         verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() } |         verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -758,13 +761,13 @@ class RepositoryTest { | |||||||
|         every { appSettingsService.isItemCachingEnabled() } returns true |         every { appSettingsService.isItemCachingEnabled() } returns true | ||||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false |         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||||
|         initializeRepository(MutableStateFlow(false)) |         initializeRepository(MutableStateFlow(false)) | ||||||
|         var testSources: List<SelfossModel.Source>? |         var testSources: List<SelfossModel.Source> | ||||||
|         runBlocking { |         runBlocking { | ||||||
|             testSources = repository.getSources() |             testSources = repository.getSourcesDetails() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         assertEquals(sourcesDB.map { it.toView() }, testSources) |         assertEquals(sourcesDB.map { it.toView() }, testSources) | ||||||
|         coVerify(exactly = 0) { api.sources() } |         coVerify(exactly = 0) { api.sourcesDetailed() } | ||||||
|         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } |         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -775,13 +778,13 @@ class RepositoryTest { | |||||||
|         every { appSettingsService.isItemCachingEnabled() } returns false |         every { appSettingsService.isItemCachingEnabled() } returns false | ||||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false |         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||||
|         initializeRepository(MutableStateFlow(false)) |         initializeRepository(MutableStateFlow(false)) | ||||||
|         var testSources: List<SelfossModel.Source>? |         var testSources: List<SelfossModel.Source> | ||||||
|         runBlocking { |         runBlocking { | ||||||
|             testSources = repository.getSources() |             testSources = repository.getSourcesDetails() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         assertEquals(sourcesDB.map { it.toView() }, testSources) |         assertEquals(sourcesDB.map { it.toView() }, testSources) | ||||||
|         coVerify(exactly = 0) { api.sources() } |         coVerify(exactly = 0) { api.sourcesDetailed() } | ||||||
|         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } |         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -1102,9 +1105,10 @@ class RepositoryTest { | |||||||
|     private fun prepareSearch() { |     private fun prepareSearch() { | ||||||
|         repository.setTagFilter(SelfossModel.Tag("Tag", "read", 0)) |         repository.setTagFilter(SelfossModel.Tag("Tag", "read", 0)) | ||||||
|         repository.setSourceFilter( |         repository.setSourceFilter( | ||||||
|             SelfossModel.Source( |             SelfossModel.SourceDetail( | ||||||
|                 1, |                 1, | ||||||
|                 "First source", |                 "First source", | ||||||
|  |                 5, | ||||||
|                 listOf("Test", "second"), |                 listOf("Test", "second"), | ||||||
|                 SPOUT, |                 SPOUT, | ||||||
|                 "", |                 "", | ||||||
|   | |||||||
| @@ -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 | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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,29 +54,48 @@ 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 | ||||||
|  |  | ||||||
|         fun isPublicModeEnabled() = publicMode ?: false |         fun isPublicModeEnabled() = publicMode ?: false | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     interface Source { | ||||||
|  |         val id: Int | ||||||
|  |         var title: String | ||||||
|  |         var unread: Int? | ||||||
|  |         var error: String? | ||||||
|  |         var icon: String? | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Serializable |     @Serializable | ||||||
|     data class Source( |     data class SourceStats( | ||||||
|         val id: Int, |         override val id: Int, | ||||||
|         val title: String, |         override var title: String, | ||||||
|  |         override var unread: Int? = null, | ||||||
|  |         override var error: String? = null, | ||||||
|  |         override var icon: String? = null | ||||||
|  |         ) : Source | ||||||
|  |  | ||||||
|  |     @Serializable | ||||||
|  |     data class SourceDetail( | ||||||
|  |         override val id: Int, | ||||||
|  |         override var title: String, | ||||||
|  |         override var unread: Int? = null, | ||||||
|         @Serializable(with = TagsListSerializer::class) |         @Serializable(with = TagsListSerializer::class) | ||||||
|         val tags: List<String>, |         var tags: List<String>? = null, | ||||||
|         val spout: String, |         var spout: String? = null, | ||||||
|         val error: String, |         override var error: String? = null, | ||||||
|         val icon: String?, |         override var icon: String? = null, | ||||||
|         val params: SourceParams? |         var params: SourceParams? = null | ||||||
|     ) |     ) : Source | ||||||
|  |  | ||||||
|     @Serializable |     @Serializable | ||||||
|     data class SourceParams( |     data class SourceParams( | ||||||
|         val url: String? |         val url: String? = null | ||||||
|     ) |     ) | ||||||
|     @Serializable |     @Serializable | ||||||
|     data class Item( |     data class Item( | ||||||
| @@ -88,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 | ||||||
|   | |||||||
| @@ -44,11 +44,11 @@ class Repository( | |||||||
|     private val _badgeStarred = MutableStateFlow(0) |     private val _badgeStarred = MutableStateFlow(0) | ||||||
|     val badgeStarred = _badgeStarred.asStateFlow() |     val badgeStarred = _badgeStarred.asStateFlow() | ||||||
|  |  | ||||||
|     private var fetchedSources = false |  | ||||||
|     private var fetchedTags = false |     private var fetchedTags = false | ||||||
|  |     private var fetchedSources = false | ||||||
|  |  | ||||||
|     private var _readerItems = ArrayList<SelfossModel.Item>() |     private var _readerItems = ArrayList<SelfossModel.Item>() | ||||||
|     private var _selectedSource: SelfossModel.Source? = null |     private var _selectedSource: SelfossModel.SourceDetail? = null | ||||||
|  |  | ||||||
|     suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { |     suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { | ||||||
|         var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() |         var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() | ||||||
| @@ -132,9 +132,9 @@ class Repository( | |||||||
|         if (isNetworkAvailable()) { |         if (isNetworkAvailable()) { | ||||||
|             val response = api.stats() |             val response = api.stats() | ||||||
|             if (response.success && response.data != null) { |             if (response.success && response.data != null) { | ||||||
|                 _badgeUnread.value = response.data.unread |                 _badgeUnread.value = response.data.unread ?: 0 | ||||||
|                 _badgeAll.value = response.data.total |                 _badgeAll.value = response.data.total | ||||||
|                 _badgeStarred.value = response.data.starred |                 _badgeStarred.value = response.data.starred ?: 0 | ||||||
|                 success = true |                 success = true | ||||||
|             } |             } | ||||||
|         } else if (appSettingsService.isItemCachingEnabled()) { |         } else if (appSettingsService.isItemCachingEnabled()) { | ||||||
| @@ -180,23 +180,46 @@ class Repository( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     suspend fun getSources(): ArrayList<SelfossModel.Source> { |     suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> { | ||||||
|  |         var sources = ArrayList<SelfossModel.Source>() | ||||||
|         val isDatabaseEnabled = |         val isDatabaseEnabled = | ||||||
|             appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() |             appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() | ||||||
|         return if (isNetworkAvailable() && !fetchedSources) { |         val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true | ||||||
|             val apiSources = api.sources() |         if (shouldFetch && isNetworkAvailable()) { | ||||||
|             if (apiSources.success && apiSources.data != null && isDatabaseEnabled) { |             if (appSettingsService.getPublicAccess()) { | ||||||
|                 resetDBSourcesWithData(apiSources.data) |                 val apiSources = api.sourcesStats() | ||||||
|                 if (!appSettingsService.isUpdateSourcesEnabled()) { |                 if (apiSources.success && apiSources.data != null) { | ||||||
|                     fetchedSources = true |                     fetchedSources = true | ||||||
|  |                     sources = apiSources.data as ArrayList<SelfossModel.Source> | ||||||
|                 } |                 } | ||||||
|             } |  | ||||||
|             apiSources.data ?: ArrayList() |  | ||||||
|         } else if (isDatabaseEnabled) { |  | ||||||
|             ArrayList(getDBSources().map { it.toView() }) |  | ||||||
|             } else { |             } else { | ||||||
|             ArrayList() |                 sources = getSourcesDetails() as ArrayList<SelfossModel.Source> | ||||||
|             } |             } | ||||||
|  |         } else if (isDatabaseEnabled) { | ||||||
|  |             sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.Source> | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return sources | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     suspend fun getSourcesDetails(): ArrayList<SelfossModel.SourceDetail> { | ||||||
|  |         var sources = ArrayList<SelfossModel.SourceDetail>() | ||||||
|  |         val isDatabaseEnabled = | ||||||
|  |             appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() | ||||||
|  |         val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true | ||||||
|  |         if (shouldFetch && isNetworkAvailable()) { | ||||||
|  |             val apiSources = api.sourcesDetailed() | ||||||
|  |             if (apiSources.success && apiSources.data != null) { | ||||||
|  |                 fetchedSources = true | ||||||
|  |                 sources = apiSources.data | ||||||
|  |                 if (isDatabaseEnabled) { | ||||||
|  |                     resetDBSourcesWithData(sources) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else if (isDatabaseEnabled) { | ||||||
|  |             sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.SourceDetail> | ||||||
|  |         } | ||||||
|  |         return sources | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     suspend fun markAsRead(item: SelfossModel.Item): Boolean { |     suspend fun markAsRead(item: SelfossModel.Item): Boolean { | ||||||
| @@ -482,7 +505,7 @@ class Repository( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun resetDBSourcesWithData(sources: List<SelfossModel.Source>) { |     private fun resetDBSourcesWithData(sources: List<SelfossModel.SourceDetail>) { | ||||||
|         db.sourcesQueries.deleteAllSources() |         db.sourcesQueries.deleteAllSources() | ||||||
|  |  | ||||||
|         db.sourcesQueries.transaction { |         db.sourcesQueries.transaction { | ||||||
| @@ -592,7 +615,7 @@ class Repository( | |||||||
|         ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1) |         ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun setSelectedSource(source: SelfossModel.Source) { |     fun setSelectedSource(source: SelfossModel.SourceDetail) { | ||||||
|         _selectedSource = source |         _selectedSource = source | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -600,7 +623,7 @@ class Repository( | |||||||
|         _selectedSource = null |         _selectedSource = null | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun getSelectedSource(): SelfossModel.Source? { |     fun getSelectedSource(): SelfossModel.SourceDetail? { | ||||||
|         return _selectedSource |         return _selectedSource | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -7,6 +7,7 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService | |||||||
| import io.github.aakira.napier.Napier | import io.github.aakira.napier.Napier | ||||||
| import io.ktor.client.* | import io.ktor.client.* | ||||||
| import io.ktor.client.plugins.* | import io.ktor.client.plugins.* | ||||||
|  | import io.ktor.client.plugins.auth.providers.* | ||||||
| import io.ktor.client.plugins.cache.* | import io.ktor.client.plugins.cache.* | ||||||
| import io.ktor.client.plugins.contentnegotiation.* | import io.ktor.client.plugins.contentnegotiation.* | ||||||
| import io.ktor.client.plugins.cookies.* | import io.ktor.client.plugins.cookies.* | ||||||
| @@ -15,6 +16,9 @@ import io.ktor.client.request.* | |||||||
| import io.ktor.client.statement.* | import io.ktor.client.statement.* | ||||||
| import io.ktor.http.* | import io.ktor.http.* | ||||||
| import io.ktor.serialization.kotlinx.json.* | import io.ktor.serialization.kotlinx.json.* | ||||||
|  | import io.ktor.util.* | ||||||
|  | import io.ktor.utils.io.charsets.* | ||||||
|  | import io.ktor.utils.io.core.* | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| @@ -32,6 +36,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | |||||||
|                     prettyPrint = true |                     prettyPrint = true | ||||||
|                     isLenient = true |                     isLenient = true | ||||||
|                     ignoreUnknownKeys = true |                     ignoreUnknownKeys = true | ||||||
|  |                     explicitNulls = false | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|             install(Logging) { |             install(Logging) { | ||||||
| @@ -63,6 +68,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | |||||||
|             expectSuccess = false |             expectSuccess = false | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|         return client |         return client | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -74,6 +80,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | |||||||
|         client = createHttpClient() |         client = createHttpClient() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fun constructBasicAuthValue(credentials: BasicAuthCredentials): String { | ||||||
|  |         val authString = "${credentials.username}:${credentials.password}" | ||||||
|  |         val authBuf = authString.toByteArray(Charsets.UTF_8).encodeBase64() | ||||||
|  |  | ||||||
|  |         return "Basic $authBuf" | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Api version was introduces after the POST login, so when there is a version, it should be available |     // 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 shouldHavePostLogin() = appSettingsService.getApiVersion() != -1 | ||||||
|     private fun hasLoginInfo() = |     private fun hasLoginInfo() = | ||||||
| @@ -96,11 +109,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 +139,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 +178,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 +194,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 +208,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 +222,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 +236,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,18 +250,51 @@ 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 sources(): StatusAndData<ArrayList<SelfossModel.Source>> = |     suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> = | ||||||
|  |         bodyOrFailure(client.tryToGet(url("/sources/stats")) { | ||||||
|  |             if (!shouldHavePostLogin()) { | ||||||
|  |                 parameter("username", appSettingsService.getUserName()) | ||||||
|  |                 parameter("password", appSettingsService.getPassword()) | ||||||
|  |             } | ||||||
|  |             if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { | ||||||
|  |                 headers { | ||||||
|  |                     append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |     suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> = | ||||||
|         bodyOrFailure(client.tryToGet(url("/sources/list")) { |         bodyOrFailure(client.tryToGet(url("/sources/list")) { | ||||||
|             if (!shouldHavePostLogin()) { |             if (!shouldHavePostLogin()) { | ||||||
|                 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")) { | ||||||
| @@ -200,6 +302,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 = | ||||||
| @@ -208,6 +316,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 = | ||||||
| @@ -216,6 +330,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 = | ||||||
| @@ -224,6 +344,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 = | ||||||
| @@ -235,6 +361,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())) | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         )) |         )) | ||||||
|  |  | ||||||
| @@ -270,6 +404,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())) | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -307,6 +449,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())) | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -316,5 +466,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() | ||||||
|  |                             ) | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         }) |         }) | ||||||
| } | } | ||||||
| @@ -10,7 +10,9 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { | |||||||
|     private var _publicAccess: Boolean? = null |     private var _publicAccess: 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 | ||||||
| @@ -96,6 +98,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 +165,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,6 +378,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { | |||||||
|     fun refreshApiSettings() { |     fun refreshApiSettings() { | ||||||
|         refreshPassword() |         refreshPassword() | ||||||
|         refreshUsername() |         refreshUsername() | ||||||
|  |         refreshBasicUsername() | ||||||
|  |         refreshBasicPassword() | ||||||
|         refreshBaseUrl() |         refreshBaseUrl() | ||||||
|         refreshApiVersion() |         refreshApiVersion() | ||||||
|         refreshPublicAccess() |         refreshPublicAccess() | ||||||
| @@ -387,7 +413,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 +433,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() | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -440,6 +478,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 +512,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" | ||||||
|   | |||||||
| @@ -12,24 +12,25 @@ fun TAG.toView(): SelfossModel.Tag = | |||||||
|         this.unread.toInt() |         this.unread.toInt() | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| fun SOURCE.toView(): SelfossModel.Source = | fun SOURCE.toView(): SelfossModel.SourceDetail = | ||||||
|     SelfossModel.Source( |     SelfossModel.SourceDetail( | ||||||
|         this.id.toInt(), |         this.id.toInt(), | ||||||
|         this.title, |         this.title, | ||||||
|         this.tags.split(","), |         null, | ||||||
|  |         this.tags?.split(","), | ||||||
|         this.spout, |         this.spout, | ||||||
|         this.error, |         this.error, | ||||||
|         this.icon, |         this.icon, | ||||||
|         SelfossModel.SourceParams(this.url) |         if (this.url != null) SelfossModel.SourceParams(this.url) else null | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| fun SelfossModel.Source.toEntity(): SOURCE = | fun SelfossModel.SourceDetail.toEntity(): SOURCE = | ||||||
|     SOURCE( |     SOURCE( | ||||||
|         this.id.toString(), |         this.id.toString(), | ||||||
|         this.title.getHtmlDecoded(), |         this.title.getHtmlDecoded(), | ||||||
|         this.tags.joinToString(","), |         this.tags?.joinToString(",").orEmpty(), | ||||||
|         this.spout, |         this.spout.orEmpty(), | ||||||
|         this.error, |         this.error.orEmpty(), | ||||||
|         this.icon.orEmpty(), |         this.icon.orEmpty(), | ||||||
|         this.params?.url |         this.params?.url | ||||||
|     ) |     ) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user