diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index ba3869210..e82e91070 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -1,16 +1,22 @@ package eu.kanade.presentation.history +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material.icons.outlined.FlipToBack +import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter @@ -23,6 +29,7 @@ import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.tachiyomi.ui.history.HistoryScreenModel +import eu.kanade.tachiyomi.ui.history.HistoryScreenModel.HistorySelectionOptions import kotlinx.collections.immutable.persistentListOf import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.i18n.MR @@ -45,45 +52,66 @@ fun HistoryScreen( onClickFavorite: (mangaId: Long) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, // KMK --> + toggleSelectionMode: () -> Unit, + onSelectAll: (Boolean) -> Unit, + onInvertSelection: () -> Unit, + onHistorySelected: (HistoryWithRelations, HistorySelectionOptions) -> Unit, onFilterClicked: () -> Unit, hasActiveFilters: Boolean, usePanoramaCover: Boolean, // KMK <-- ) { + // KMK --> + BackHandler(enabled = state.selectionMode, onBack = toggleSelectionMode) + // KMK <-- + Scaffold( topBar = { scrollBehavior -> - SearchToolbar( - titleContent = { AppBarTitle(stringResource(MR.strings.history)) }, - searchQuery = state.searchQuery, - onChangeSearchQuery = onSearchQueryChange, - actions = { - AppBarActions( - persistentListOf( - // KMK --> - AppBar.Action( - title = stringResource(MR.strings.action_filter), - icon = Icons.Outlined.FilterList, - iconTint = if (hasActiveFilters) MaterialTheme.colorScheme.active else LocalContentColor.current, - onClick = onFilterClicked, + // KMK --> + when { + state.selectionMode -> HistorySelectionToolbar( + selectedCount = state.selection.size, + onCancelActionMode = toggleSelectionMode, + onClickSelectAll = { onSelectAll(true) }, + onClickInvertSelection = onInvertSelection, + onClickClearHistory = { onDialogChange(HistoryScreenModel.Dialog.Delete(state.selected)) }, + ) + // KMK <-- + else -> SearchToolbar( + titleContent = { AppBarTitle(stringResource(MR.strings.history)) }, + searchQuery = state.searchQuery, + onChangeSearchQuery = onSearchQueryChange, + actions = { + AppBarActions( + persistentListOf( + // KMK --> + AppBar.Action( + title = stringResource(MR.strings.action_filter), + icon = Icons.Outlined.FilterList, + iconTint = if (hasActiveFilters) MaterialTheme.colorScheme.active else LocalContentColor.current, + onClick = onFilterClicked, + ), + // KMK <-- + AppBar.Action( + title = stringResource(MR.strings.pref_clear_history), + // KMK --> + icon = Icons.Outlined.Checklist, + onClick = toggleSelectionMode, + // KMK <-- + ), ), - // KMK <-- - AppBar.Action( - title = stringResource(MR.strings.pref_clear_history), - icon = Icons.Outlined.DeleteSweep, - onClick = { - onDialogChange(HistoryScreenModel.Dialog.DeleteAll) - }, - ), - ), - ) - }, - scrollBehavior = scrollBehavior, - ) + ) + }, + scrollBehavior = scrollBehavior, + ) + } }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { contentPadding -> state.list.let { - if (it == null) { + // KMK --> + if (state.isLoading) { + // KMK <-- LoadingScreen(Modifier.padding(contentPadding)) } else if (it.isEmpty()) { val msg = if (!state.searchQuery.isNullOrEmpty()) { @@ -96,14 +124,22 @@ fun HistoryScreen( modifier = Modifier.padding(contentPadding), ) } else { + // KMK --> + val uiModels = remember(state.list) { state.getUiModel() } + // KMK <-- HistoryScreenContent( - history = it, + // KMK --> + state = state, + history = uiModels, + // KMK <-- contentPadding = contentPadding, onClickCover = { history -> onClickCover(history.mangaId) }, onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, onClickFavorite = { history -> onClickFavorite(history.mangaId) }, // KMK --> + selectionMode = state.selectionMode, + onHistorySelected = onHistorySelected, usePanoramaCover = usePanoramaCover, // KMK <-- ) @@ -114,6 +150,9 @@ fun HistoryScreen( @Composable private fun HistoryScreenContent( + // KMK --> + state: HistoryScreenModel.State, + history: List, contentPadding: PaddingValues, onClickCover: (HistoryWithRelations) -> Unit, @@ -121,6 +160,8 @@ private fun HistoryScreenContent( onClickDelete: (HistoryWithRelations) -> Unit, onClickFavorite: (HistoryWithRelations) -> Unit, // KMK --> + selectionMode: Boolean, + onHistorySelected: (HistoryWithRelations, HistorySelectionOptions) -> Unit, usePanoramaCover: Boolean, // KMK <-- ) { @@ -146,14 +187,40 @@ private fun HistoryScreenContent( } is HistoryUiModel.Item -> { val value = item.item + // KMK --> + val isSelected = remember(state.selection) { value.chapterId in state.selection } + // KMK <-- HistoryItem( modifier = Modifier.animateItemFastScroll(), history = value, onClickCover = { onClickCover(value) }, - onClickResume = { onClickResume(value) }, + // KMK --> + onClick = { + when { + selectionMode -> onHistorySelected( + item.item, + HistorySelectionOptions( + selected = !isSelected, + fromLongPress = false, + ), + ) + else -> onClickResume(value) + } + }, + onLongClick = { + onHistorySelected( + item.item, + HistorySelectionOptions( + selected = !isSelected, + fromLongPress = true, + ), + ) + }, + // KMK <-- onClickDelete = { onClickDelete(value) }, onClickFavorite = { onClickFavorite(value) }, // KMK --> + selected = isSelected, readProgress = value.lastPageRead .takeIf { !value.read && it > 0L } ?.let { @@ -174,9 +241,50 @@ private fun HistoryScreenContent( sealed interface HistoryUiModel { data class Header(val date: LocalDate) : HistoryUiModel + // KMK --> data class Item(val item: HistoryWithRelations) : HistoryUiModel + // KMK <-- } +// KMK --> +@Composable +private fun HistorySelectionToolbar( + selectedCount: Int, + onCancelActionMode: () -> Unit, + onClickSelectAll: () -> Unit, + onClickInvertSelection: () -> Unit, + onClickClearHistory: () -> Unit, +) { + AppBar( + titleContent = { Text(text = "$selectedCount") }, + actions = { + AppBarActions( + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_select_all), + icon = Icons.Outlined.SelectAll, + onClick = onClickSelectAll, + ), + AppBar.Action( + title = stringResource(MR.strings.action_select_inverse), + icon = Icons.Outlined.FlipToBack, + onClick = onClickInvertSelection, + ), + AppBar.Action( + title = stringResource(MR.strings.pref_clear_history), + icon = Icons.Outlined.DeleteSweep, + onClick = onClickClearHistory, + enabled = selectedCount > 0, + ), + ), + ) + }, + isActionMode = true, + onCancelActionMode = onCancelActionMode, + ) +} +// KMK <-- + @PreviewLightDark @Composable internal fun HistoryScreenPreviews( @@ -193,6 +301,10 @@ internal fun HistoryScreenPreviews( onDialogChange = {}, onClickFavorite = {}, // KMK --> + toggleSelectionMode = {}, + onSelectAll = {}, + onInvertSelection = {}, + onHistorySelected = { _, _ -> }, onFilterClicked = {}, hasActiveFilters = true, usePanoramaCover = true, diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt index 76c1d4883..cae0a0321 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreenModelStateProvider.kt @@ -8,6 +8,7 @@ import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.manga.model.MangaCover import java.time.Instant import java.time.LocalDate +import java.time.ZoneId import java.time.temporal.ChronoUnit import java.util.Date import kotlin.random.Random @@ -17,13 +18,15 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider + persistentListOf(HistoryWithRelationExamples.headerToday) .asSequence() - .plus(HistoryUiModelExamples.items().take(3)) - .plus(HistoryUiModelExamples.header { it.minus(1, ChronoUnit.DAYS) }) - .plus(HistoryUiModelExamples.items().take(1)) - .plus(HistoryUiModelExamples.header { it.minus(2, ChronoUnit.DAYS) }) - .plus(HistoryUiModelExamples.items().take(7)) + .plus(HistoryWithRelationExamples.items().take(3)) + .plus(HistoryWithRelationExamples.header { it.minus(1, ChronoUnit.DAYS) }) + .plus(HistoryWithRelationExamples.items().take(1)) + .plus(HistoryWithRelationExamples.header { it.minus(2, ChronoUnit.DAYS) }) + .plus(HistoryWithRelationExamples.items().take(7)) + // KMK <-- .toImmutableList(), dialog = null, ) @@ -31,8 +34,10 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider + HistoryWithRelationExamples.headerToday, + HistoryWithRelationExamples.items().first(), + // KMK <-- ), dialog = null, ) @@ -40,8 +45,10 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider + HistoryWithRelationExamples.headerTomorrow, + HistoryWithRelationExamples.items().first(), + // KMK <-- ), dialog = null, ) @@ -58,7 +65,9 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider + isLoading = true, + // KMK <-- dialog = null, ) @@ -71,13 +80,17 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider + private object HistoryWithRelationExamples { + val headerToday = randItem() + val headerTomorrow = randItem(LocalDate.now().plusDays(1).toDate()) fun header(instantBuilder: (Instant) -> Instant = { it }) = - HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now()))) + randItem(LocalDate.from(instantBuilder(Instant.now())).toDate()) + + fun LocalDate.toDate(zone: ZoneId = ZoneId.systemDefault()): Date = + Date.from(atStartOfDay(zone).toInstant()) + // KMK <-- fun items() = sequence { var count = 1 @@ -87,32 +100,35 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider HistoryWithRelations = { it }) = - HistoryUiModel.Item( - historyBuilder( - HistoryWithRelations( - id = Random.nextLong(), - chapterId = Random.nextLong(), + fun randItem( + // KMK --> + readAt: Date = Date.from(Instant.now()), + // KMK <-- + historyBuilder: (HistoryWithRelations) -> HistoryWithRelations = { it }, + ) = + historyBuilder( + HistoryWithRelations( + id = Random.nextLong(), + chapterId = Random.nextLong(), + mangaId = Random.nextLong(), + // SY --> + ogTitle = "Test Title", + // SY <-- + chapterNumber = Random.nextDouble(), + // KMK --> + read = Random.nextBoolean(), + lastPageRead = Random.nextLong(1, 10), + totalCountCalculated = Random.nextLong(1, 100), + readCountCalculated = 1, + readAt = readAt, + // KMK <-- + readDuration = Random.nextLong(), + coverData = MangaCover( mangaId = Random.nextLong(), - // SY --> - ogTitle = "Test Title", - // SY <-- - chapterNumber = Random.nextDouble(), - // KMK --> - read = Random.nextBoolean(), - lastPageRead = Random.nextLong(1, 10), - totalCountCalculated = Random.nextLong(1, 100), - readCountCalculated = 1, - // KMK <-- - readAt = Date.from(Instant.now()), - readDuration = Random.nextLong(), - coverData = MangaCover( - mangaId = Random.nextLong(), - sourceId = Random.nextLong(), - isMangaFavorite = Random.nextBoolean(), - ogUrl = "https://example.com/cover.png", - lastModified = Random.nextLong(), - ), + sourceId = Random.nextLong(), + isMangaFavorite = Random.nextBoolean(), + ogUrl = "https://example.com/cover.png", + lastModified = Random.nextLong(), ), ), ) diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt index e6b33c401..eece59f1f 100644 --- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt +++ b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt @@ -1,6 +1,6 @@ package eu.kanade.presentation.history.components -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight @@ -23,6 +23,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewLightDark @@ -42,6 +44,7 @@ import tachiyomi.i18n.kmk.KMR import tachiyomi.presentation.core.components.material.DISABLED_ALPHA import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.selectedBackground private val HistoryItemHeight = 96.dp @@ -49,11 +52,15 @@ private val HistoryItemHeight = 96.dp fun HistoryItem( history: HistoryWithRelations, onClickCover: () -> Unit, - onClickResume: () -> Unit, + // KMK --> + onClick: () -> Unit, + onLongClick: () -> Unit, + // KMK <-- onClickDelete: () -> Unit, onClickFavorite: () -> Unit, modifier: Modifier = Modifier, // KMK --> + selected: Boolean, readProgress: String?, hasUnread: Boolean, usePanoramaCover: Boolean, @@ -61,11 +68,21 @@ fun HistoryItem( // KMK <-- ) { // KMK --> + val haptic = LocalHapticFeedback.current val textAlpha = if (history.read) DISABLED_ALPHA else 1f // KMK <-- Row( modifier = modifier - .clickable(onClick = onClickResume) + // KMK --> + .selectedBackground(selected) + .combinedClickable( + onClick = onClick, + onLongClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() + }, + ) + // KMK <-- .height(HistoryItemHeight) .padding(horizontal = MaterialTheme.padding.medium, vertical = MaterialTheme.padding.small), verticalAlignment = Alignment.CenterVertically, @@ -78,16 +95,24 @@ fun HistoryItem( if (DebugToggles.HIDE_COVER_IMAGE_ONLY_SHOW_COLOR.enabled) { MangaCoverHide.Book( modifier = Modifier.fillMaxHeight(), - bgColor = bgColor, + bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { selected }, tint = onBgColor, size = MangaCover.Size.Medium, ) } else { if (usePanoramaCover && coverIsWide) { MangaCover.Panorama( - modifier = Modifier.fillMaxHeight(), + modifier = Modifier.fillMaxHeight() + // KMK --> + .combinedClickable( + onClick = onClickCover, + onLongClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() + }, + ), + // KMK <-- data = mangaCover, - onClick = onClickCover, // KMK --> bgColor = bgColor, tint = onBgColor, @@ -101,9 +126,17 @@ fun HistoryItem( } else { // KMK <-- MangaCover.Book( - modifier = Modifier.fillMaxHeight(), + modifier = Modifier.fillMaxHeight() + // KMK --> + .combinedClickable( + onClick = onClickCover, + onLongClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() + }, + ), + // KMK <-- data = mangaCover, - onClick = onClickCover, // KMK --> bgColor = bgColor, tint = onBgColor, @@ -209,12 +242,18 @@ private fun HistoryItemPreviews( HistoryItem( history = historyWithRelations, onClickCover = {}, - onClickResume = {}, + // KMK --> + onClick = {}, + onLongClick = {}, + // KMK <-- onClickDelete = {}, onClickFavorite = {}, readProgress = "Page 5", + // KMK --> hasUnread = true, + selected = true, usePanoramaCover = false, + // KMK <-- ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt index 3aec5caf6..4fb192350 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.history import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable +import androidx.compose.ui.util.fastFilter import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.util.insertSeparators @@ -10,6 +11,7 @@ import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.presentation.history.HistoryUiModel import eu.kanade.tachiyomi.util.lang.toLocalDate import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel @@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority +import mihon.core.common.utils.mutate import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.mapAsCheckboxState @@ -71,6 +74,11 @@ class HistoryScreenModel( private val _events: Channel = Channel(Channel.UNLIMITED) val events: Flow = _events.receiveAsFlow() + // KMK --> + // First and last selected index in list + private val selectedPositions: Array = arrayOf(-1, -1) + // KMK <-- + init { screenModelScope.launch { // KMK --> @@ -97,10 +105,18 @@ class HistoryScreenModel( logcat(LogPriority.ERROR, error) _events.send(Event.InternalError) } - .map { it.toHistoryUiModels() } .flowOn(Dispatchers.IO) } - .collect { newList -> mutableState.update { it.copy(list = newList) } } + .collect { newList -> + mutableState.update { + it.copy( + // KMK --> + isLoading = false, + list = newList.toImmutableList(), + // KMK <-- + ) + } + } } // KMK --> @@ -123,19 +139,6 @@ class HistoryScreenModel( // KMK <-- } - private fun List.toHistoryUiModels(): List { - return map { HistoryUiModel.Item(it) } - .insertSeparators { before, after -> - val beforeDate = before?.item?.readAt?.time?.toLocalDate() - val afterDate = after?.item?.readAt?.time?.toLocalDate() - when { - beforeDate != afterDate && afterDate != null -> HistoryUiModel.Header(afterDate) - // Return null to avoid adding a separator between two items. - else -> null - } - } - } - suspend fun getNextChapter(): Chapter? { return withIOContext { getNextChapters.await(onlyUnread = false).firstOrNull() } } @@ -151,17 +154,21 @@ class HistoryScreenModel( _events.send(Event.OpenChapter(chapter)) } - fun removeFromHistory(history: HistoryWithRelations) { + // KMK --> + fun removeFromHistory(toDelete: List) { screenModelScope.launchIO { - removeHistory.await(history) + removeHistory.await(toDelete.map { it.id }) } + toggleSelectionMode(false) } - fun removeAllFromHistory(mangaId: Long) { + fun removeAllFromHistory(toDelete: List) { screenModelScope.launchIO { - removeHistory.await(mangaId) + removeHistory.awaitManga(toDelete.map { it.mangaId }) } + toggleSelectionMode(false) } + // KMK <-- fun removeAllHistory() { screenModelScope.launchIO { @@ -169,6 +176,7 @@ class HistoryScreenModel( if (!result) return@launchIO _events.send(Event.HistoryCleared) } + toggleSelectionMode(false) } fun updateSearchQuery(query: String?) { @@ -280,6 +288,113 @@ class HistoryScreenModel( } // KMK --> + data class HistorySelectionOptions( + val selected: Boolean, + val fromLongPress: Boolean = false, + ) + + fun toggleSelection( + item: HistoryWithRelations, + selectionOptions: HistorySelectionOptions, + ) { + val (selected, fromLongPress) = selectionOptions + if (item.chapterId in state.value.selection == selected) return + + mutableState.update { state -> + val selection = state.selection.mutate { list -> + state.list.run { + val selectedIndex = indexOfFirst { it.chapterId == item.chapterId } + if (selectedIndex < 0) return@run + + val firstSelection = list.isEmpty() + if (selected) list.add(item.chapterId) else list.remove(item.chapterId) + + if (firstSelection) { + // Since it can go into selectionMode from toolbar (without long press), we need to set both positions here + selectedPositions[0] = selectedIndex + selectedPositions[1] = selectedIndex + } else if (selected && fromLongPress) { + // Try to select the items in-between when possible + val range: IntRange + if (selectedIndex < selectedPositions[0]) { + range = selectedIndex + 1.. selectedPositions[1]) { + range = (selectedPositions[1] + 1).. selectedPositions[1]) { + selectedPositions[1] = selectedIndex + } + } + } + } + } + state.copy( + selection = selection, + selectionMode = selected || selection.isNotEmpty(), + ) + } + } + + fun toggleAllSelection(selected: Boolean) { + mutableState.update { state -> + val selection = if (selected) { + state.list.mapTo(mutableSetOf()) { it.chapterId } + } else { + emptySet() + } + selectedPositions[0] = -1 + selectedPositions[1] = -1 + state.copy( + selection = selection, + selectionMode = selected, + ) + } + } + + fun invertSelection() { + mutableState.update { state -> + val selection = state.selection.mutate { list -> + state.list.forEach { item -> + if (!list.remove(item.chapterId)) list.add(item.chapterId) + } + } + selectedPositions[0] = -1 + selectedPositions[1] = -1 + state.copy(selection = selection) + } + } + + fun toggleSelectionMode(newMode: Boolean? = null) { + if (newMode == false || state.value.selectionMode) { + toggleAllSelection(false) + } else { + mutableState.update { it.copy(selectionMode = newMode ?: !it.selectionMode) } + } + } + // KMK <-- + private fun getHistoryItemPreferenceFlow(): Flow { return combine( historyPreferences.filterUnfinishedManga().changes(), @@ -309,16 +424,39 @@ class HistoryScreenModel( @Immutable data class State( val searchQuery: String? = null, - val list: List? = null, + // KMK --> + val list: ImmutableList = persistentListOf(), + val isLoading: Boolean = true, + // KMK <-- val dialog: Dialog? = null, // KMk --> + val selection: Set = emptySet(), val hasActiveFilters: Boolean = false, - // KMK <-- - ) + val selectionMode: Boolean = false, + ) { + val selected + get() = list.fastFilter { it.chapterId in selection } + + fun getUiModel() = list.map { HistoryUiModel.Item(it) } + .insertSeparators { before, after -> + val beforeDate = before?.item?.readAt?.time?.toLocalDate() + val afterDate = after?.item?.readAt?.time?.toLocalDate() + when { + beforeDate != afterDate && afterDate != null -> HistoryUiModel.Header(afterDate) + // Return null to avoid adding a separator between two items. + else -> null + } + } + } + // KMK <-- sealed interface Dialog { data object DeleteAll : Dialog - data class Delete(val history: HistoryWithRelations) : Dialog + // KMK --> + data class Delete(val histories: List) : Dialog { + constructor(history: HistoryWithRelations) : this(listOf(history)) + } + // KMK <-- data class DuplicateManga(val manga: Manga, val duplicates: List) : Dialog data class ChangeCategory( val manga: Manga, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt index 42e5b4073..48e608f34 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt @@ -101,6 +101,10 @@ data object HistoryTab : Tab { onDialogChange = screenModel::setDialog, onClickFavorite = screenModel::addFavorite, // KMK --> + toggleSelectionMode = screenModel::toggleSelectionMode, + onSelectAll = screenModel::toggleAllSelection, + onInvertSelection = screenModel::invertSelection, + onHistorySelected = screenModel::toggleSelection, onFilterClicked = screenModel::showFilterDialog, hasActiveFilters = state.hasActiveFilters, usePanoramaCover = usePanoramaCover, @@ -113,11 +117,13 @@ data object HistoryTab : Tab { HistoryDeleteDialog( onDismissRequest = onDismissRequest, onDelete = { all -> + // KMK --> if (all) { - screenModel.removeAllFromHistory(dialog.history.mangaId) + screenModel.removeAllFromHistory(dialog.histories) } else { - screenModel.removeFromHistory(dialog.history) + screenModel.removeFromHistory(dialog.histories) } + // KMK <-- }, ) } @@ -169,8 +175,10 @@ data object HistoryTab : Tab { null -> {} } - LaunchedEffect(state.list) { - if (state.list != null) { + // KMK --> + LaunchedEffect(state.isLoading) { + if (!state.isLoading) { + // KMK <-- (context as? MainActivity)?.ready = true // AM (DISCORD) --> diff --git a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt index a86c132ab..1cec34b29 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt @@ -125,9 +125,9 @@ class EHentaiUpdateHelper(context: Context) { ) // Delete the duplicate history first - deleteHistory.forEach { - removeHistory.awaitById(it) - } + // KMK --> + removeHistory.await(deleteHistory) + // KMK <-- // Insert new history newHistory.forEach { diff --git a/data/src/main/java/tachiyomi/data/history/HistoryRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/history/HistoryRepositoryImpl.kt index 9829b1c2e..816e161ca 100644 --- a/data/src/main/java/tachiyomi/data/history/HistoryRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/history/HistoryRepositoryImpl.kt @@ -56,17 +56,21 @@ class HistoryRepositoryImpl( return handler.awaitList { historyQueries.getHistoryByMangaId(mangaId, HistoryMapper::mapHistory) } } - override suspend fun resetHistory(historyId: Long) { + // KMK --> + override suspend fun resetHistory(historyIds: List) { try { - handler.await { historyQueries.resetHistoryById(historyId) } + handler.await { historyQueries.resetHistoryByIds(historyIds) } + // KMK <-- } catch (e: Exception) { logcat(LogPriority.ERROR, throwable = e) } } - override suspend fun resetHistoryByMangaId(mangaId: Long) { + // KMK --> + override suspend fun resetHistoryByMangaIds(mangaIds: List) { try { - handler.await { historyQueries.resetHistoryByMangaId(mangaId) } + handler.await { historyQueries.resetHistoryByMangaIds(mangaIds) } + // KMK <-- } catch (e: Exception) { logcat(LogPriority.ERROR, throwable = e) } diff --git a/data/src/main/sqldelight/tachiyomi/data/history.sq b/data/src/main/sqldelight/tachiyomi/data/history.sq index c35b6b3d9..01ba77e86 100644 --- a/data/src/main/sqldelight/tachiyomi/data/history.sq +++ b/data/src/main/sqldelight/tachiyomi/data/history.sq @@ -33,12 +33,16 @@ JOIN chapters C ON H.chapter_id = C._id WHERE C.manga_id = :mangaId AND C.url = :chapterUrl AND C._id = H.chapter_id; -resetHistoryById: +-- KMK --> +resetHistoryByIds: +-- KMK <-- UPDATE history SET last_read = 0 -WHERE _id = :historyId; +-- KMK --> +WHERE _id IN :historyIds; -resetHistoryByMangaId: +resetHistoryByMangaIds: +-- KMK <-- UPDATE history SET last_read = 0 WHERE _id IN ( @@ -48,7 +52,9 @@ WHERE _id IN ( ON M._id = C.manga_id INNER JOIN history H ON C._id = H.chapter_id - WHERE M._id = :mangaId + -- KMK --> + WHERE M._id IN :mangaIds +-- KMK <-- ); removeAllHistory: diff --git a/domain/src/main/java/tachiyomi/domain/history/interactor/RemoveHistory.kt b/domain/src/main/java/tachiyomi/domain/history/interactor/RemoveHistory.kt index d5d93734f..793b779e2 100644 --- a/domain/src/main/java/tachiyomi/domain/history/interactor/RemoveHistory.kt +++ b/domain/src/main/java/tachiyomi/domain/history/interactor/RemoveHistory.kt @@ -1,6 +1,5 @@ package tachiyomi.domain.history.interactor -import tachiyomi.domain.history.model.HistoryWithRelations import tachiyomi.domain.history.repository.HistoryRepository class RemoveHistory( @@ -11,17 +10,13 @@ class RemoveHistory( return repository.deleteAllHistory() } - suspend fun await(history: HistoryWithRelations) { - repository.resetHistory(history.id) + // KMK --> + suspend fun await(historyIds: List) { + repository.resetHistory(historyIds) } - suspend fun await(mangaId: Long) { - repository.resetHistoryByMangaId(mangaId) + suspend fun awaitManga(mangaIds: List) { + repository.resetHistoryByMangaIds(mangaIds) } - - // SY --> - suspend fun awaitById(historyId: Long) { - repository.resetHistory(historyId) - } - // SY <-- + // KMK <-- } diff --git a/domain/src/main/java/tachiyomi/domain/history/repository/HistoryRepository.kt b/domain/src/main/java/tachiyomi/domain/history/repository/HistoryRepository.kt index 43c5c8cd2..cdf82be30 100644 --- a/domain/src/main/java/tachiyomi/domain/history/repository/HistoryRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/history/repository/HistoryRepository.kt @@ -22,9 +22,11 @@ interface HistoryRepository { suspend fun getHistoryByMangaId(mangaId: Long): List - suspend fun resetHistory(historyId: Long) + // KMK --> + suspend fun resetHistory(historyIds: List) - suspend fun resetHistoryByMangaId(mangaId: Long) + suspend fun resetHistoryByMangaIds(mangaIds: List) + // KMK <-- suspend fun deleteAllHistory(): Boolean