MangaScreen: Save selection state (#7560)

This commit is contained in:
Ivan Iskandar 2022-07-19 03:42:46 +07:00 committed by GitHub
parent 473dc688f0
commit 00519e3b93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 145 additions and 120 deletions

View file

@ -38,8 +38,6 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@ -106,6 +104,11 @@ fun MangaScreen(
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onMultiDeleteClicked: (List<Chapter>) -> Unit,
// Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
MangaScreenSmallImpl(
@ -131,6 +134,9 @@ fun MangaScreen(
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
onMultiDeleteClicked = onMultiDeleteClicked,
onChapterSelected = onChapterSelected,
onAllChapterSelected = onAllChapterSelected,
onInvertSelection = onInvertSelection,
)
} else {
MangaScreenLargeImpl(
@ -157,6 +163,9 @@ fun MangaScreen(
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
onMultiDeleteClicked = onMultiDeleteClicked,
onChapterSelected = onChapterSelected,
onAllChapterSelected = onAllChapterSelected,
onInvertSelection = onInvertSelection,
)
}
}
@ -191,18 +200,21 @@ private fun MangaScreenSmallImpl(
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onMultiDeleteClicked: (List<Chapter>) -> Unit,
// Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val chapterListState = rememberLazyListState()
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
val chapters = remember(state) { state.processedChapters.toList() }
val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
val internalOnBackPressed = {
if (selected.isNotEmpty()) {
selected.clear()
if (chapters.any { it.selected }) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
@ -236,21 +248,14 @@ private fun MangaScreenSmallImpl(
onDownloadClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked,
onMigrateClicked = onMigrateClicked,
actionModeCounter = selected.size,
onSelectAll = {
selected.clear()
selected.addAll(chapters)
},
onInvertSelection = {
val toSelect = chapters - selected
selected.clear()
selected.addAll(toSelect)
},
actionModeCounter = chapters.count { it.selected },
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
)
},
bottomBar = {
SharedMangaBottomActionMenu(
selected = selected,
selected = chapters.filter { it.selected },
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@ -262,7 +267,7 @@ private fun MangaScreenSmallImpl(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
AnimatedVisibility(
visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
visible = chapters.any { !it.chapter.read } && chapters.none { it.selected },
enter = fadeIn(),
exit = fadeOut(),
) {
@ -370,10 +375,9 @@ private fun MangaScreenSmallImpl(
sharedChapterItems(
chapters = chapters,
selected = selected,
selectedPositions = selectedPositions,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected,
)
}
}
@ -412,6 +416,11 @@ fun MangaScreenLargeImpl(
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onMultiDeleteClicked: (List<Chapter>) -> Unit,
// Chapter selection
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current
@ -436,12 +445,10 @@ fun MangaScreenLargeImpl(
) {
val chapterListState = rememberLazyListState()
val chapters = remember(state) { state.processedChapters.toList() }
val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
val internalOnBackPressed = {
if (selected.isNotEmpty()) {
selected.clear()
if (chapters.any { it.selected }) {
onAllChapterSelected(false)
} else {
onBackClicked()
}
@ -454,7 +461,7 @@ fun MangaScreenLargeImpl(
MangaSmallAppBar(
modifier = Modifier.onSizeChanged { onTopBarHeightChanged(it.height) },
title = state.manga.title,
titleAlphaProvider = { if (selected.isEmpty()) 0f else 1f },
titleAlphaProvider = { if (chapters.any { it.selected }) 1f else 0f },
backgroundAlphaProvider = { 1f },
incognitoMode = state.isIncognitoMode,
downloadedOnlyMode = state.isDownloadedOnlyMode,
@ -463,16 +470,9 @@ fun MangaScreenLargeImpl(
onDownloadClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked,
onMigrateClicked = onMigrateClicked,
actionModeCounter = selected.size,
onSelectAll = {
selected.clear()
selected.addAll(chapters)
},
onInvertSelection = {
val toSelect = chapters - selected
selected.clear()
selected.addAll(toSelect)
},
actionModeCounter = chapters.count { it.selected },
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
)
},
bottomBar = {
@ -481,7 +481,7 @@ fun MangaScreenLargeImpl(
contentAlignment = Alignment.BottomEnd,
) {
SharedMangaBottomActionMenu(
selected = selected,
selected = chapters.filter { it.selected },
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
@ -494,7 +494,7 @@ fun MangaScreenLargeImpl(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
AnimatedVisibility(
visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
visible = chapters.any { !it.chapter.read } && chapters.none { it.selected },
enter = fadeIn(),
exit = fadeOut(),
) {
@ -578,10 +578,9 @@ fun MangaScreenLargeImpl(
sharedChapterItems(
chapters = chapters,
selected = selected,
selectedPositions = selectedPositions,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected,
)
}
}
@ -592,7 +591,7 @@ fun MangaScreenLargeImpl(
@Composable
private fun SharedMangaBottomActionMenu(
selected: SnapshotStateList<ChapterItem>,
selected: List<ChapterItem>,
onMultiBookmarkClicked: (List<Chapter>, bookmarked: Boolean) -> Unit,
onMultiMarkAsReadClicked: (List<Chapter>, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
@ -605,33 +604,26 @@ private fun SharedMangaBottomActionMenu(
modifier = Modifier.fillMaxWidth(fillFraction),
onBookmarkClicked = {
onMultiBookmarkClicked.invoke(selected.map { it.chapter }, true)
selected.clear()
}.takeIf { selected.any { !it.chapter.bookmark } },
onRemoveBookmarkClicked = {
onMultiBookmarkClicked.invoke(selected.map { it.chapter }, false)
selected.clear()
}.takeIf { selected.all { it.chapter.bookmark } },
onMarkAsReadClicked = {
onMultiMarkAsReadClicked(selected.map { it.chapter }, true)
selected.clear()
}.takeIf { selected.any { !it.chapter.read } },
onMarkAsUnreadClicked = {
onMultiMarkAsReadClicked(selected.map { it.chapter }, false)
selected.clear()
}.takeIf { selected.any { it.chapter.read } },
onMarkPreviousAsReadClicked = {
onMarkPreviousAsReadClicked(selected[0].chapter)
selected.clear()
}.takeIf { selected.size == 1 },
onDownloadClicked = {
onDownloadChapter!!(selected.toList(), ChapterDownloadAction.START)
selected.clear()
}.takeIf {
onDownloadChapter != null && selected.any { it.downloadState != Download.State.DOWNLOADED }
},
onDeleteClicked = {
onMultiDeleteClicked(selected.map { it.chapter })
selected.clear()
}.takeIf {
onDownloadChapter != null && selected.any { it.downloadState == Download.State.DOWNLOADED }
},
@ -640,10 +632,9 @@ private fun SharedMangaBottomActionMenu(
private fun LazyListScope.sharedChapterItems(
chapters: List<ChapterItem>,
selected: SnapshotStateList<ChapterItem>,
selectedPositions: Array<Int>,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
) {
items(
items = chapters,
@ -658,24 +649,18 @@ private fun LazyListScope.sharedChapterItems(
scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() },
read = chapterItem.chapter.read,
bookmark = chapterItem.chapter.bookmark,
selected = selected.contains(chapterItem),
selected = chapterItem.selected,
downloadStateProvider = { chapterItem.downloadState },
downloadProgressProvider = { chapterItem.downloadProgress },
onLongClick = {
val dispatched = onChapterItemLongClick(
chapterItem = chapterItem,
selected = selected,
chapters = chapters,
selectedPositions = selectedPositions,
)
if (dispatched) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onChapterSelected(chapterItem, !chapterItem.selected, true, true)
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = {
onChapterItemClick(
chapterItem = chapterItem,
selected = selected,
chapters = chapters,
selectedPositions = selectedPositions,
onToggleSelection = { onChapterSelected(chapterItem, !chapterItem.selected, true, false) },
onChapterClicked = onChapterClicked,
)
},
@ -686,72 +671,15 @@ private fun LazyListScope.sharedChapterItems(
}
}
private fun onChapterItemLongClick(
chapterItem: ChapterItem,
selected: MutableList<ChapterItem>,
chapters: List<ChapterItem>,
selectedPositions: Array<Int>,
): Boolean {
if (!selected.contains(chapterItem)) {
val selectedIndex = chapters.indexOf(chapterItem)
if (selected.isEmpty()) {
selected.add(chapterItem)
selectedPositions[0] = selectedIndex
selectedPositions[1] = selectedIndex
return true
}
// Try to select the items in-between when possible
val range: IntRange
if (selectedIndex < selectedPositions[0]) {
range = selectedIndex until selectedPositions[0]
selectedPositions[0] = selectedIndex
} else if (selectedIndex > selectedPositions[1]) {
range = (selectedPositions[1] + 1)..selectedIndex
selectedPositions[1] = selectedIndex
} else {
// Just select itself
range = selectedIndex..selectedIndex
}
range.forEach {
val toAdd = chapters[it]
if (!selected.contains(toAdd)) {
selected.add(toAdd)
}
}
return true
}
return false
}
private fun onChapterItemClick(
chapterItem: ChapterItem,
selected: MutableList<ChapterItem>,
chapters: List<ChapterItem>,
selectedPositions: Array<Int>,
onToggleSelection: (Boolean) -> Unit,
onChapterClicked: (Chapter) -> Unit,
) {
val selectedIndex = chapters.indexOf(chapterItem)
when {
selected.contains(chapterItem) -> {
val removedIndex = chapters.indexOf(chapterItem)
selected.remove(chapterItem)
if (removedIndex == selectedPositions[0]) {
selectedPositions[0] = chapters.indexOfFirst { selected.contains(it) }
} else if (removedIndex == selectedPositions[1]) {
selectedPositions[1] = chapters.indexOfLast { selected.contains(it) }
}
}
selected.isNotEmpty() -> {
if (selectedIndex < selectedPositions[0]) {
selectedPositions[0] = selectedIndex
} else if (selectedIndex > selectedPositions[1]) {
selectedPositions[1] = selectedIndex
}
selected.add(chapterItem)
}
chapterItem.selected -> onToggleSelection(false)
chapters.any { it.selected } -> onToggleSelection(true)
else -> onChapterClicked(chapterItem.chapter)
}
}

View file

@ -142,6 +142,9 @@ class MangaController :
onMultiMarkAsReadClicked = presenter::markChaptersRead,
onMarkPreviousAsReadClicked = presenter::markPreviousChapterRead,
onMultiDeleteClicked = this::deleteChaptersWithConfirmation,
onChapterSelected = presenter::toggleSelection,
onAllChapterSelected = presenter::toggleAllSelection,
onInvertSelection = presenter::invertSelection,
)
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {

View file

@ -140,6 +140,8 @@ class MangaPresenter(
val processedChapters: Sequence<ChapterItem>?
get() = successState?.processedChapters
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
/**
* Helper function to update the UI state only if it's currently in success state
*/
@ -583,6 +585,7 @@ class MangaPresenter(
values = chapters.toTypedArray(),
)
}
toggleAllSelection(false)
}
/**
@ -592,6 +595,7 @@ class MangaPresenter(
fun downloadChapters(chapters: List<DomainChapter>) {
val manga = successState?.manga ?: return
downloadManager.downloadChapters(manga, chapters.map { it.toDbChapter() })
toggleAllSelection(false)
}
/**
@ -605,6 +609,7 @@ class MangaPresenter(
.map { ChapterUpdate(id = it.id, bookmark = bookmarked) }
.let { updateChapter.awaitAll(it) }
}
toggleAllSelection(false)
}
/**
@ -627,12 +632,16 @@ class MangaPresenter(
deletedChapters.forEach {
val index = indexOf(it)
val toAdd = removeAt(index)
.copy(downloadState = Download.State.NOT_DOWNLOADED, downloadProgress = 0)
.copy(
downloadState = Download.State.NOT_DOWNLOADED,
downloadProgress = 0,
)
add(index, toAdd)
}
}
successState.copy(chapters = newChapters)
}
toggleAllSelection(false)
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
}
@ -725,6 +734,89 @@ class MangaPresenter(
}
}
fun toggleSelection(
item: ChapterItem,
selected: Boolean,
userSelected: Boolean = false,
fromLongPress: Boolean = false,
) {
updateSuccessState { successState ->
val modifiedIndex = successState.chapters.indexOfFirst { it.chapter.id == item.chapter.id }
if (modifiedIndex < 0) return@updateSuccessState successState
val oldItem = successState.chapters[modifiedIndex]
if ((oldItem.selected && selected) || (!oldItem.selected && !selected)) return@updateSuccessState successState
val newChapters = successState.chapters.toMutableList().apply {
val firstSelection = none { it.selected }
var newItem = removeAt(modifiedIndex)
add(modifiedIndex, newItem.copy(selected = selected))
if (selected && userSelected && fromLongPress) {
if (firstSelection) {
selectedPositions[0] = modifiedIndex
selectedPositions[1] = modifiedIndex
} else {
// Try to select the items in-between when possible
val range: IntRange
if (modifiedIndex < selectedPositions[0]) {
range = modifiedIndex + 1 until selectedPositions[0]
selectedPositions[0] = modifiedIndex
} else if (modifiedIndex > selectedPositions[1]) {
range = (selectedPositions[1] + 1) until modifiedIndex
selectedPositions[1] = modifiedIndex
} else {
// Just select itself
range = IntRange.EMPTY
}
range.forEach {
newItem = removeAt(it)
add(it, newItem.copy(selected = true))
}
}
} else if (userSelected && !fromLongPress) {
if (!selected) {
if (modifiedIndex == selectedPositions[0]) {
selectedPositions[0] = indexOfFirst { it.selected }
} else if (modifiedIndex == selectedPositions[1]) {
selectedPositions[1] = indexOfLast { it.selected }
}
} else {
if (modifiedIndex < selectedPositions[0]) {
selectedPositions[0] = modifiedIndex
} else if (modifiedIndex > selectedPositions[1]) {
selectedPositions[1] = modifiedIndex
}
}
}
}
successState.copy(chapters = newChapters)
}
}
fun toggleAllSelection(selected: Boolean) {
updateSuccessState { successState ->
val newChapters = successState.chapters.map {
it.copy(selected = selected)
}
selectedPositions[0] = -1
selectedPositions[1] = -1
successState.copy(chapters = newChapters)
}
}
fun invertSelection() {
updateSuccessState { successState ->
val newChapters = successState.chapters.map {
it.copy(selected = !it.selected)
}
selectedPositions[0] = -1
selectedPositions[1] = -1
successState.copy(chapters = newChapters)
}
}
// Chapters list - end
// Track sheet - start
@ -962,6 +1054,8 @@ data class ChapterItem(
val chapterTitleString: String,
val dateUploadString: String?,
val readProgressString: String?,
val selected: Boolean = false,
) {
val isDownloaded = downloadState == Download.State.DOWNLOADED
}