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 modifier = Modifier
.size( .size(
when (size) { when (size) {
Size.Big -> 16.dp Size.Big -> COVER_TEMPLATE_SIZE_BIG
Size.Medium -> 24.dp Size.Medium -> COVER_TEMPLATE_SIZE_MEDIUM
else -> 32.dp else -> COVER_TEMPLATE_SIZE_NORMAL
}, },
) )
.align(Alignment.Center), .align(Alignment.Center),
@ -122,9 +122,9 @@ enum class MangaCover(val ratio: Float) {
modifier = Modifier modifier = Modifier
.size( .size(
when (size) { when (size) {
Size.Big -> 16.dp Size.Big -> COVER_TEMPLATE_SIZE_BIG
Size.Medium -> 24.dp Size.Medium -> COVER_TEMPLATE_SIZE_MEDIUM
else -> 32.dp else -> COVER_TEMPLATE_SIZE_NORMAL
}, },
) )
.align(Alignment.Center), .align(Alignment.Center),
@ -149,6 +149,12 @@ enum class MangaCover(val ratio: Float) {
contentScale = scale, 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) { 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 CoverPlaceholderColor = Color(0x1F888888)
internal val CoverPlaceholderOnBgColor = Color(0x8F888888) 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.Icons
import androidx.compose.material.icons.outlined.CalendarMonth import androidx.compose.material.icons.outlined.CalendarMonth
import androidx.compose.material.icons.outlined.FlipToBack 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.Refresh
import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TopAppBarScrollBehavior 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.data.download.model.Download
import eu.kanade.tachiyomi.ui.updates.UpdatesItem import eu.kanade.tachiyomi.ui.updates.UpdatesItem
import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel import eu.kanade.tachiyomi.ui.updates.UpdatesScreenModel
import eu.kanade.tachiyomi.ui.updates.groupByDateAndManga
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@ -59,7 +63,13 @@ fun UpdateScreen(
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit, onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
onOpenChapter: (UpdatesItem) -> Unit, onOpenChapter: (UpdatesItem) -> Unit,
// KMK -->
collapseToggle: (key: String) -> Unit,
// KMK <--
) { ) {
// KMK -->
val usePanoramaCover = remember { mutableStateOf(false) }
// KMK <--
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) }) BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
Scaffold( Scaffold(
@ -72,6 +82,10 @@ fun UpdateScreen(
onInvertSelection = { onInvertSelection() }, onInvertSelection = { onInvertSelection() },
onCancelActionMode = { onSelectAll(false) }, onCancelActionMode = { onSelectAll(false) },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
// KMK -->
usePanoramaCover = usePanoramaCover.value,
usePanoramaCoverClick = { usePanoramaCover.value = !usePanoramaCover.value },
// KMK
) )
}, },
bottomBar = { bottomBar = {
@ -116,7 +130,19 @@ fun UpdateScreen(
updatesLastUpdatedItem(lastUpdated) updatesLastUpdatedItem(lastUpdated)
updatesUiItems( 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, selectionMode = state.selectionMode,
// SY --> // SY -->
preserveReadingPosition = preserveReadingPosition, preserveReadingPosition = preserveReadingPosition,
@ -143,6 +169,10 @@ private fun UpdatesAppBar(
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
onCancelActionMode: () -> Unit, onCancelActionMode: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior, scrollBehavior: TopAppBarScrollBehavior,
// KMK -->
usePanoramaCover: Boolean,
usePanoramaCoverClick: () -> Unit,
// KMK <--
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
AppBar( AppBar(
@ -151,6 +181,14 @@ private fun UpdatesAppBar(
actions = { actions = {
AppBarActions( AppBarActions(
persistentListOf( 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( AppBar.Action(
title = stringResource(MR.strings.action_view_upcoming), title = stringResource(MR.strings.action_view_upcoming),
icon = Icons.Outlined.CalendarMonth, icon = Icons.Outlined.CalendarMonth,
@ -222,5 +260,9 @@ private fun UpdatesBottomBar(
sealed interface UpdatesUiModel { sealed interface UpdatesUiModel {
data class Header(val date: LocalDate) : 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope 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.Icons
import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.Circle 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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableFloatState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.ChapterDownloadIndicator
import eu.kanade.presentation.manga.components.DotSeparatorText import eu.kanade.presentation.manga.components.DotSeparatorText
import eu.kanade.presentation.manga.components.MangaCover 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.animateItemFastScroll
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.updates.UpdatesItem import eu.kanade.tachiyomi.ui.updates.UpdatesItem
import eu.kanade.tachiyomi.ui.updates.groupByDateAndManga
import tachiyomi.domain.updates.model.UpdatesWithRelations import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ListGroupHeader import tachiyomi.presentation.core.components.ListGroupHeader
@ -69,6 +76,11 @@ internal fun LazyListScope.updatesLastUpdatedItem(
internal fun LazyListScope.updatesUiItems( internal fun LazyListScope.updatesUiItems(
uiModels: List<UpdatesUiModel>, uiModels: List<UpdatesUiModel>,
// KMK -->
expandedState: Set<String>,
collapseToggle: (key: String) -> Unit,
usePanoramaCover: Boolean,
// KMK <--
selectionMode: Boolean, selectionMode: Boolean,
// SY --> // SY -->
preserveReadingPosition: Boolean, preserveReadingPosition: Boolean,
@ -96,7 +108,10 @@ internal fun LazyListScope.updatesUiItems(
when (item) { when (item) {
is UpdatesUiModel.Header -> { is UpdatesUiModel.Header -> {
ListGroupHeader( ListGroupHeader(
modifier = Modifier.animateItemFastScroll(), modifier = Modifier.animateItemFastScroll()
// KMK -->
.padding(top = MaterialTheme.padding.extraSmall),
// KMK <--
text = relativeDateText(item.date), text = relativeDateText(item.date),
) )
} }
@ -135,6 +150,13 @@ internal fun LazyListScope.updatesUiItems(
}.takeIf { !selectionMode }, }.takeIf { !selectionMode },
downloadStateProvider = updatesItem.downloadStateProvider, downloadStateProvider = updatesItem.downloadStateProvider,
downloadProgressProvider = updatesItem.downloadProgressProvider, 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 // Download Indicator
downloadStateProvider: () -> Download.State, downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int, downloadProgressProvider: () -> Int,
// KMK -->
isLeader: Boolean,
isExpandable: Boolean,
expanded: Boolean,
collapseToggle: (key: String) -> Unit,
usePanoramaCover: Boolean,
coverRatio: MutableFloatState = remember { mutableFloatStateOf(1f) },
// KMK <--
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
@ -168,27 +198,64 @@ private fun UpdatesUiItem(
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}, },
) )
.height(56.dp) .padding(
.padding(horizontal = MaterialTheme.padding.medium), // KMK -->
vertical = MaterialTheme.padding.extraSmall,
// KMK <--
horizontal = MaterialTheme.padding.medium,
),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
// KMK --> // KMK -->
val mangaCover = update.coverData val mangaCover = update.coverData
val coverIsWide = coverRatio.floatValue <= RatioSwitchToPanorama
val bgColor = mangaCover.dominantCoverColors?.first?.let { Color(it) } val bgColor = mangaCover.dominantCoverColors?.first?.let { Color(it) }
val onBgColor = mangaCover.dominantCoverColors?.second val onBgColor = mangaCover.dominantCoverColors?.second
// KMK <-- if (isLeader) {
MangaCover.Square( if (usePanoramaCover && coverIsWide) {
modifier = Modifier MangaCover.Panorama(
.padding(vertical = 6.dp) modifier = Modifier
.fillMaxHeight(), .padding(top = MaterialTheme.padding.small)
data = mangaCover, .width(UpdateItemPanoramaWidth),
onClick = onClickCover, data = mangaCover,
// KMK --> onClick = onClickCover,
bgColor = bgColor, // KMK -->
tint = onBgColor, bgColor = bgColor,
size = MangaCover.Size.Big, 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 <-- // KMK <--
) }
Column( Column(
modifier = Modifier modifier = Modifier
@ -247,6 +314,15 @@ private fun UpdatesUiItem(
} }
} }
// KMK -->
if (isLeader && isExpandable) {
CollapseButton(
expanded = expanded,
collapseToggle = { collapseToggle(update.groupByDateAndManga()) },
)
}
// KMK <--
ChapterDownloadIndicator( ChapterDownloadIndicator(
enabled = onDownloadChapter != null, enabled = onDownloadChapter != null,
modifier = Modifier.padding(start = 4.dp), 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.PersistentList
import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -379,18 +380,43 @@ class UpdatesScreenModel(
libraryPreferences.newUpdatesCount().set(0) 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 @Immutable
data class State( data class State(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val items: PersistentList<UpdatesItem> = persistentListOf(), val items: PersistentList<UpdatesItem> = persistentListOf(),
// KMK -->
val expandedState: Set<String> = persistentSetOf(),
// KMK <--
val dialog: Dialog? = null, val dialog: Dialog? = null,
) { ) {
val selected = items.filter { it.selected } val selected = items.filter { it.selected }
val selectionMode = selected.isNotEmpty() val selectionMode = selected.isNotEmpty()
fun getUiModel(): List<UpdatesUiModel> { fun getUiModel(): List<UpdatesUiModel> {
return items // KMK -->
.map { UpdatesUiModel.Item(it) } 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 -> .insertSeparators { before, after ->
val beforeDate = before?.item?.update?.dateFetch?.toLocalDate() val beforeDate = before?.item?.update?.dateFetch?.toLocalDate()
val afterDate = after?.item?.update?.dateFetch?.toLocalDate() val afterDate = after?.item?.update?.dateFetch?.toLocalDate()
@ -400,6 +426,21 @@ class UpdatesScreenModel(
else -> null 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 <-- // SY <--
} }
// KMK -->
fun UpdatesWithRelations.groupByDateAndManga() = "${dateFetch.toLocalDate().toEpochDay()}-$mangaId"
// KMK <--

View file

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