Compare commits

..

No commits in common. "97793a7fc81952f08dd8ac308210b0aba1a58837" and "e1c64cef46a37506b024ec6ed0125b9f0464eba9" have entirely different histories.

11 changed files with 59 additions and 174 deletions

View File

@ -55,7 +55,7 @@ class SourcesActivity : AppCompatActivity(), DIAware {
binding.recyclerView.layoutManager = mLayoutManager binding.recyclerView.layoutManager = mLayoutManager
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSourcesDetails() val response = repository.getSources()
if (response.isNotEmpty()) { if (response.isNotEmpty()) {
items = response items = response
val mAdapter = SourcesListAdapter( val mAdapter = SourcesListAdapter(

View File

@ -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

View File

@ -61,7 +61,7 @@ class SourcesListAdapter(
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
} }
if (itm.error.isNullOrBlank()) { if (itm.error.isNotBlank()) {
binding.errorText.visibility = View.VISIBLE binding.errorText.visibility = View.VISIBLE
binding.errorText.text = itm.error binding.errorText.text = itm.error
} else { } else {

View File

@ -84,7 +84,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
) { ) {
val sourceGroup = binding.sourcesGroup val sourceGroup = binding.sourcesGroup
repository.getSourcesStats().forEach { source -> repository.getSources().forEach { source ->
val c = Chip(context) val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END c.ellipsize = TextUtils.TruncateAt.END
@ -141,9 +141,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
selectedChip = c selectedChip = c
} }
c.isEnabled = source.error.isNullOrBlank() c.isEnabled = source.error.isBlank()
if (!source.error.isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (source.error.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
c.tooltipText = source.error c.tooltipText = source.error
} }

View File

@ -303,7 +303,6 @@ class RepositoryTest {
repository.setSourceFilter(SelfossModel.Source( repository.setSourceFilter(SelfossModel.Source(
1, 1,
"Test", "Test",
0,
listOf("tags"), listOf("tags"),
SPOUT, SPOUT,
"", "",
@ -610,19 +609,19 @@ 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.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sources.map { SelfossModel.Source(it) }, testSources) assertSame(sources, testSources)
assertNotEquals(sourcesDB.map { it.toView() }, testSources) assertNotEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 1) { api.sourcesDetailed() } coVerify(exactly = 1) { api.sources() }
} }
private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> { private fun prepareSources(): Pair<ArrayList<SelfossModel.Source>, List<SOURCE>> {
val sources = arrayListOf( val sources = arrayListOf(
SelfossModel.SourceDetail( SelfossModel.Source(
1, 1,
"First source", "First source",
listOf("Test", "second"), listOf("Test", "second"),
@ -631,7 +630,7 @@ class RepositoryTest {
IMAGE_URL_2, IMAGE_URL_2,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
), ),
SelfossModel.SourceDetail( SelfossModel.Source(
2, 2,
"Second source", "Second source",
listOf("second"), listOf("second"),
@ -645,7 +644,6 @@ class RepositoryTest {
SOURCE( SOURCE(
"1", "1",
"First DB source", "First DB source",
3,
"Test,second", "Test,second",
SPOUT, SPOUT,
"", "",
@ -655,7 +653,6 @@ class RepositoryTest {
SOURCE( SOURCE(
"2", "2",
"Second source", "Second source",
null,
"second", "second",
SPOUT, SPOUT,
"", "",
@ -664,7 +661,7 @@ class RepositoryTest {
) )
) )
coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources) coEvery { api.sources() } 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)
} }
@ -678,13 +675,13 @@ class RepositoryTest {
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSources()
// 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.getSourcesDetails() testSources = repository.getSources()
} }
coVerify(exactly = 1) { api.sourcesDetailed() } coVerify(exactly = 1) { api.sources() }
assertNotEquals(sources.map { SelfossModel.Source(it) }, testSources) assertNotSame(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() }
} }
@ -696,13 +693,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.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sources.map { SelfossModel.Source(it) }, testSources) assertSame(sources, testSources)
coVerify(exactly = 1) { api.sourcesDetailed() } coVerify(exactly = 1) { api.sources() }
verify(exactly = 0) { db.sourcesQueries } verify(exactly = 0) { db.sourcesQueries }
} }
@ -713,13 +710,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.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sources.map { SelfossModel.Source(it) }, testSources) assertSame(sources, testSources)
coVerify(exactly = 1) { api.sourcesDetailed() } coVerify(exactly = 1) { api.sources() }
verify(atLeast = 1) { db.sourcesQueries } verify(atLeast = 1) { db.sourcesQueries }
} }
@ -727,13 +724,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.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sourcesDB.map { it.toView() }, testSources) assertEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 0) { api.sourcesDetailed() } coVerify(exactly = 0) { api.sources() }
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
} }
@ -744,13 +741,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.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(emptyList<SelfossModel.Source>(), testSources) assertEquals(emptyList<SelfossModel.Source>(), testSources)
coVerify(exactly = 0) { api.sourcesDetailed() } coVerify(exactly = 0) { api.sources() }
verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() } verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() }
} }
@ -761,13 +758,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.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sourcesDB.map { it.toView() }, testSources) assertEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 0) { api.sourcesDetailed() } coVerify(exactly = 0) { api.sources() }
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
} }
@ -778,13 +775,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.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sourcesDB.map { it.toView() }, testSources) assertEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 0) { api.sourcesDetailed() } coVerify(exactly = 0) { api.sources() }
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
} }
@ -1108,7 +1105,6 @@ class RepositoryTest {
SelfossModel.Source( SelfossModel.Source(
1, 1,
"First source", "First source",
5,
listOf("Test", "second"), listOf("Test", "second"),
SPOUT, SPOUT,
"", "",

View File

@ -63,79 +63,8 @@ class SelfossModel {
fun isPublicModeEnabled() = publicMode ?: false fun isPublicModeEnabled() = publicMode ?: false
} }
@Serializable
data class Source( data class Source(
val id: Int,
val title: String,
var unread: Int?,
var tags: List<String>?,
var spout: String?,
var error: String?,
var icon: String?,
var params: SourceParams?
) {
constructor(sourceDetail: SourceDetail) : this(
id = sourceDetail.id,
title = sourceDetail.title,
unread = null,
tags = sourceDetail.tags,
spout = sourceDetail.spout,
error = sourceDetail.error,
icon = sourceDetail.icon,
params = sourceDetail.params
)
constructor(sourceStat: SourceStats) : this(
id = sourceStat.id,
title = sourceStat.title,
unread = sourceStat.unread,
tags = null,
spout = null,
error = null,
icon = null,
params = null
)
fun update(sourceStat: SourceStats) {
if (this.id != sourceStat.id || this.title != sourceStat.title) {
throw Exception()
}
this.unread = sourceStat.unread
}
fun update(sourceDetail: SourceDetail) {
if (this.id != sourceDetail.id || this.title != sourceDetail.title) {
throw Exception()
}
this.tags = sourceDetail.tags
this.spout = sourceDetail.spout
this.error = sourceDetail.error
this.icon = sourceDetail.icon
this.params = sourceDetail.params
}
fun update(source: Source) {
if (this.id != source.id || this.title != source.title) {
throw Exception()
}
this.unread = source.unread
this.tags = source.tags
this.spout = source.spout
this.error = source.error
this.icon = source.icon
this.params = source.params
}
}
@Serializable
data class SourceStats(
val id: Int,
val title: String,
val unread: Int
)
@Serializable
data class SourceDetail(
val id: Int, val id: Int,
val title: String, val title: String,
@Serializable(with = TagsListSerializer::class) @Serializable(with = TagsListSerializer::class)

View File

@ -44,8 +44,7 @@ class Repository(
private val _badgeStarred = MutableStateFlow(0) private val _badgeStarred = MutableStateFlow(0)
val badgeStarred = _badgeStarred.asStateFlow() val badgeStarred = _badgeStarred.asStateFlow()
private var sources = ArrayList<SelfossModel.Source>() private var fetchedSources = false
private var fetchedTags = false private var fetchedTags = false
private var _readerItems = ArrayList<SelfossModel.Item>() private var _readerItems = ArrayList<SelfossModel.Item>()
@ -181,49 +180,23 @@ class Repository(
} }
} }
private fun updateSources(newSources: ArrayList<SelfossModel.Source>) { suspend fun getSources(): ArrayList<SelfossModel.Source> {
val isDatabaseEnabled = val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
val newIds = newSources.map {it.id} return if (isNetworkAvailable() && !fetchedSources) {
val filteredSources = sources.filterNot { it.id in newIds } val apiSources = api.sources()
for (source in filteredSources) { if (apiSources.success && apiSources.data != null && isDatabaseEnabled) {
val newSource = newSources.find { it.id == source.id } resetDBSourcesWithData(apiSources.data)
source.update(newSource!!) if (!appSettingsService.isUpdateSourcesEnabled()) {
} fetchedSources = true
sources = (filteredSources + newSources).distinctBy { it.id } as ArrayList<SelfossModel.Source>
if (isDatabaseEnabled) {
resetDBSourcesWithData(sources)
} }
} }
apiSources.data ?: ArrayList()
suspend fun getSourcesStats(): ArrayList<SelfossModel.Source> {
val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
if (isNetworkAvailable() && sources.none { it.unread != null }) {
val apiSources = api.sourcesStats()
if (apiSources.success && apiSources.data != null) {
updateSources(apiSources.data.map { SelfossModel.Source(it) } as ArrayList)
}
} else if (isDatabaseEnabled) { } else if (isDatabaseEnabled) {
updateSources(getDBSources().map { it.toView() } as ArrayList) ArrayList(getDBSources().map { it.toView() })
} else {
ArrayList()
} }
return sources
}
suspend fun getSourcesDetails(): ArrayList<SelfossModel.Source> {
val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
if (isNetworkAvailable() && sources.none { it.spout != null }) {
val apiSources = api.sourcesDetailed()
if (apiSources.success && apiSources.data != null) {
updateSources(apiSources.data.map { SelfossModel.Source(it) } as ArrayList)
}
} else if (isDatabaseEnabled) {
updateSources(getDBSources().map { it.toView() } as ArrayList)
}
return sources
} }
suspend fun markAsRead(item: SelfossModel.Item): Boolean { suspend fun markAsRead(item: SelfossModel.Item): Boolean {
@ -400,7 +373,6 @@ class Repository(
items = ArrayList(items.filter { it.sourcetitle != title }) items = ArrayList(items.filter { it.sourcetitle != title })
setReaderItems(items) setReaderItems(items)
db.itemsQueries.deleteItemsWhereSource(title) db.itemsQueries.deleteItemsWhereSource(title)
sources.removeAll { it.id == id }
} }
return success return success

View File

@ -183,15 +183,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
}) })
suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> = suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> =
bodyOrFailure(client.tryToGet(url("/sources/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
})
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())

