feat(history): Allow multi/range-selection and batch reset for history items (#1437)

* feat(history): Allow multi-selection and batch reset for history items

* Use chapterId instead of history.id

* use remember getUiModel

* Toolbar selection mode

* cleanup History

* Replace selectedChapterIds with selection for chapter selection state

* Remove userSelected parameter from selection logic and related function signatures

* remove parentheses

* Fix range selection

* longclick on cover too

* disable clear-history button when none-selected
This commit is contained in:
Cuong-Tran 2026-02-02 11:21:32 +07:00 committed by GitHub
parent f373818d93
commit 8f57f4a5a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 450 additions and 130 deletions

View file

@ -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,14 +52,32 @@ 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(
// 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,
@ -69,21 +94,24 @@ fun HistoryScreen(
// KMK <--
AppBar.Action(
title = stringResource(MR.strings.pref_clear_history),
icon = Icons.Outlined.DeleteSweep,
onClick = {
onDialogChange(HistoryScreenModel.Dialog.DeleteAll)
},
// KMK -->
icon = Icons.Outlined.Checklist,
onClick = toggleSelectionMode,
// KMK <--
),
),
)
},
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<HistoryUiModel>,
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,

View file

@ -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<HistoryScreenMo
private val multiPage = HistoryScreenModel.State(
searchQuery = null,
list =
persistentListOf(HistoryUiModelExamples.headerToday)
// KMK -->
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<HistoryScreenMo
private val shortRecent = HistoryScreenModel.State(
searchQuery = null,
list = persistentListOf(
HistoryUiModelExamples.headerToday,
HistoryUiModelExamples.items().first(),
// KMK -->
HistoryWithRelationExamples.headerToday,
HistoryWithRelationExamples.items().first(),
// KMK <--
),
dialog = null,
)
@ -40,8 +45,10 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
private val shortFuture = HistoryScreenModel.State(
searchQuery = null,
list = persistentListOf(
HistoryUiModelExamples.headerTomorrow,
HistoryUiModelExamples.items().first(),
// KMK -->
HistoryWithRelationExamples.headerTomorrow,
HistoryWithRelationExamples.items().first(),
// KMK <--
),
dialog = null,
)
@ -58,7 +65,9 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
private val loading = HistoryScreenModel.State(
searchQuery = null,
list = null,
// KMK -->
isLoading = true,
// KMK <--
dialog = null,
)
@ -71,13 +80,17 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
loading,
)
private object HistoryUiModelExamples {
val headerToday = header()
val headerTomorrow =
HistoryUiModel.Header(LocalDate.now().plusDays(1))
// KMK -->
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,8 +100,12 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
}
}
fun randItem(historyBuilder: (HistoryWithRelations) -> HistoryWithRelations = { it }) =
HistoryUiModel.Item(
fun randItem(
// KMK -->
readAt: Date = Date.from(Instant.now()),
// KMK <--
historyBuilder: (HistoryWithRelations) -> HistoryWithRelations = { it },
) =
historyBuilder(
HistoryWithRelations(
id = Random.nextLong(),
@ -103,8 +120,8 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
lastPageRead = Random.nextLong(1, 10),
totalCountCalculated = Random.nextLong(1, 100),
readCountCalculated = 1,
readAt = readAt,
// KMK <--
readAt = Date.from(Instant.now()),
readDuration = Random.nextLong(),
coverData = MangaCover(
mangaId = Random.nextLong(),
@ -114,7 +131,6 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
lastModified = Random.nextLong(),
),
),
),
)
}
}

View file

@ -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(),
data = mangaCover,
modifier = Modifier.fillMaxHeight()
// KMK -->
.combinedClickable(
onClick = onClickCover,
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onLongClick()
},
),
// KMK <--
data = mangaCover,
// KMK -->
bgColor = bgColor,
tint = onBgColor,
@ -101,9 +126,17 @@ fun HistoryItem(
} else {
// KMK <--
MangaCover.Book(
modifier = Modifier.fillMaxHeight(),
data = mangaCover,
modifier = Modifier.fillMaxHeight()
// KMK -->
.combinedClickable(
onClick = onClickCover,
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onLongClick()
},
),
// KMK <--
data = mangaCover,
// 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 <--
)
}
}

