Add Quantity Badge to Upcoming Screen (#1250)

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
Roshan Varughese 2024-10-13 00:51:34 +13:00 committed by GitHub
parent 7c7af72f8c
commit 6b2bba4e54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 73 additions and 12 deletions

View file

@ -5,7 +5,7 @@ import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators( fun <T : R, R : Any> List<T>.insertSeparators(
generator: (T?, T?) -> R?, generator: (before: T?, after: T?) -> R?,
): List<R> { ): List<R> {
if (isEmpty()) return emptyList() if (isEmpty()) return emptyList()
val newList = mutableListOf<R>() val newList = mutableListOf<R>()
@ -19,6 +19,24 @@ fun <T : R, R : Any> List<T>.insertSeparators(
return newList return newList
} }
/**
* Similar to [eu.kanade.core.util.insertSeparators] but iterates from last to first element
*/
fun <T : R, R : Any> List<T>.insertSeparatorsReversed(
generator: (before: T?, after: T?) -> R?,
): List<R> {
if (isEmpty()) return emptyList()
val newList = mutableListOf<R>()
for (i in size downTo 0) {
val after = getOrNull(i)
after?.let(newList::add)
val before = getOrNull(i - 1)
val separator = generator.invoke(before, after)
separator?.let(newList::add)
}
return newList.asReversed()
}
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) { if (shouldAdd) {
add(value) add(value)

View file

@ -1,18 +1,25 @@
package mihon.feature.upcoming package mihon.feature.upcoming
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material3.Badge
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@ -27,9 +34,9 @@ import tachiyomi.core.common.Constants
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.TwoPanelBox import tachiyomi.presentation.core.components.TwoPanelBox
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import java.time.LocalDate import java.time.LocalDate
import java.time.YearMonth import java.time.YearMonth
@ -99,6 +106,33 @@ private fun UpcomingToolbar() {
) )
} }
@Composable
private fun DateHeading(
date: LocalDate,
mangaCount: Int,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = relativeDateText(date),
modifier = Modifier
.padding(MaterialTheme.padding.small)
.padding(start = MaterialTheme.padding.small),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyMedium,
)
Badge(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
) {
Text("$mangaCount")
}
}
}
@Composable @Composable
private fun UpcomingScreenSmallImpl( private fun UpcomingScreenSmallImpl(
listState: LazyListState, listState: LazyListState,
@ -140,7 +174,10 @@ private fun UpcomingScreenSmallImpl(
) )
} }
is UpcomingUIModel.Header -> { is UpcomingUIModel.Header -> {
ListGroupHeader(text = relativeDateText(item.date)) DateHeading(
date = item.date,
mangaCount = item.mangaCount,
)
} }
} }
} }
@ -188,7 +225,10 @@ private fun UpcomingScreenLargeImpl(
) )
} }
is UpcomingUIModel.Header -> { is UpcomingUIModel.Header -> {
ListGroupHeader(text = relativeDateText(item.date)) DateHeading(
date = item.date,
mangaCount = item.mangaCount,
)
} }
} }
} }

View file

@ -4,7 +4,7 @@ import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMapIndexedNotNull import androidx.compose.ui.util.fastMapIndexedNotNull
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.insertSeparators import eu.kanade.core.util.insertSeparatorsReversed
import eu.kanade.tachiyomi.util.lang.toLocalDate import eu.kanade.tachiyomi.util.lang.toLocalDate
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
@ -33,7 +33,7 @@ class UpcomingScreenModel(
val upcomingItems = it.toUpcomingUIModels() val upcomingItems = it.toUpcomingUIModels()
state.copy( state.copy(
items = upcomingItems, items = upcomingItems,
events = it.toEvents(), events = upcomingItems.toEvents(),
headerIndexes = upcomingItems.getHeaderIndexes(), headerIndexes = upcomingItems.getHeaderIndexes(),
) )
} }
@ -42,13 +42,16 @@ class UpcomingScreenModel(
} }
private fun List<Manga>.toUpcomingUIModels(): ImmutableList<UpcomingUIModel> { private fun List<Manga>.toUpcomingUIModels(): ImmutableList<UpcomingUIModel> {
var mangaCount = 0
return fastMap { UpcomingUIModel.Item(it) } return fastMap { UpcomingUIModel.Item(it) }
.insertSeparators { before, after -> .insertSeparatorsReversed { before, after ->
if (after != null) mangaCount++
val beforeDate = before?.manga?.expectedNextUpdate?.toLocalDate() val beforeDate = before?.manga?.expectedNextUpdate?.toLocalDate()
val afterDate = after?.manga?.expectedNextUpdate?.toLocalDate() val afterDate = after?.manga?.expectedNextUpdate?.toLocalDate()
if (beforeDate != afterDate && afterDate != null) { if (beforeDate != afterDate && afterDate != null) {
UpcomingUIModel.Header(afterDate) UpcomingUIModel.Header(afterDate, mangaCount).also { mangaCount = 0 }
} else { } else {
null null
} }
@ -56,9 +59,9 @@ class UpcomingScreenModel(
.toImmutableList() .toImmutableList()
} }
private fun List<Manga>.toEvents(): ImmutableMap<LocalDate, Int> { private fun List<UpcomingUIModel>.toEvents(): ImmutableMap<LocalDate, Int> {
return groupBy { it.expectedNextUpdate?.toLocalDate() ?: LocalDate.MAX } return filterIsInstance<UpcomingUIModel.Header>()
.mapValues { it.value.size } .associate { it.date to it.mangaCount }
.toImmutableMap() .toImmutableMap()
} }

View file

@ -4,6 +4,6 @@ import tachiyomi.domain.manga.model.Manga
import java.time.LocalDate import java.time.LocalDate
sealed interface UpcomingUIModel { sealed interface UpcomingUIModel {
data class Header(val date: LocalDate) : UpcomingUIModel data class Header(val date: LocalDate, val mangaCount: Int) : UpcomingUIModel
data class Item(val manga: Manga) : UpcomingUIModel data class Item(val manga: Manga) : UpcomingUIModel
} }