Add data listener to updates

This commit is contained in:
Syer10
2023-02-02 23:44:23 -05:00
parent a23d9e85c3
commit a943bf638d
5 changed files with 319 additions and 118 deletions

View File

@@ -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())
} }
} }

View File

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

View File

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

View File

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

View File

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