diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt index a0e6924c50..74d760587a 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index d966437a14..15897da79c 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -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) -> 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 <-- } diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt index 5229ccea19..c7ec4a15eb 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesUiItem.kt @@ -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, + // KMK --> + expandedState: Set, + 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 <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index ea03a19ea8..4ebbe02233 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -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 @@ -372,18 +373,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 = persistentListOf(), + // KMK --> + val expandedState: Set = persistentSetOf(), + // KMK <-- val dialog: Dialog? = null, ) { val selected = items.filter { it.selected } val selectionMode = selected.isNotEmpty() fun getUiModel(): List { - 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() @@ -393,6 +419,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 <-- } } @@ -419,3 +460,7 @@ data class UpdatesItem( } // SY <-- } + +// KMK --> +fun UpdatesWithRelations.groupByDateAndManga() = "${dateFetch.toLocalDate().toEpochDay()}-$mangaId" +// KMK <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index 4dbb405a41..99592ed308 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -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) }