View file

@ -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<Event> = Channel(Channel.UNLIMITED)
val events: Flow<Event> = _events.receiveAsFlow()
// KMK -->
// First and last selected index in list
private val selectedPositions: Array<Int> = 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<HistoryWithRelations>.toHistoryUiModels(): List<HistoryUiModel> {
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<HistoryWithRelations>) {
screenModelScope.launchIO {
removeHistory.await(history)
removeHistory.await(toDelete.map { it.id })
}
toggleSelectionMode(false)
}
fun removeAllFromHistory(mangaId: Long) {
fun removeAllFromHistory(toDelete: List<HistoryWithRelations>) {
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[0]
selectedPositions[0] = selectedIndex
} else if (selectedIndex > selectedPositions[1]) {
range = (selectedPositions[1] + 1)..<selectedIndex
selectedPositions[1] = selectedIndex
} else {
// Just select itself
range = IntRange.EMPTY
}
range.forEach {
val inBetweenItem = get(it)
if (inBetweenItem.chapterId !in list) {
list.add(inBetweenItem.chapterId)
}
}
} else if (!fromLongPress) {
if (!selected) {
if (selectedIndex == selectedPositions[0]) {
selectedPositions[0] = indexOfFirst { it.chapterId in list }
} else if (selectedIndex == selectedPositions[1]) {
selectedPositions[1] = indexOfLast { it.chapterId in list }
}
} else {
if (selectedIndex < selectedPositions[0]) {
selectedPositions[0] = selectedIndex
} else if (selectedIndex > 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<ItemPreferences> {
return combine(
historyPreferences.filterUnfinishedManga().changes(),
@ -309,16 +424,39 @@ class HistoryScreenModel(
@Immutable
data class State(
val searchQuery: String? = null,
val list: List<HistoryUiModel>? = null,
// KMK -->
val list: ImmutableList<HistoryWithRelations> = persistentListOf(),
val isLoading: Boolean = true,
// KMK <--
val dialog: Dialog? = null,
// KMk -->
val selection: Set<Long> = emptySet(),
val hasActiveFilters: Boolean = false,
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<HistoryWithRelations>) : Dialog {
constructor(history: HistoryWithRelations) : this(listOf(history))
}
// KMK <--
data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
data class ChangeCategory(
val manga: Manga,

View file

@ -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) -->

View file

@ -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 {

View file

@ -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<Long>) {
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<Long>) {
try {
handler.await { historyQueries.resetHistoryByMangaId(mangaId) }
handler.await { historyQueries.resetHistoryByMangaIds(mangaIds) }
// KMK <--
} catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e)
}

View file

@ -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:

View file

@ -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<Long>) {
repository.resetHistory(historyIds)
}
suspend fun await(mangaId: Long) {
repository.resetHistoryByMangaId(mangaId)
suspend fun awaitManga(mangaIds: List<Long>) {
repository.resetHistoryByMangaIds(mangaIds)
}
// SY -->
suspend fun awaitById(historyId: Long) {
repository.resetHistory(historyId)
}
// SY <--
// KMK <--
}

View file

@ -22,9 +22,11 @@ interface HistoryRepository {
suspend fun getHistoryByMangaId(mangaId: Long): List<History>
suspend fun resetHistory(historyId: Long)
// KMK -->
suspend fun resetHistory(historyIds: List<Long>)
suspend fun resetHistoryByMangaId(mangaId: Long)
suspend fun resetHistoryByMangaIds(mangaIds: List<Long>)
// KMK <--
suspend fun deleteAllHistory(): Boolean