mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-17 02:02:05 +01:00
Add data listener to updates
This commit is contained in:
@@ -33,13 +33,15 @@ class ServerListeners @Inject constructor() {
|
|||||||
)
|
)
|
||||||
val mangaListener = _mangaListener.asSharedFlow()
|
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
|
||||||
)
|
)
|
||||||
|
val chapterIndexesListener = _chapterIndexesListener.asSharedFlow()
|
||||||
|
|
||||||
private val chapterIdsListener = MutableSharedFlow<Pair<Long?, List<Long>>>(
|
private val _chapterIdsListener = MutableSharedFlow<Pair<Long?, List<Long>>>(
|
||||||
extraBufferCapacity = Channel.UNLIMITED
|
extraBufferCapacity = Channel.UNLIMITED
|
||||||
)
|
)
|
||||||
|
val chapterIdsListener = _chapterIdsListener.asSharedFlow()
|
||||||
|
|
||||||
private val categoryMangaListener = MutableSharedFlow<Long>(
|
private val categoryMangaListener = MutableSharedFlow<Long>(
|
||||||
extraBufferCapacity = Channel.UNLIMITED
|
extraBufferCapacity = Channel.UNLIMITED
|
||||||
@@ -87,14 +89,14 @@ class ServerListeners @Inject constructor() {
|
|||||||
idPredate: (suspend (Long?, List<Long>) -> Boolean)? = null
|
idPredate: (suspend (Long?, List<Long>) -> Boolean)? = null
|
||||||
): Flow<T> {
|
): Flow<T> {
|
||||||
val indexListener = if (indexPredate != null) {
|
val indexListener = if (indexPredate != null) {
|
||||||
chapterIndexesListener.filter { indexPredate(it.first, it.second) }.startWith(Unit)
|
_chapterIndexesListener.filter { indexPredate(it.first, it.second) }.startWith(Unit)
|
||||||
} else {
|
} else {
|
||||||
chapterIndexesListener.startWith(Unit)
|
_chapterIndexesListener.startWith(Unit)
|
||||||
}
|
}
|
||||||
val idsListener = if (idPredate != null) {
|
val idsListener = if (idPredate != null) {
|
||||||
chapterIdsListener.filter { idPredate(it.first, it.second) }.startWith(Unit)
|
_chapterIdsListener.filter { idPredate(it.first, it.second) }.startWith(Unit)
|
||||||
} else {
|
} else {
|
||||||
chapterIdsListener.startWith(Unit)
|
_chapterIdsListener.startWith(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
return combine(indexListener, idsListener) { _, _ -> }
|
return combine(indexListener, idsListener) { _, _ -> }
|
||||||
@@ -104,25 +106,25 @@ class ServerListeners @Inject constructor() {
|
|||||||
|
|
||||||
fun updateChapters(mangaId: Long, chapterIndexes: List<Int>) {
|
fun updateChapters(mangaId: Long, chapterIndexes: List<Int>) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
chapterIndexesListener.emit(mangaId to chapterIndexes.ifEmpty { null })
|
_chapterIndexesListener.emit(mangaId to chapterIndexes.ifEmpty { null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChapters(mangaId: Long, vararg chapterIndexes: Int) {
|
fun updateChapters(mangaId: Long, vararg chapterIndexes: Int) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
chapterIndexesListener.emit(mangaId to chapterIndexes.toList().ifEmpty { null })
|
_chapterIndexesListener.emit(mangaId to chapterIndexes.toList().ifEmpty { null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChapters(mangaId: Long?, chapterIds: List<Long>) {
|
fun updateChapters(mangaId: Long?, chapterIds: List<Long>) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
chapterIdsListener.emit(mangaId to chapterIds)
|
_chapterIdsListener.emit(mangaId to chapterIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChapters(mangaId: Long?, vararg chapterIds: Long) {
|
fun updateChapters(mangaId: Long?, vararg chapterIds: Long) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
chapterIdsListener.emit(mangaId to chapterIds.toList())
|
_chapterIdsListener.emit(mangaId to chapterIds.toList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* 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.updates.interactor
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import ca.gosyer.jui.domain.ServerListeners
|
||||||
|
import ca.gosyer.jui.domain.chapter.interactor.GetChapter
|
||||||
|
import ca.gosyer.jui.domain.chapter.model.Chapter
|
||||||
|
import ca.gosyer.jui.domain.manga.interactor.GetManga
|
||||||
|
import ca.gosyer.jui.domain.manga.model.Manga
|
||||||
|
import ca.gosyer.jui.domain.updates.model.MangaAndChapter
|
||||||
|
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.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.runningFold
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import me.tatarka.inject.annotations.Inject
|
||||||
|
|
||||||
|
class UpdatesPager @Inject constructor(
|
||||||
|
private val getRecentUpdates: GetRecentUpdates,
|
||||||
|
private val getManga: GetManga,
|
||||||
|
private val getChapter: GetChapter,
|
||||||
|
private val serverListeners: ServerListeners,
|
||||||
|
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()){
|
||||||
|
private val updatesMutex = Mutex()
|
||||||
|
|
||||||
|
private val fetchedUpdates = MutableSharedFlow<List<MangaAndChapter>>()
|
||||||
|
private val foldedUpdates = fetchedUpdates.runningFold(emptyList<Updates>()) { updates, newUpdates ->
|
||||||
|
updates.ifEmpty {
|
||||||
|
val first = newUpdates.firstOrNull()?.chapter ?: return@runningFold updates
|
||||||
|
listOf(
|
||||||
|
Updates.Date(
|
||||||
|
Instant.fromEpochSeconds(first.fetchedAt)
|
||||||
|
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
.date
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} + newUpdates.fold(emptyList()) { list, (manga, chapter) ->
|
||||||
|
val date = (list.lastOrNull() as? Updates.Update)?.let {
|
||||||
|
val lastUpdateDate = Instant.fromEpochSeconds(it.chapter.fetchedAt)
|
||||||
|
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
.date
|
||||||
|
val chapterDate = Instant.fromEpochSeconds(chapter.fetchedAt)
|
||||||
|
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
.date
|
||||||
|
chapterDate.takeUnless { it == lastUpdateDate }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date == null) {
|
||||||
|
list + Updates.Update(manga, chapter)
|
||||||
|
} else {
|
||||||
|
list + Updates.Date(date) + Updates.Update(manga, chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
|
private val mangaIds = fetchedUpdates.map { updates -> updates.map { it.manga.id } }
|
||||||
|
.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||||
|
private val chapterIds = fetchedUpdates.map { updates -> updates.map { Triple(it.manga.id, it.chapter.index, it.chapter.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())
|
||||||
|
|
||||||
|
private val changedChapters = MutableStateFlow(emptyMap<Long, Chapter>())
|
||||||
|
|
||||||
|
init {
|
||||||
|
serverListeners.chapterIndexesListener
|
||||||
|
.onEach {(mangaId, chapterIndexes) ->
|
||||||
|
if (chapterIndexes == null) {
|
||||||
|
val chapters = coroutineScope {
|
||||||
|
foldedUpdates.value.filterIsInstance<Updates.Update>().filter { it.manga.id == mangaId }.map {
|
||||||
|
async {
|
||||||
|
getChapter.await(it.manga.id, it.chapter.index)
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull().associateBy { it.id }
|
||||||
|
}
|
||||||
|
changedChapters.update { it + chapters }
|
||||||
|
} else {
|
||||||
|
val chapters = coroutineScope {
|
||||||
|
chapterIndexes.mapNotNull { index -> chapterIds.value.find { it.first == mangaId && it.second == index } }
|
||||||
|
.map {
|
||||||
|
async {
|
||||||
|
getChapter.await(it.first, it.second)
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull().associateBy { it.id }
|
||||||
|
}
|
||||||
|
changedChapters.update { it + chapters }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(this)
|
||||||
|
serverListeners.chapterIdsListener
|
||||||
|
.onEach { (_, updatedChapterIds) ->
|
||||||
|
val chapters = coroutineScope {
|
||||||
|
updatedChapterIds.mapNotNull { id -> chapterIds.value.find { it.third == id } }.map {
|
||||||
|
async {
|
||||||
|
getChapter.await(it.first, it.second)
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull().associateBy { it.id }
|
||||||
|
}
|
||||||
|
changedChapters.update { it + chapters }
|
||||||
|
}
|
||||||
|
.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val updates = combine(
|
||||||
|
foldedUpdates,
|
||||||
|
changedManga,
|
||||||
|
changedChapters
|
||||||
|
) { updates, changedManga, changedChapters ->
|
||||||
|
updates.map {
|
||||||
|
when (it) {
|
||||||
|
is Updates.Date -> it
|
||||||
|
is Updates.Update -> it.copy(
|
||||||
|
manga = changedManga[it.manga.id] ?: it.manga,
|
||||||
|
chapter = changedChapters[it.chapter.id] ?: it.chapter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
|
private val currentPage = MutableStateFlow(0)
|
||||||
|
private val hasNextPage = MutableStateFlow(true)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class Updates {
|
||||||
|
@Immutable
|
||||||
|
data class Update(val manga: Manga, val chapter: Chapter) : Updates()
|
||||||
|
@Immutable
|
||||||
|
data class Date(val date: String) : Updates() {
|
||||||
|
constructor(date: LocalDate) : this(date.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadNextPage(
|
||||||
|
onComplete: (() -> Unit)? = null,
|
||||||
|
onError: suspend (Throwable) -> Unit
|
||||||
|
) {
|
||||||
|
launch {
|
||||||
|
if (hasNextPage.value && updatesMutex.tryLock()) {
|
||||||
|
currentPage.value++
|
||||||
|
if (!getUpdates(currentPage.value, onError)) {
|
||||||
|
currentPage.value--
|
||||||
|
}
|
||||||
|
updatesMutex.unlock()
|
||||||
|
}
|
||||||
|
onComplete?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getUpdates(page: Int, onError: suspend (Throwable) -> Unit): Boolean {
|
||||||
|
val updates = getRecentUpdates.await(page, onError) ?: return false
|
||||||
|
hasNextPage.value = updates.hasNextPage
|
||||||
|
fetchedUpdates.emit(updates.page)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.ui.updates
|
package ca.gosyer.jui.ui.updates
|
||||||
|
|
||||||
|
import ca.gosyer.jui.core.lang.launchDefault
|
||||||
import ca.gosyer.jui.domain.chapter.interactor.BatchUpdateChapter
|
import ca.gosyer.jui.domain.chapter.interactor.BatchUpdateChapter
|
||||||
import ca.gosyer.jui.domain.chapter.interactor.DeleteChapterDownload
|
import ca.gosyer.jui.domain.chapter.interactor.DeleteChapterDownload
|
||||||
import ca.gosyer.jui.domain.chapter.model.Chapter
|
import ca.gosyer.jui.domain.chapter.model.Chapter
|
||||||
@@ -15,6 +16,7 @@ import ca.gosyer.jui.domain.download.interactor.StopChapterDownload
|
|||||||
import ca.gosyer.jui.domain.download.service.DownloadService
|
import ca.gosyer.jui.domain.download.service.DownloadService
|
||||||
import ca.gosyer.jui.domain.updates.interactor.GetRecentUpdates
|
import ca.gosyer.jui.domain.updates.interactor.GetRecentUpdates
|
||||||
import ca.gosyer.jui.domain.updates.interactor.UpdateLibrary
|
import ca.gosyer.jui.domain.updates.interactor.UpdateLibrary
|
||||||
|
import ca.gosyer.jui.domain.updates.interactor.UpdatesPager
|
||||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadItem
|
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadItem
|
||||||
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
|
||||||
@@ -22,24 +24,19 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.datetime.toLocalDateTime
|
|
||||||
import me.tatarka.inject.annotations.Inject
|
import me.tatarka.inject.annotations.Inject
|
||||||
import org.lighthousegames.logging.logging
|
import org.lighthousegames.logging.logging
|
||||||
|
|
||||||
@@ -51,25 +48,27 @@ class UpdatesScreenViewModel @Inject constructor(
|
|||||||
private val batchUpdateChapter: BatchUpdateChapter,
|
private val batchUpdateChapter: BatchUpdateChapter,
|
||||||
private val batchChapterDownload: BatchChapterDownload,
|
private val batchChapterDownload: BatchChapterDownload,
|
||||||
private val updateLibrary: UpdateLibrary,
|
private val updateLibrary: UpdateLibrary,
|
||||||
|
private val updatesPager: UpdatesPager,
|
||||||
contextWrapper: ContextWrapper
|
contextWrapper: ContextWrapper
|
||||||
) : ViewModel(contextWrapper) {
|
) : ViewModel(contextWrapper) {
|
||||||
|
|
||||||
private val _isLoading = MutableStateFlow(true)
|
private val _isLoading = MutableStateFlow(true)
|
||||||
val isLoading = _isLoading.asStateFlow()
|
val isLoading = _isLoading.asStateFlow()
|
||||||
|
|
||||||
private val _updates = MutableStateFlow<ImmutableList<UpdatesUI>>(persistentListOf())
|
val updates = updatesPager.updates.map {
|
||||||
val updates = _updates.asStateFlow()
|
it.map {
|
||||||
|
when (it) {
|
||||||
private val currentPage = MutableStateFlow(1)
|
is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date)
|
||||||
private val hasNextPage = MutableStateFlow(false)
|
is UpdatesPager.Updates.Update -> UpdatesUI.Item(ChapterDownloadItem(it.manga, it.chapter))
|
||||||
|
}
|
||||||
private val updatesMutex = Mutex()
|
}.toImmutableList()
|
||||||
private var downloadServiceJob: Job? = null
|
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||||
|
|
||||||
private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf())
|
private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf())
|
||||||
val selectedItems = combine(updates, _selectedIds) { updates, selecteditems ->
|
val selectedItems = combine(updates, _selectedIds) { updates, selectedItems ->
|
||||||
updates.filterIsInstance<UpdatesUI.Item>()
|
updates.asSequence()
|
||||||
.filter { it.chapterDownloadItem.isSelected(selecteditems) }
|
.filterIsInstance<UpdatesUI.Item>()
|
||||||
|
.filter { it.chapterDownloadItem.isSelected(selectedItems) }
|
||||||
.map { it.chapterDownloadItem }
|
.map { it.chapterDownloadItem }
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||||
@@ -78,78 +77,40 @@ class UpdatesScreenViewModel @Inject constructor(
|
|||||||
.stateIn(scope, SharingStarted.Eagerly, false)
|
.stateIn(scope, SharingStarted.Eagerly, false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch(Dispatchers.Default) {
|
updatesPager.loadNextPage(
|
||||||
getUpdates(currentPage.value)
|
onComplete = {
|
||||||
|
_isLoading.value = false
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
toast(it.message.orEmpty())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun loadNextPage() {
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
if (hasNextPage.value && updatesMutex.tryLock()) {
|
|
||||||
currentPage.value++
|
|
||||||
getUpdates(currentPage.value)
|
|
||||||
updatesMutex.unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getUpdates(page: Int) {
|
|
||||||
getRecentUpdates.asFlow(page)
|
|
||||||
.onEach { updates ->
|
|
||||||
val lastUpdateDate = (_updates.value.lastOrNull() as? UpdatesUI.Item)
|
|
||||||
?.let {
|
|
||||||
Instant.fromEpochSeconds(it.chapterDownloadItem.chapter.fetchedAt)
|
|
||||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
|
||||||
.date
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
val items = updates.page
|
|
||||||
.map {
|
|
||||||
ChapterDownloadItem(
|
|
||||||
it.manga,
|
|
||||||
it.chapter
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
.groupBy {
|
|
||||||
Instant.fromEpochSeconds(it.chapter.fetchedAt).toLocalDateTime(TimeZone.currentSystemDefault()).date
|
|
||||||
}
|
|
||||||
.entries
|
|
||||||
.sortedByDescending { it.key.toEpochDays() }
|
|
||||||
_updates.value = _updates.value.plus(
|
|
||||||
items
|
|
||||||
.flatMap { (date, updates) ->
|
|
||||||
listOf(UpdatesUI.Header(date.toString())).dropWhile { it.date == lastUpdateDate } +
|
|
||||||
updates
|
updates
|
||||||
.sortedByDescending { it.chapter.fetchedAt }
|
.map { updates ->
|
||||||
.map { UpdatesUI.Item(it) }
|
updates.filterIsInstance<UpdatesUI.Item>().mapNotNull {
|
||||||
}
|
|
||||||
).toImmutableList()
|
|
||||||
|
|
||||||
downloadServiceJob?.cancel()
|
|
||||||
val mangaIds = _updates.value.filterIsInstance<UpdatesUI.Item>().mapNotNull {
|
|
||||||
it.chapterDownloadItem.manga?.id
|
it.chapterDownloadItem.manga?.id
|
||||||
}.toSet()
|
}.toSet()
|
||||||
downloadServiceJob = DownloadService.registerWatches(mangaIds)
|
}
|
||||||
|
.combine(DownloadService.downloadQueue) { mangaIds, queue ->
|
||||||
|
mangaIds to queue
|
||||||
|
}
|
||||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
.onEach { chapters ->
|
.onEach { (mangaIds, queue) ->
|
||||||
_updates.value.filterIsInstance<UpdatesUI.Item>().forEach {
|
val chapters = queue.filter { it.mangaId in mangaIds }
|
||||||
|
updates.value.filterIsInstance<UpdatesUI.Item>().forEach {
|
||||||
it.chapterDownloadItem.updateFrom(chapters)
|
it.chapterDownloadItem.updateFrom(chapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
hasNextPage.value = updates.hasNextPage
|
fun loadNextPage() {
|
||||||
_isLoading.value = false
|
updatesPager.loadNextPage(
|
||||||
}
|
onError = {
|
||||||
.catch {
|
|
||||||
toast(it.message.orEmpty())
|
toast(it.message.orEmpty())
|
||||||
log.warn(it) { "Failed to get updates for page $page" }
|
|
||||||
if (page > 1) {
|
|
||||||
currentPage.value = page - 1
|
|
||||||
}
|
}
|
||||||
_isLoading.value = false
|
)
|
||||||
}
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setRead(chapterIds: List<Long>, read: Boolean) {
|
private fun setRead(chapterIds: List<Long>, read: Boolean) {
|
||||||
@@ -183,7 +144,7 @@ class UpdatesScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDownloadedChapter(chapter: Chapter?) {
|
fun deleteDownloadedChapter(chapter: Chapter?) {
|
||||||
scope.launch {
|
scope.launchDefault {
|
||||||
if (chapter == null) {
|
if (chapter == null) {
|
||||||
val selectedIds = _selectedIds.value
|
val selectedIds = _selectedIds.value
|
||||||
batchUpdateChapter.await(selectedIds, delete = true, onError = { toast(it.message.orEmpty()) })
|
batchUpdateChapter.await(selectedIds, delete = true, onError = { toast(it.message.orEmpty()) })
|
||||||
@@ -191,9 +152,9 @@ class UpdatesScreenViewModel @Inject constructor(
|
|||||||
it.setNotDownloaded()
|
it.setNotDownloaded()
|
||||||
}
|
}
|
||||||
_selectedIds.value = persistentListOf()
|
_selectedIds.value = persistentListOf()
|
||||||
return@launch
|
return@launchDefault
|
||||||
}
|
}
|
||||||
_updates.value
|
updates.value
|
||||||
.filterIsInstance<UpdatesUI.Item>()
|
.filterIsInstance<UpdatesUI.Item>()
|
||||||
.find { (chapterDownloadItem) ->
|
.find { (chapterDownloadItem) ->
|
||||||
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
||||||
@@ -205,8 +166,8 @@ class UpdatesScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stopDownloadingChapter(chapter: Chapter) {
|
fun stopDownloadingChapter(chapter: Chapter) {
|
||||||
scope.launch {
|
scope.launchDefault {
|
||||||
_updates.value
|
updates.value
|
||||||
.filterIsInstance<UpdatesUI.Item>()
|
.filterIsInstance<UpdatesUI.Item>()
|
||||||
.find { (chapterDownloadItem) ->
|
.find { (chapterDownloadItem) ->
|
||||||
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
||||||
@@ -218,7 +179,7 @@ class UpdatesScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun selectAll() {
|
fun selectAll() {
|
||||||
scope.launch {
|
scope.launchDefault {
|
||||||
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
||||||
.map { it.chapterDownloadItem.chapter.id }
|
.map { it.chapterDownloadItem.chapter.id }
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
@@ -226,7 +187,7 @@ class UpdatesScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun invertSelection() {
|
fun invertSelection() {
|
||||||
scope.launch {
|
scope.launchDefault {
|
||||||
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
||||||
.map { it.chapterDownloadItem.chapter.id }
|
.map { it.chapterDownloadItem.chapter.id }
|
||||||
.minus(_selectedIds.value)
|
.minus(_selectedIds.value)
|
||||||
@@ -235,24 +196,29 @@ class UpdatesScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun selectChapter(id: Long) {
|
fun selectChapter(id: Long) {
|
||||||
scope.launch {
|
scope.launchDefault {
|
||||||
_selectedIds.value = _selectedIds.value.plus(id).toImmutableList()
|
_selectedIds.value = _selectedIds.value.plus(id).toImmutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun unselectChapter(id: Long) {
|
fun unselectChapter(id: Long) {
|
||||||
scope.launch {
|
scope.launchDefault {
|
||||||
_selectedIds.value = _selectedIds.value.minus(id).toImmutableList()
|
_selectedIds.value = _selectedIds.value.minus(id).toImmutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearSelection() {
|
fun clearSelection() {
|
||||||
scope.launch {
|
scope.launchDefault {
|
||||||
_selectedIds.value = persistentListOf()
|
_selectedIds.value = persistentListOf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibrary() {
|
fun updateLibrary() {
|
||||||
scope.launch { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) }
|
scope.launchDefault { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDispose() {
|
||||||
|
super.onDispose()
|
||||||
|
updatesPager.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.ContentAlpha
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -88,6 +89,13 @@ fun UpdatesItem(
|
|||||||
data = manga,
|
data = manga,
|
||||||
contentDescription = manga.title
|
contentDescription = manga.title
|
||||||
)
|
)
|
||||||
|
val textColor = if (chapter.bookmarked && !chapter.read) {
|
||||||
|
MaterialTheme.colors.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colors.onSurface.copy(
|
||||||
|
alpha = if (chapter.read) ContentAlpha.disabled else ContentAlpha.medium
|
||||||
|
)
|
||||||
|
}
|
||||||
MangaListItemColumn(
|
MangaListItemColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -96,10 +104,13 @@ fun UpdatesItem(
|
|||||||
) {
|
) {
|
||||||
MangaListItemTitle(
|
MangaListItemTitle(
|
||||||
text = manga.title,
|
text = manga.title,
|
||||||
fontWeight = FontWeight.SemiBold
|
bookmarked = chapter.bookmarked,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textColor = textColor
|
||||||
)
|
)
|
||||||
MangaListItemSubtitle(
|
MangaListItemSubtitle(
|
||||||
text = chapter.name
|
text = chapter.name,
|
||||||
|
textColor = textColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,31 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.FilterQuality
|
import androidx.compose.ui.graphics.FilterQuality
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ca.gosyer.jui.i18n.MR
|
||||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||||
|
import ca.gosyer.jui.uicore.resources.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaListItem(
|
fun MangaListItem(
|
||||||
@@ -65,27 +80,48 @@ fun MangaListItemColumn(
|
|||||||
fun MangaListItemTitle(
|
fun MangaListItemTitle(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String,
|
text: String,
|
||||||
|
bookmarked: Boolean = false,
|
||||||
maxLines: Int = 1,
|
maxLines: Int = 1,
|
||||||
fontWeight: FontWeight = FontWeight.Normal
|
fontWeight: FontWeight = FontWeight.Normal,
|
||||||
|
textColor: Color = Color.Unspecified
|
||||||
) {
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
var textHeight by remember { mutableStateOf(0) }
|
||||||
|
if (bookmarked) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Bookmark,
|
||||||
|
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
|
||||||
|
modifier = Modifier
|
||||||
|
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||||
|
tint = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = text,
|
text = text,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = MaterialTheme.typography.h5,
|
style = MaterialTheme.typography.h5,
|
||||||
fontWeight = fontWeight
|
fontWeight = fontWeight,
|
||||||
|
color = textColor,
|
||||||
|
onTextLayout = {
|
||||||
|
textHeight = it.size.height
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaListItemSubtitle(
|
fun MangaListItemSubtitle(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String
|
text: String,
|
||||||
|
textColor: Color = Color.Unspecified
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = text,
|
text = text,
|
||||||
|
color = textColor,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = MaterialTheme.typography.body1
|
style = MaterialTheme.typography.body1
|
||||||
|
|||||||
Reference in New Issue
Block a user