View File

@ -16,8 +16,7 @@ fun SOURCE.toView(): SelfossModel.Source =
SelfossModel.Source( SelfossModel.Source(
this.id.toInt(), this.id.toInt(),
this.title, this.title,
this.unread?.toInt(), this.tags.split(","),
this.tags?.split(","),
this.spout, this.spout,
this.error, this.error,
this.icon, this.icon,
@ -28,8 +27,7 @@ fun SelfossModel.Source.toEntity(): SOURCE =
SOURCE( SOURCE(
this.id.toString(), this.id.toString(),
this.title.getHtmlDecoded(), this.title.getHtmlDecoded(),
this.unread?.toLong(), this.tags.joinToString(","),
this.tags?.joinToString(","),
this.spout, this.spout,
this.error, this.error,
this.icon.orEmpty(), this.icon.orEmpty(),

View File

@ -1 +0,0 @@
ALTER TABLE SOURCE ADD COLUMN `unread` INTEGER;

View File

@ -1,11 +1,10 @@
CREATE TABLE SOURCE ( CREATE TABLE SOURCE (
`id` TEXT NOT NULL, `id` TEXT NOT NULL,
`title` TEXT NOT NULL, `title` TEXT NOT NULL,
`unread` INTEGER, `tags` TEXT NOT NULL,
`tags` TEXT, `spout` TEXT NOT NULL,
`spout` TEXT, `error` TEXT NOT NULL,
`error` TEXT, `icon` TEXT NOT NULL,
`icon` TEXT,
`url` TEXT, `url` TEXT,
PRIMARY KEY(`id`) PRIMARY KEY(`id`)
); );