Group update entries together (#610)

* collapsible grouping Updates

* - Reverse chapter order

- Only show cover & marker on the first one

* - Bigger cover & support panorama view
This commit is contained in:
Cuong-Tran 2024-12-31 11:46:53 +07:00 committed by GitHub
parent a6f3a2cdcf
commit 1f3d2cbd3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 226 additions and 27 deletions

View file

@ -99,9 +99,9 @@ enum class MangaCover(val ratio: Float) {
modifier = Modifier
.size(
when (size) {
Size.Big -> 16.dp
Size.Medium -> 24.dp
else -> 32.dp
Size.Big -> COVER_TEMPLATE_SIZE_BIG
Size.Medium -> COVER_TEMPLATE_SIZE_MEDIUM
else -> COVER_TEMPLATE_SIZE_NORMAL
},
)
.align(Alignment.Center),
@ -122,9 +122,9 @@ enum class MangaCover(val ratio: Float) {
modifier = Modifier
.size(
when (size) {
Size.Big -> 16.dp
Size.Medium -> 24.dp
else -> 32.dp
Size.Big -> COVER_TEMPLATE_SIZE_BIG
Size.Medium -> COVER_TEMPLATE_SIZE_MEDIUM
else -> COVER_TEMPLATE_SIZE_NORMAL
},
)
.align(Alignment.Center),
@ -149,6 +149,12 @@ enum class MangaCover(val ratio: Float) {
contentScale = scale,
)
}
companion object {
val COVER_TEMPLATE_SIZE_BIG = 16.dp
val COVER_TEMPLATE_SIZE_MEDIUM = 24.dp
val COVER_TEMPLATE_SIZE_NORMAL = 32.dp
}
}
enum class MangaCoverHide(private val ratio: Float) {
@ -200,7 +206,7 @@ enum class MangaCoverHide(private val ratio: Float) {
}
}
internal val RatioSwitchToPanorama = 0.75f
internal const val RatioSwitchToPanorama = 0.75f
internal val CoverPlaceholderColor = Color(0x1F888888)
internal val CoverPlaceholderOnBgColor = Color(0x8F888888)

View file

@ -6,8 +6,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CalendarMonth
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.Panorama
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TopAppBarScrollBehavior
@ -27,10 +29,12 @@ import eu.kanade.presentation.manga.components.MangaBottomActionMenu
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel
import eu.kanade.tachiyomi.ui.updates.groupByDateAndManga
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold
@ -59,7 +63,13 @@ fun UpdateScreen(
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
onOpenChapter: (UpdatesItem) -> Unit,
// KMK -->
collapseToggle: (key: String) -> Unit,
// KMK <--
) {
// KMK -->
val usePanoramaCover = remember { mutableStateOf(false) }
// KMK <--
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
Scaffold(
@ -72,6 +82,10 @@ fun UpdateScreen(
onInvertSelection = { onInvertSelection() },
onCancelActionMode = { onSelectAll(false) },
scrollBehavior = scrollBehavior,
// KMK -->
usePanoramaCover = usePanoramaCover.value,
usePanoramaCoverClick = { usePanoramaCover.value = !usePanoramaCover.value },
// KMK
)
},
bottomBar = {
@ -116,7 +130,19 @@ fun UpdateScreen(
updatesLastUpdatedItem(lastUpdated)
updatesUiItems(
uiModels = state.getUiModel(),
uiModels = state.getUiModel()
// KMK -->
.filter {
when (it) {
is UpdatesUiModel.Header, is UpdatesUiModel.Leader -> true
is UpdatesUiModel.Item ->
state.expandedState.contains(it.item.update.groupByDateAndManga())
}
},
expandedState = state.expandedState,
collapseToggle = collapseToggle,
usePanoramaCover = usePanoramaCover.value,
// KMK <--
selectionMode = state.selectionMode,
// SY -->
preserveReadingPosition = preserveReadingPosition,
@ -143,6 +169,10 @@ private fun UpdatesAppBar(
onInvertSelection: () -> Unit,
onCancelActionMode: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
// KMK -->
usePanoramaCover: Boolean,
usePanoramaCoverClick: () -> Unit,
// KMK <--
modifier: Modifier = Modifier,
) {
AppBar(
@ -151,6 +181,14 @@ private fun UpdatesAppBar(
actions = {
AppBarActions(
persistentListOf(
// KMK -->
AppBar.Action(
title = stringResource(KMR.strings.action_panorama_cover),
icon = Icons.Outlined.Panorama,
iconTint = MaterialTheme.colorScheme.primary.takeIf { usePanoramaCover },
onClick = usePanoramaCoverClick,
),
// KMK <--
AppBar.Action(
title = stringResource(MR.strings.action_view_upcoming),
icon = Icons.Outlined.CalendarMonth,
@ -222,5 +260,9 @@ private fun UpdatesBottomBar(
sealed interface UpdatesUiModel {
data class Header(val date: LocalDate) : UpdatesUiModel
data class Item(val item: UpdatesItem) : UpdatesUiModel
open class Item(open val item: UpdatesItem, open val isExpandable: Boolean = false) : UpdatesUiModel
// KMK -->
/** The first [Item] in a group of chapters from same manga */
data class Leader(override val item: UpdatesItem, override val isExpandable: Boolean) : Item(item)
// KMK <--
}

View file

@ -5,9 +5,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
@ -15,12 +15,17 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.Circle
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableFloatState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -38,10 +43,12 @@ import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
import eu.kanade.presentation.manga.components.DotSeparatorText
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.manga.components.RatioSwitchToPanorama
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.updates.UpdatesItem
import eu.kanade.tachiyomi.ui.updates.groupByDateAndManga
import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ListGroupHeader
@ -69,6 +76,11 @@ internal fun LazyListScope.updatesLastUpdatedItem(
internal fun LazyListScope.updatesUiItems(
uiModels: List<UpdatesUiModel>,
// KMK -->
expandedState: Set<String>,
collapseToggle: (key: String) -> Unit,
usePanoramaCover: Boolean,
// KMK <--
selectionMode: Boolean,
// SY -->
preserveReadingPosition: Boolean,
@ -96,7 +108,10 @@ internal fun LazyListScope.updatesUiItems(
when (item) {
is UpdatesUiModel.Header -> {
ListGroupHeader(
modifier = Modifier.animateItemFastScroll(),
modifier = Modifier.animateItemFastScroll()
// KMK -->
.padding(top = MaterialTheme.padding.extraSmall),
// KMK <--
text = relativeDateText(item.date),
)
}
@ -135,6 +150,13 @@ internal fun LazyListScope.updatesUiItems(
}.takeIf { !selectionMode },
downloadStateProvider = updatesItem.downloadStateProvider,
downloadProgressProvider = updatesItem.downloadProgressProvider,
// KMK -->
isLeader = item is UpdatesUiModel.Leader,
isExpandable = item.isExpandable,
expanded = expandedState.contains(updatesItem.update.groupByDateAndManga()),
collapseToggle = collapseToggle,
usePanoramaCover = usePanoramaCover,
// KMK <--
)
}
}
@ -153,6 +175,14 @@ private fun UpdatesUiItem(
// Download Indicator
downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int,
// KMK -->
isLeader: Boolean,
isExpandable: Boolean,
expanded: Boolean,
collapseToggle: (key: String) -> Unit,
usePanoramaCover: Boolean,
coverRatio: MutableFloatState = remember { mutableFloatStateOf(1f) },
// KMK <--
modifier: Modifier = Modifier,
) {
val haptic = LocalHapticFeedback.current
@ -168,27 +198,64 @@ private fun UpdatesUiItem(
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
)
.height(56.dp)
.padding(horizontal = MaterialTheme.padding.medium),
.padding(
// KMK -->
vertical = MaterialTheme.padding.extraSmall,
// KMK <--
horizontal = MaterialTheme.padding.medium,
),
verticalAlignment = Alignment.CenterVertically,
) {
// KMK -->
val mangaCover = update.coverData
val coverIsWide = coverRatio.floatValue <= RatioSwitchToPanorama
val bgColor = mangaCover.dominantCoverColors?.first?.let { Color(it) }
val onBgColor = mangaCover.dominantCoverColors?.second
// KMK <--
MangaCover.Square(
modifier = Modifier
.padding(vertical = 6.dp)
.fillMaxHeight(),
data = mangaCover,
onClick = onClickCover,
// KMK -->
bgColor = bgColor,
tint = onBgColor,
size = MangaCover.Size.Big,
if (isLeader) {
if (usePanoramaCover && coverIsWide) {
MangaCover.Panorama(
modifier = Modifier
.padding(top = MaterialTheme.padding.small)
.width(UpdateItemPanoramaWidth),
data = mangaCover,
onClick = onClickCover,
// KMK -->
bgColor = bgColor,
tint = onBgColor,
size = MangaCover.Size.Medium,
onCoverLoaded = { _, result ->
val image = result.result.image
coverRatio.floatValue = image.height.toFloat() / image.width
},
// KMK <--
)
} else {
// KMK <--
MangaCover.Book(
modifier = Modifier
// KMK -->
.padding(top = MaterialTheme.padding.small)
.width(UpdateItemWidth),
// KMK <--
data = mangaCover,
onClick = onClickCover,
// KMK -->
bgColor = bgColor,
tint = onBgColor,
size = MangaCover.Size.Medium,
onCoverLoaded = { _, result ->
val image = result.result.image
coverRatio.floatValue = image.height.toFloat() / image.width
},
)
}
} else {
Box(
modifier = Modifier
.width(if (usePanoramaCover && coverIsWide) UpdateItemPanoramaWidth else UpdateItemWidth),
)
// KMK <--
)
}
Column(
modifier = Modifier
@ -247,6 +314,15 @@ private fun UpdatesUiItem(
}
}
// KMK -->
if (isLeader && isExpandable) {
CollapseButton(
expanded = expanded,
collapseToggle = { collapseToggle(update.groupByDateAndManga()) },
)
}
// KMK <--
ChapterDownloadIndicator(
enabled = onDownloadChapter != null,
modifier = Modifier.padding(start = 4.dp),
@ -256,3 +332,30 @@ private fun UpdatesUiItem(
)
}
}
// KMK -->
@Composable
fun CollapseButton(
expanded: Boolean,
collapseToggle: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.size(IndicatorSize),
contentAlignment = Alignment.Center,
) {
IconButton(onClick = { collapseToggle() }) {
Icon(
imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}
}
}
private val IndicatorSize = 18.dp
private val UpdateItemPanoramaWidth = 126.dp
private val UpdateItemWidth = 56.dp
// KMK <--

View file

@ -23,6 +23,7 @@ import exh.source.EXH_SOURCE_ID
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
@ -379,18 +380,43 @@ class UpdatesScreenModel(
libraryPreferences.newUpdatesCount().set(0)
}
// KMK -->
fun toggleExpandedState(key: String) {
mutableState.update {
it.copy(
expandedState = it.expandedState.toMutableSet().apply {
if (it.expandedState.contains(key)) remove(key) else add(key)
},
)
}
}
// KMK <--
@Immutable
data class State(
val isLoading: Boolean = true,
val items: PersistentList<UpdatesItem> = persistentListOf(),
// KMK -->
val expandedState: Set<String> = persistentSetOf(),
// KMK <--
val dialog: Dialog? = null,
) {
val selected = items.filter { it.selected }
val selectionMode = selected.isNotEmpty()
fun getUiModel(): List<UpdatesUiModel> {
return items
.map { UpdatesUiModel.Item(it) }
// KMK -->
var lastMangaId = -1L
// KMK <--
return items.groupBy { it.update.dateFetch.toLocalDate() }
.flatMap { groupDate ->
groupDate.value.groupBy { it.update.mangaId }
.flatMap { groupManga ->
val list = groupManga.value
list.sortedBy { it.update.dateFetch }
.map { UpdatesUiModel.Item(it, list.size > 1) }
}
}
.insertSeparators { before, after ->
val beforeDate = before?.item?.update?.dateFetch?.toLocalDate()
val afterDate = after?.item?.update?.dateFetch?.toLocalDate()
@ -400,6 +426,21 @@ class UpdatesScreenModel(
else -> null
}
}
// KMK -->
.map {
if (it is UpdatesUiModel.Header) {
lastMangaId = -1L
it
} else {
if ((it as UpdatesUiModel.Item).item.update.mangaId != lastMangaId) {
lastMangaId = it.item.update.mangaId
UpdatesUiModel.Leader(it.item, it.isExpandable)
} else {
it
}
}
}
// KMK <--
}
}
@ -426,3 +467,7 @@ data class UpdatesItem(
}
// SY <--
}
// KMK -->
fun UpdatesWithRelations.groupByDateAndManga() = "${dateFetch.toLocalDate().toEpochDay()}-$mangaId"
// KMK <--

View file

@ -94,6 +94,9 @@ data object UpdatesTab : Tab {
context.startActivity(intent)
},
onCalendarClicked = { navigator.push(UpcomingScreen()) },
// KMK -->
collapseToggle = screenModel::toggleExpandedState,
// KMK <--
)
val onDismissDialog = { screenModel.setDialog(null) }