From e21906e70de780e73d3f0d5353981dbfe5fd16c4 Mon Sep 17 00:00:00 2001 From: davidoskky Date: Mon, 13 Mar 2023 16:26:54 +0000 Subject: [PATCH] feat: Use /sources/stats in the home (#133) ## Types of changes - [x] I have read the **CONTRIBUTING** document. - [x] My code follows the code style of this project. - [ ] I have updated the documentation accordingly. - [x] I have added tests to cover my changes. - [x] All new and existing tests passed. - [x] This is **NOT** translation related. This is implements feature #131 and it will allow implementing #132 With this, public mode functions perfectly and also has source filtering. Co-authored-by: davidoskky Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/133 Co-authored-by: davidoskky Co-committed-by: davidoskky --- .../android/SourcesActivity.kt | 4 +- .../android/UpsertSourceActivity.kt | 4 +- .../android/adapters/SourcesListAdapter.kt | 4 +- .../android/fragments/FilterSheetFragment.kt | 6 +- androidApp/src/test/kotlin/RepositoryTest.kt | 72 ++++++++++--------- .../readerforselfossv2/model/SelfossModel.kt | 37 +++++++--- .../repository/RepositoryImpl.kt | 53 ++++++++++---- .../readerforselfossv2/rest/SelfossApi.kt | 10 ++- .../readerforselfossv2/utils/EntityUtils.kt | 15 ++-- 9 files changed, 130 insertions(+), 75 deletions(-) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt index d8019a0..ca47080 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt @@ -49,13 +49,13 @@ class SourcesActivity : AppCompatActivity(), DIAware { super.onResume() val mLayoutManager = LinearLayoutManager(this) - var items: ArrayList + var items: ArrayList binding.recyclerView.setHasFixedSize(true) binding.recyclerView.layoutManager = mLayoutManager CoroutineScope(Dispatchers.Main).launch { - val response = repository.getSources() + val response = repository.getSourcesDetails() if (response.isNotEmpty()) { items = response val mAdapter = SourcesListAdapter( diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/UpsertSourceActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/UpsertSourceActivity.kt index 882ff1b..c24babe 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/UpsertSourceActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/UpsertSourceActivity.kt @@ -24,7 +24,7 @@ import org.kodein.di.instance class UpsertSourceActivity : AppCompatActivity(), DIAware { - private var existingSource: SelfossModel.Source? = null + private var existingSource: SelfossModel.SourceDetail? = null private var mSpoutsValue: String? = null private lateinit var binding: ActivityUpsertSourceBinding @@ -68,7 +68,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware { private fun initFields(items: Map) { binding.nameInput.setText(existingSource!!.title) - binding.tags.setText(existingSource!!.tags.joinToString(", ")) + binding.tags.setText(existingSource!!.tags?.joinToString(", ")) binding.sourceUri.setText(existingSource!!.params?.url) binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout)) binding.progress.visibility = View.GONE diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt index 4bf3436..2eba4c1 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt @@ -31,7 +31,7 @@ import org.kodein.di.instance class SourcesListAdapter( private val app: Activity, - private val items: ArrayList + private val items: ArrayList ) : RecyclerView.Adapter(), DIAware { private val c: Context = app.baseContext private val generator: ColorGenerator = ColorGenerator.MATERIAL @@ -61,7 +61,7 @@ class SourcesListAdapter( c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) } - if (itm.error.isNotBlank()) { + if (!itm.error.isNullOrBlank()) { binding.errorText.visibility = View.VISIBLE binding.errorText.text = itm.error } else { diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt index c9e51fd..4c0586e 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt @@ -82,7 +82,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { ) { val sourceGroup = binding.sourcesGroup - repository.getSources().forEach { source -> + repository.getSourcesDetailsOrStats().forEach { source -> val c = Chip(context) c.ellipsize = TextUtils.TruncateAt.END @@ -127,9 +127,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { selectedChip = c } - c.isEnabled = source.error.isBlank() + c.isEnabled = source.error.isNullOrBlank() - if (source.error.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (!source.error.isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { c.tooltipText = source.error } diff --git a/androidApp/src/test/kotlin/RepositoryTest.kt b/androidApp/src/test/kotlin/RepositoryTest.kt index ec307b9..78b6ca9 100644 --- a/androidApp/src/test/kotlin/RepositoryTest.kt +++ b/androidApp/src/test/kotlin/RepositoryTest.kt @@ -300,9 +300,10 @@ class RepositoryTest { every { appSettingsService.isItemCachingEnabled() } returns true initializeRepository(MutableStateFlow(false)) - repository.setSourceFilter(SelfossModel.Source( + repository.setSourceFilter(SelfossModel.SourceDetail( 1, "Test", + null, listOf("tags"), SPOUT, "", @@ -609,30 +610,32 @@ class RepositoryTest { fun get_sources() { val (sources, sourcesDB) = prepareSources() initializeRepository() - var testSources: List? + var testSources: List runBlocking { - testSources = repository.getSources() + testSources = repository.getSourcesDetails() } - assertSame(sources, testSources) + assertEquals(sources, testSources) assertNotEquals(sourcesDB.map { it.toView() }, testSources) - coVerify(exactly = 1) { api.sources() } + coVerify(exactly = 1) { api.sourcesDetailed() } } - private fun prepareSources(): Pair, List> { + private fun prepareSources(): Pair, List> { val sources = arrayListOf( - SelfossModel.Source( + SelfossModel.SourceDetail( 1, "First source", + null, listOf("Test", "second"), SPOUT, "", IMAGE_URL_2, SelfossModel.SourceParams("url") ), - SelfossModel.Source( + SelfossModel.SourceDetail( 2, "Second source", + null, listOf("second"), SPOUT, "", @@ -661,7 +664,7 @@ class RepositoryTest { ) ) - coEvery { api.sources() } returns StatusAndData(success = true, data = sources) + coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources) every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB return Pair(sources, sourcesDB) } @@ -675,13 +678,13 @@ class RepositoryTest { initializeRepository() var testSources: List? runBlocking { - testSources = repository.getSources() + testSources = repository.getSourcesDetails() // Sources will be fetched from the database on the second call, thus testSources != sources - testSources = repository.getSources() + testSources = repository.getSourcesDetails() } - coVerify(exactly = 1) { api.sources() } - assertNotSame(sources, testSources) + coVerify(exactly = 1) { api.sourcesDetailed() } + assertNotEquals(sources, testSources) assertEquals(sourcesDB.map { it.toView() }, testSources) verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } } @@ -693,13 +696,13 @@ class RepositoryTest { every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns false initializeRepository() - var testSources: List? + var testSources: List runBlocking { - testSources = repository.getSources() + testSources = repository.getSourcesDetails() } - assertSame(sources, testSources) - coVerify(exactly = 1) { api.sources() } + assertEquals(sources, testSources) + coVerify(exactly = 1) { api.sourcesDetailed() } verify(exactly = 0) { db.sourcesQueries } } @@ -710,13 +713,13 @@ class RepositoryTest { every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false initializeRepository() - var testSources: List? + var testSources: List runBlocking { - testSources = repository.getSources() + testSources = repository.getSourcesDetails() } - assertSame(sources, testSources) - coVerify(exactly = 1) { api.sources() } + assertEquals(sources, testSources) + coVerify(exactly = 1) { api.sourcesDetailed() } verify(atLeast = 1) { db.sourcesQueries } } @@ -724,13 +727,13 @@ class RepositoryTest { fun get_sources_without_connection() { val (_, sourcesDB) = prepareSources() initializeRepository(MutableStateFlow(false)) - var testSources: List? + var testSources: List runBlocking { - testSources = repository.getSources() + testSources = repository.getSourcesDetails() } assertEquals(sourcesDB.map { it.toView() }, testSources) - coVerify(exactly = 0) { api.sources() } + coVerify(exactly = 0) { api.sourcesDetailed() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } } @@ -741,13 +744,13 @@ class RepositoryTest { every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns true initializeRepository(MutableStateFlow(false)) - var testSources: List? + var testSources: List runBlocking { - testSources = repository.getSources() + testSources = repository.getSourcesDetails() } assertEquals(emptyList(), testSources) - coVerify(exactly = 0) { api.sources() } + coVerify(exactly = 0) { api.sourcesDetailed() } verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() } } @@ -758,13 +761,13 @@ class RepositoryTest { every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns false initializeRepository(MutableStateFlow(false)) - var testSources: List? + var testSources: List runBlocking { - testSources = repository.getSources() + testSources = repository.getSourcesDetails() } assertEquals(sourcesDB.map { it.toView() }, testSources) - coVerify(exactly = 0) { api.sources() } + coVerify(exactly = 0) { api.sourcesDetailed() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } } @@ -775,13 +778,13 @@ class RepositoryTest { every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false initializeRepository(MutableStateFlow(false)) - var testSources: List? + var testSources: List runBlocking { - testSources = repository.getSources() + testSources = repository.getSourcesDetails() } assertEquals(sourcesDB.map { it.toView() }, testSources) - coVerify(exactly = 0) { api.sources() } + coVerify(exactly = 0) { api.sourcesDetailed() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } } @@ -1102,9 +1105,10 @@ class RepositoryTest { private fun prepareSearch() { repository.setTagFilter(SelfossModel.Tag("Tag", "read", 0)) repository.setSourceFilter( - SelfossModel.Source( + SelfossModel.SourceDetail( 1, "First source", + 5, listOf("Test", "second"), SPOUT, "", diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt index 4d68efc..eebfb77 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt @@ -63,17 +63,36 @@ class SelfossModel { fun isPublicModeEnabled() = publicMode ?: false } + interface Source { + val id: Int + var title: String + var unread: Int? + var error: String? + var icon: String? + } + @Serializable - data class Source( - val id: Int, - val title: String, + data class SourceStats( + override val id: Int, + override var title: String, + override var unread: Int?, + override var error: String? = null, + override var icon: String? = null + ) : Source + + @Serializable + data class SourceDetail( + override val id: Int, + override var title: String, + override var unread: Int? = null, @Serializable(with = TagsListSerializer::class) - val tags: List, - val spout: String, - val error: String, - val icon: String?, - val params: SourceParams? - ) + var tags: List?, + var spout: String?, + override var error: String?, + override var icon: String?, + var params: SourceParams? + ) : Source + @Serializable data class SourceParams( val url: String? = null diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt index 940e3d1..8d6cff4 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt @@ -44,11 +44,11 @@ class Repository( private val _badgeStarred = MutableStateFlow(0) val badgeStarred = _badgeStarred.asStateFlow() - private var fetchedSources = false private var fetchedTags = false + private var fetchedSources = false private var _readerItems = ArrayList() - private var _selectedSource: SelfossModel.Source? = null + private var _selectedSource: SelfossModel.SourceDetail? = null suspend fun getNewerItems(): ArrayList { var fetchedItems: StatusAndData> = StatusAndData.error() @@ -180,23 +180,46 @@ class Repository( } } - suspend fun getSources(): ArrayList { + suspend fun getSourcesDetailsOrStats(): ArrayList { + var sources = ArrayList() val isDatabaseEnabled = appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() - return if (isNetworkAvailable() && !fetchedSources) { - val apiSources = api.sources() - if (apiSources.success && apiSources.data != null && isDatabaseEnabled) { - resetDBSourcesWithData(apiSources.data) - if (!appSettingsService.isUpdateSourcesEnabled()) { + val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true + if (shouldFetch && isNetworkAvailable()) { + if (appSettingsService.getPublicAccess()) { + val apiSources = api.sourcesStats() + if (apiSources.success && apiSources.data != null) { fetchedSources = true + sources = apiSources.data as ArrayList + } + } else { + sources = getSourcesDetails() as ArrayList + } + } else if (isDatabaseEnabled) { + sources = getDBSources().map { it.toView() } as ArrayList + } + + return sources + } + + suspend fun getSourcesDetails(): ArrayList { + var sources = ArrayList() + val isDatabaseEnabled = + appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() + val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true + if (shouldFetch && isNetworkAvailable()) { + val apiSources = api.sourcesDetailed() + if (apiSources.success && apiSources.data != null) { + fetchedSources = true + sources = apiSources.data + if (isDatabaseEnabled) { + resetDBSourcesWithData(sources) } } - apiSources.data ?: ArrayList() } else if (isDatabaseEnabled) { - ArrayList(getDBSources().map { it.toView() }) - } else { - ArrayList() + sources = getDBSources().map { it.toView() } as ArrayList } + return sources } suspend fun markAsRead(item: SelfossModel.Item): Boolean { @@ -482,7 +505,7 @@ class Repository( } } - private fun resetDBSourcesWithData(sources: List) { + private fun resetDBSourcesWithData(sources: List) { db.sourcesQueries.deleteAllSources() db.sourcesQueries.transaction { @@ -592,7 +615,7 @@ class Repository( ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1) } - fun setSelectedSource(source: SelfossModel.Source) { + fun setSelectedSource(source: SelfossModel.SourceDetail) { _selectedSource = source } @@ -600,7 +623,7 @@ class Repository( _selectedSource = null } - fun getSelectedSource(): SelfossModel.Source? { + fun getSelectedSource(): SelfossModel.SourceDetail? { return _selectedSource } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt index 4d10b3b..3870852 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt @@ -183,7 +183,15 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { } }) - suspend fun sources(): StatusAndData> = + suspend fun sourcesStats(): StatusAndData> = + bodyOrFailure(client.tryToGet(url("/sources/stats")) { + if (!shouldHavePostLogin()) { + parameter("username", appSettingsService.getUserName()) + parameter("password", appSettingsService.getPassword()) + } + }) + + suspend fun sourcesDetailed(): StatusAndData> = bodyOrFailure(client.tryToGet(url("/sources/list")) { if (!shouldHavePostLogin()) { parameter("username", appSettingsService.getUserName()) diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/EntityUtils.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/EntityUtils.kt index 875da49..7c1c1af 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/EntityUtils.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/EntityUtils.kt @@ -12,24 +12,25 @@ fun TAG.toView(): SelfossModel.Tag = this.unread.toInt() ) -fun SOURCE.toView(): SelfossModel.Source = - SelfossModel.Source( +fun SOURCE.toView(): SelfossModel.SourceDetail = + SelfossModel.SourceDetail( this.id.toInt(), this.title, - this.tags.split(","), + null, + this.tags?.split(","), this.spout, this.error, this.icon, if (this.url != null) SelfossModel.SourceParams(this.url) else null ) -fun SelfossModel.Source.toEntity(): SOURCE = +fun SelfossModel.SourceDetail.toEntity(): SOURCE = SOURCE( this.id.toString(), this.title.getHtmlDecoded(), - this.tags.joinToString(","), - this.spout, - this.error, + this.tags?.joinToString(",").orEmpty(), + this.spout.orEmpty(), + this.error.orEmpty(), this.icon.orEmpty(), this.params?.url )