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:
parent
f373818d93
commit
8f57f4a5a0
10 changed files with 450 additions and 130 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) -->
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue