mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-13 08:12:02 +01:00
Add data listener to updates
This commit is contained in:
@@ -33,13 +33,15 @@ class ServerListeners @Inject constructor() {
|
||||
)
|
||||
val mangaListener = _mangaListener.asSharedFlow()
|
||||
|
||||
private val chapterIndexesListener = MutableSharedFlow<Pair<Long, List<Int>?>>(
|
||||
private val _chapterIndexesListener = MutableSharedFlow<Pair<Long, List<Int>?>>(
|
||||
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
|
||||
)
|
||||
val chapterIdsListener = _chapterIdsListener.asSharedFlow()
|
||||
|
||||
private val categoryMangaListener = MutableSharedFlow<Long>(
|
||||
extraBufferCapacity = Channel.UNLIMITED
|
||||
@@ -87,14 +89,14 @@ class ServerListeners @Inject constructor() {
|
||||
idPredate: (suspend (Long?, List<Long>) -> Boolean)? = null
|
||||
): Flow<T> {
|
||||
val indexListener = if (indexPredate != null) {
|
||||
chapterIndexesListener.filter { indexPredate(it.first, it.second) }.startWith(Unit)
|
||||
_chapterIndexesListener.filter { indexPredate(it.first, it.second) }.startWith(Unit)
|
||||
} else {
|
||||
chapterIndexesListener.startWith(Unit)
|
||||
_chapterIndexesListener.startWith(Unit)
|
||||
}
|
||||
val idsListener = if (idPredate != null) {
|
||||
chapterIdsListener.filter { idPredate(it.first, it.second) }.startWith(Unit)
|
||||
_chapterIdsListener.filter { idPredate(it.first, it.second) }.startWith(Unit)
|
||||
} else {
|
||||
chapterIdsListener.startWith(Unit)
|
||||
_chapterIdsListener.startWith(Unit)
|
||||
}
|
||||
|
||||
return combine(indexListener, idsListener) { _, _ -> }
|
||||
@@ -104,25 +106,25 @@ class ServerListeners @Inject constructor() {
|
||||
|
||||
fun updateChapters(mangaId: Long, chapterIndexes: List<Int>) {
|
||||
scope.launch {
|
||||
chapterIndexesListener.emit(mangaId to chapterIndexes.ifEmpty { null })
|
||||
_chapterIndexesListener.emit(mangaId to chapterIndexes.ifEmpty { null })
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChapters(mangaId: Long, vararg chapterIndexes: Int) {
|
||||
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>) {
|
||||
scope.launch {
|
||||
chapterIdsListener.emit(mangaId to chapterIds)
|
||||
_chapterIdsListener.emit(mangaId to chapterIds)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChapters(mangaId: Long?, vararg chapterIds: Long) {
|
||||
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
|
||||
|
||||
import ca.gosyer.jui.core.lang.launchDefault
|
||||
import ca.gosyer.jui.domain.chapter.interactor.BatchUpdateChapter
|
||||
import ca.gosyer.jui.domain.chapter.interactor.DeleteChapterDownload
|
||||
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.updates.interactor.GetRecentUpdates
|
||||
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.uicore.vm.ContextWrapper
|
||||
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.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
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 org.lighthousegames.logging.logging
|
||||
|
||||
@@ -51,25 +48,27 @@ class UpdatesScreenViewModel @Inject constructor(
|
||||
private val batchUpdateChapter: BatchUpdateChapter,
|
||||
private val batchChapterDownload: BatchChapterDownload,
|
||||
private val updateLibrary: UpdateLibrary,
|
||||
private val updatesPager: UpdatesPager,
|
||||
contextWrapper: ContextWrapper
|
||||
) : ViewModel(contextWrapper) {
|
||||
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
private val _updates = MutableStateFlow<ImmutableList<UpdatesUI>>(persistentListOf())
|
||||
val updates = _updates.asStateFlow()
|
||||
|
||||
private val currentPage = MutableStateFlow(1)
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
|
||||
private val updatesMutex = Mutex()
|
||||
private var downloadServiceJob: Job? = null
|
||||
val updates = updatesPager.updates.map {
|
||||
it.map {
|
||||
when (it) {
|
||||
is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date)
|
||||
is UpdatesPager.Updates.Update -> UpdatesUI.Item(ChapterDownloadItem(it.manga, it.chapter))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf())
|
||||
val selectedItems = combine(updates, _selectedIds) { updates, selecteditems ->
|
||||
updates.filterIsInstance<UpdatesUI.Item>()
|
||||
.filter { it.chapterDownloadItem.isSelected(selecteditems) }
|
||||
val selectedItems = combine(updates, _selectedIds) { updates, selectedItems ->
|
||||
updates.asSequence()
|
||||
.filterIsInstance<UpdatesUI.Item>()
|
||||
.filter { it.chapterDownloadItem.isSelected(selectedItems) }
|
||||
.map { it.chapterDownloadItem }
|
||||
.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
@@ -78,78 +77,40 @@ class UpdatesScreenViewModel @Inject constructor(
|
||||
.stateIn(scope, SharingStarted.Eagerly, false)
|
||||
|
||||
init {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
getUpdates(currentPage.value)
|
||||
updatesPager.loadNextPage(
|
||||
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
|
||||
.sortedByDescending { it.chapter.fetchedAt }
|
||||
.map { UpdatesUI.Item(it) }
|
||||
}
|
||||
).toImmutableList()
|
||||
|
||||
downloadServiceJob?.cancel()
|
||||
val mangaIds = _updates.value.filterIsInstance<UpdatesUI.Item>().mapNotNull {
|
||||
.map { updates ->
|
||||
updates.filterIsInstance<UpdatesUI.Item>().mapNotNull {
|
||||
it.chapterDownloadItem.manga?.id
|
||||
}.toSet()
|
||||
downloadServiceJob = DownloadService.registerWatches(mangaIds)
|
||||
}
|
||||
.combine(DownloadService.downloadQueue) { mangaIds, queue ->
|
||||
mangaIds to queue
|
||||
}
|
||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.onEach { chapters ->
|
||||
_updates.value.filterIsInstance<UpdatesUI.Item>().forEach {
|
||||
.onEach { (mangaIds, queue) ->
|
||||
val chapters = queue.filter { it.mangaId in mangaIds }
|
||||
updates.value.filterIsInstance<UpdatesUI.Item>().forEach {
|
||||
it.chapterDownloadItem.updateFrom(chapters)
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
hasNextPage.value = updates.hasNextPage
|
||||
_isLoading.value = false
|
||||
}
|
||||
.catch {
|
||||
fun loadNextPage() {
|
||||
updatesPager.loadNextPage(
|
||||
onError = {
|
||||
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) {
|
||||
@@ -183,7 +144,7 @@ class UpdatesScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun deleteDownloadedChapter(chapter: Chapter?) {
|
||||
scope.launch {
|
||||
scope.launchDefault {
|
||||
if (chapter == null) {
|
||||
val selectedIds = _selectedIds.value
|
||||
batchUpdateChapter.await(selectedIds, delete = true, onError = { toast(it.message.orEmpty()) })
|
||||
@@ -191,9 +152,9 @@ class UpdatesScreenViewModel @Inject constructor(
|
||||
it.setNotDownloaded()
|
||||
}
|
||||
_selectedIds.value = persistentListOf()
|
||||
return@launch
|
||||
return@launchDefault
|
||||
}
|
||||
_updates.value
|
||||
updates.value
|
||||
.filterIsInstance<UpdatesUI.Item>()
|
||||
.find { (chapterDownloadItem) ->
|
||||
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
||||
@@ -205,8 +166,8 @@ class UpdatesScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun stopDownloadingChapter(chapter: Chapter) {
|
||||
scope.launch {
|
||||
_updates.value
|
||||
scope.launchDefault {
|
||||
updates.value
|
||||
.filterIsInstance<UpdatesUI.Item>()
|
||||
.find { (chapterDownloadItem) ->
|
||||
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
||||
@@ -218,7 +179,7 @@ class UpdatesScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
scope.launch {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
||||
.map { it.chapterDownloadItem.chapter.id }
|
||||
.toImmutableList()
|
||||
@@ -226,7 +187,7 @@ class UpdatesScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
scope.launch {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
||||
.map { it.chapterDownloadItem.chapter.id }
|
||||
.minus(_selectedIds.value)
|
||||
@@ -235,24 +196,29 @@ class UpdatesScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun selectChapter(id: Long) {
|
||||
scope.launch {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = _selectedIds.value.plus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
fun unselectChapter(id: Long) {
|
||||
scope.launch {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = _selectedIds.value.minus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
scope.launch {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -88,6 +89,13 @@ fun UpdatesItem(
|
||||
data = manga,
|
||||
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(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
@@ -96,10 +104,13 @@ fun UpdatesItem(
|
||||
) {
|
||||
MangaListItemTitle(
|
||||
text = manga.title,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
bookmarked = chapter.bookmarked,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textColor = textColor
|
||||
)
|
||||
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.Row
|
||||
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.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun MangaListItem(
|
||||
@@ -65,27 +80,48 @@ fun MangaListItemColumn(
|
||||
fun MangaListItemTitle(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
bookmarked: Boolean = false,
|
||||
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(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
maxLines = maxLines,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h5,
|
||||
fontWeight = fontWeight
|
||||
fontWeight = fontWeight,
|
||||
color = textColor,
|
||||
onTextLayout = {
|
||||
textHeight = it.size.height
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MangaListItemSubtitle(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String
|
||||
text: String,
|
||||
textColor: Color = Color.Unspecified
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.body1
|
||||
|
||||
Reference in New Issue
Block a user