mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-11 15:22:03 +01:00
Add data listener to browse source
This commit is contained in:
@@ -13,6 +13,7 @@ import kotlinx.coroutines.channels.BufferOverflow
|
|||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
@@ -27,9 +28,10 @@ class ServerListeners @Inject constructor() {
|
|||||||
|
|
||||||
private fun <T> Flow<T>.startWith(value: T) = onStart { emit(value) }
|
private fun <T> Flow<T>.startWith(value: T) = onStart { emit(value) }
|
||||||
|
|
||||||
private val mangaListener = MutableSharedFlow<List<Long>>(
|
private val _mangaListener = MutableSharedFlow<List<Long>>(
|
||||||
extraBufferCapacity = Channel.UNLIMITED
|
extraBufferCapacity = Channel.UNLIMITED
|
||||||
)
|
)
|
||||||
|
val mangaListener = _mangaListener.asSharedFlow()
|
||||||
|
|
||||||
private val chapterIndexesListener = MutableSharedFlow<Pair<Long, List<Int>?>>(
|
private val chapterIndexesListener = MutableSharedFlow<Pair<Long, List<Int>?>>(
|
||||||
extraBufferCapacity = Channel.UNLIMITED
|
extraBufferCapacity = Channel.UNLIMITED
|
||||||
@@ -49,18 +51,18 @@ class ServerListeners @Inject constructor() {
|
|||||||
|
|
||||||
fun <T> combineMangaUpdates(flow: Flow<T>, predate: (suspend (List<Long>) -> Boolean)? = null) =
|
fun <T> combineMangaUpdates(flow: Flow<T>, predate: (suspend (List<Long>) -> Boolean)? = null) =
|
||||||
if (predate != null) {
|
if (predate != null) {
|
||||||
mangaListener
|
_mangaListener
|
||||||
.filter(predate)
|
.filter(predate)
|
||||||
.startWith(Unit)
|
.startWith(Unit)
|
||||||
} else {
|
} else {
|
||||||
mangaListener.startWith(Unit)
|
_mangaListener.startWith(Unit)
|
||||||
}
|
}
|
||||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
.flatMapLatest { flow }
|
.flatMapLatest { flow }
|
||||||
|
|
||||||
fun updateManga(vararg ids: Long) {
|
fun updateManga(vararg ids: Long) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
mangaListener.emit(ids.toList())
|
_mangaListener.emit(ids.toList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.jui.domain.source.interactor
|
||||||
|
|
||||||
|
import ca.gosyer.jui.domain.ServerListeners
|
||||||
|
import ca.gosyer.jui.domain.manga.interactor.GetManga
|
||||||
|
import ca.gosyer.jui.domain.manga.model.Manga
|
||||||
|
import ca.gosyer.jui.domain.source.model.MangaPage
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.runningFold
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import me.tatarka.inject.annotations.Inject
|
||||||
|
import org.lighthousegames.logging.logging
|
||||||
|
|
||||||
|
class SourcePager @Inject constructor(
|
||||||
|
private val getManga: GetManga,
|
||||||
|
private val serverListeners: ServerListeners,
|
||||||
|
private val fetcher: suspend (page: Int) -> MangaPage?,
|
||||||
|
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) {
|
||||||
|
private val sourceMutex = Mutex()
|
||||||
|
|
||||||
|
private val _sourceManga = MutableStateFlow<List<Manga>>(emptyList())
|
||||||
|
|
||||||
|
private val mangaIds = _sourceManga.map { mangas -> mangas.map { it.id } }
|
||||||
|
.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
|
private val changedManga = serverListeners.mangaListener.runningFold(emptyMap<Long, Manga>()) { manga, updatedMangaIds ->
|
||||||
|
coroutineScope {
|
||||||
|
manga + updatedMangaIds.filter { it in mangaIds.value }.map {
|
||||||
|
async {
|
||||||
|
getManga.await(it)
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull().associateBy { it.id }
|
||||||
|
}
|
||||||
|
}.stateIn(this, SharingStarted.Eagerly, emptyMap())
|
||||||
|
|
||||||
|
val mangas = combine(_sourceManga, changedManga) { sourceManga, changedManga ->
|
||||||
|
sourceManga.map { changedManga[it.id] ?: it }
|
||||||
|
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
|
private val _pageNum = MutableStateFlow(0)
|
||||||
|
val pageNum = _pageNum.asStateFlow()
|
||||||
|
|
||||||
|
private val _hasNextPage = MutableStateFlow(true)
|
||||||
|
val hasNextPage = _hasNextPage.asStateFlow()
|
||||||
|
|
||||||
|
private val _loading = MutableStateFlow(true)
|
||||||
|
val loading = _loading.asStateFlow()
|
||||||
|
|
||||||
|
fun loadNextPage() {
|
||||||
|
launch {
|
||||||
|
if (hasNextPage.value && sourceMutex.tryLock()) {
|
||||||
|
_pageNum.value++
|
||||||
|
val page = fetcher(_pageNum.value)
|
||||||
|
if (page != null) {
|
||||||
|
_sourceManga.value = _sourceManga.value + page.mangaList
|
||||||
|
_hasNextPage.value = page.hasNextPage
|
||||||
|
} else {
|
||||||
|
_pageNum.value--
|
||||||
|
}
|
||||||
|
sourceMutex.unlock()
|
||||||
|
}
|
||||||
|
_loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = logging()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ package ca.gosyer.jui.ui.sources.browse
|
|||||||
|
|
||||||
import ca.gosyer.jui.domain.library.model.DisplayMode
|
import ca.gosyer.jui.domain.library.model.DisplayMode
|
||||||
import ca.gosyer.jui.domain.library.service.LibraryPreferences
|
import ca.gosyer.jui.domain.library.service.LibraryPreferences
|
||||||
import ca.gosyer.jui.domain.manga.model.Manga
|
|
||||||
import ca.gosyer.jui.domain.source.interactor.GetLatestManga
|
import ca.gosyer.jui.domain.source.interactor.GetLatestManga
|
||||||
import ca.gosyer.jui.domain.source.interactor.GetPopularManga
|
import ca.gosyer.jui.domain.source.interactor.GetPopularManga
|
||||||
import ca.gosyer.jui.domain.source.interactor.GetSearchManga
|
import ca.gosyer.jui.domain.source.interactor.GetSearchManga
|
||||||
|
import ca.gosyer.jui.domain.source.interactor.SourcePager
|
||||||
import ca.gosyer.jui.domain.source.model.MangaPage
|
import ca.gosyer.jui.domain.source.model.MangaPage
|
||||||
import ca.gosyer.jui.domain.source.model.Source
|
import ca.gosyer.jui.domain.source.model.Source
|
||||||
import ca.gosyer.jui.domain.source.service.CatalogPreferences
|
import ca.gosyer.jui.domain.source.service.CatalogPreferences
|
||||||
@@ -19,15 +19,14 @@ import ca.gosyer.jui.ui.base.state.SavedStateHandle
|
|||||||
import ca.gosyer.jui.ui.base.state.getStateFlow
|
import ca.gosyer.jui.ui.base.state.getStateFlow
|
||||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.plus
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import me.tatarka.inject.annotations.Inject
|
import me.tatarka.inject.annotations.Inject
|
||||||
import org.lighthousegames.logging.logging
|
import org.lighthousegames.logging.logging
|
||||||
|
|
||||||
@@ -38,6 +37,7 @@ class SourceScreenViewModel(
|
|||||||
private val getSearchManga: GetSearchManga,
|
private val getSearchManga: GetSearchManga,
|
||||||
private val catalogPreferences: CatalogPreferences,
|
private val catalogPreferences: CatalogPreferences,
|
||||||
private val libraryPreferences: LibraryPreferences,
|
private val libraryPreferences: LibraryPreferences,
|
||||||
|
private val getSourcePager: (suspend (page: Int) -> MangaPage?) -> SourcePager,
|
||||||
contextWrapper: ContextWrapper,
|
contextWrapper: ContextWrapper,
|
||||||
private val savedStateHandle: SavedStateHandle,
|
private val savedStateHandle: SavedStateHandle,
|
||||||
initialQuery: String?
|
initialQuery: String?
|
||||||
@@ -49,6 +49,7 @@ class SourceScreenViewModel(
|
|||||||
getSearchManga: GetSearchManga,
|
getSearchManga: GetSearchManga,
|
||||||
catalogPreferences: CatalogPreferences,
|
catalogPreferences: CatalogPreferences,
|
||||||
libraryPreferences: LibraryPreferences,
|
libraryPreferences: LibraryPreferences,
|
||||||
|
getSourcePager: (suspend (page: Int) -> MangaPage?) -> SourcePager,
|
||||||
contextWrapper: ContextWrapper,
|
contextWrapper: ContextWrapper,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
params: Params
|
params: Params
|
||||||
@@ -59,6 +60,7 @@ class SourceScreenViewModel(
|
|||||||
getSearchManga,
|
getSearchManga,
|
||||||
catalogPreferences,
|
catalogPreferences,
|
||||||
libraryPreferences,
|
libraryPreferences,
|
||||||
|
getSourcePager,
|
||||||
contextWrapper,
|
contextWrapper,
|
||||||
savedStateHandle,
|
savedStateHandle,
|
||||||
params.initialQuery
|
params.initialQuery
|
||||||
@@ -68,15 +70,6 @@ class SourceScreenViewModel(
|
|||||||
val gridColumns = libraryPreferences.gridColumns().stateIn(scope)
|
val gridColumns = libraryPreferences.gridColumns().stateIn(scope)
|
||||||
val gridSize = libraryPreferences.gridSize().stateIn(scope)
|
val gridSize = libraryPreferences.gridSize().stateIn(scope)
|
||||||
|
|
||||||
private val _mangas = MutableStateFlow<ImmutableList<Manga>>(persistentListOf())
|
|
||||||
val mangas = _mangas.asStateFlow()
|
|
||||||
|
|
||||||
private val _hasNextPage = MutableStateFlow(false)
|
|
||||||
val hasNextPage = _hasNextPage.asStateFlow()
|
|
||||||
|
|
||||||
private val _loading = MutableStateFlow(true)
|
|
||||||
val loading = _loading.asStateFlow()
|
|
||||||
|
|
||||||
private val _isLatest by savedStateHandle.getStateFlow { false }
|
private val _isLatest by savedStateHandle.getStateFlow { false }
|
||||||
val isLatest = _isLatest.asStateFlow()
|
val isLatest = _isLatest.asStateFlow()
|
||||||
|
|
||||||
@@ -87,71 +80,74 @@ class SourceScreenViewModel(
|
|||||||
|
|
||||||
private val _query = MutableStateFlow(sourceSearchQuery.value)
|
private val _query = MutableStateFlow(sourceSearchQuery.value)
|
||||||
|
|
||||||
private val _pageNum = MutableStateFlow(1)
|
private val pager = MutableStateFlow(getPager())
|
||||||
val pageNum = _pageNum.asStateFlow()
|
|
||||||
|
val mangas = pager.flatMapLatest { it.mangas.map { mangas -> mangas.toImmutableList() } }
|
||||||
|
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||||
|
val loading = pager.flatMapLatest { it.loading }
|
||||||
|
.stateIn(scope, SharingStarted.Eagerly, true)
|
||||||
|
val hasNextPage = pager.flatMapLatest { it.hasNextPage }
|
||||||
|
.stateIn(scope, SharingStarted.Eagerly, true)
|
||||||
|
|
||||||
private val sourceMutex = Mutex()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
pager.value.loadNextPage()
|
||||||
getPage()?.let { (mangas, hasNextPage) ->
|
|
||||||
_mangas.value = mangas.toImmutableList()
|
|
||||||
_hasNextPage.value = hasNextPage
|
|
||||||
}
|
|
||||||
|
|
||||||
_loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadNextPage() {
|
fun loadNextPage() {
|
||||||
scope.launch {
|
pager.value.loadNextPage()
|
||||||
if (hasNextPage.value && sourceMutex.tryLock()) {
|
|
||||||
_pageNum.value++
|
|
||||||
val page = getPage()
|
|
||||||
if (page != null) {
|
|
||||||
_mangas.value = _mangas.value.toPersistentList() + page.mangaList
|
|
||||||
_hasNextPage.value = page.hasNextPage
|
|
||||||
} else {
|
|
||||||
_pageNum.value--
|
|
||||||
}
|
|
||||||
sourceMutex.unlock()
|
|
||||||
}
|
|
||||||
_loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setMode(toLatest: Boolean) {
|
fun setMode(toLatest: Boolean) {
|
||||||
if (isLatest.value != toLatest) {
|
if (isLatest.value != toLatest) {
|
||||||
_isLatest.value = toLatest
|
_isLatest.value = toLatest
|
||||||
// [loadNextPage] increments by 1
|
|
||||||
_pageNum.value = 0
|
|
||||||
_loading.value = true
|
|
||||||
_query.value = null
|
_query.value = null
|
||||||
_mangas.value = persistentListOf()
|
updatePager()
|
||||||
loadNextPage()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getPage(): MangaPage? {
|
private fun getPager(): SourcePager {
|
||||||
return when {
|
val fetcher: suspend (page: Int) -> MangaPage? = when {
|
||||||
isLatest.value -> getLatestManga.await(source, pageNum.value, onError = { toast(it.message.orEmpty()) })
|
_query.value != null || _usingFilters.value -> {
|
||||||
_query.value != null || _usingFilters.value -> getSearchManga.await(
|
{ page ->
|
||||||
sourceId = source.id,
|
getSearchManga.await(
|
||||||
searchTerm = _query.value,
|
sourceId = source.id,
|
||||||
page = pageNum.value,
|
searchTerm = _query.value,
|
||||||
onError = { toast(it.message.orEmpty()) }
|
page = page,
|
||||||
)
|
onError = { toast(it.message.orEmpty()) }
|
||||||
else -> getPopularManga.await(source.id, pageNum.value, onError = { toast(it.message.orEmpty()) })
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLatest.value -> {
|
||||||
|
{ page ->
|
||||||
|
getLatestManga.await(
|
||||||
|
source,
|
||||||
|
page,
|
||||||
|
onError = { toast(it.message.orEmpty()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
{ page ->
|
||||||
|
getPopularManga.await(
|
||||||
|
source.id,
|
||||||
|
page,
|
||||||
|
onError = { toast(it.message.orEmpty()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return getSourcePager(fetcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePager() {
|
||||||
|
pager.value.cancel()
|
||||||
|
pager.value = getPager()
|
||||||
|
pager.value.loadNextPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startSearch(query: String?) {
|
fun startSearch(query: String?) {
|
||||||
_pageNum.value = 0
|
|
||||||
_hasNextPage.value = true
|
|
||||||
_loading.value = true
|
|
||||||
_query.value = query
|
_query.value = query
|
||||||
_mangas.value = persistentListOf()
|
updatePager()
|
||||||
loadNextPage()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUsingFilters(usingFilters: Boolean) {
|
fun setUsingFilters(usingFilters: Boolean) {
|
||||||
@@ -171,6 +167,11 @@ class SourceScreenViewModel(
|
|||||||
|
|
||||||
data class Params(val source: Source, val initialQuery: String?)
|
data class Params(val source: Source, val initialQuery: String?)
|
||||||
|
|
||||||
|
override fun onDispose() {
|
||||||
|
super.onDispose()
|
||||||
|
pager.value.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private val log = logging()
|
private val log = logging()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user