diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 5d1bb16f98..476a387b58 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -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, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List) -> 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, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List) -> 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().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, markAsRead: Boolean) -> Unit, onMarkPreviousAsReadClicked: (Chapter) -> Unit, onMultiDeleteClicked: (List) -> 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().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, + selected: List, onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit, onMultiMarkAsReadClicked: (List, 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, - selected: SnapshotStateList, - selectedPositions: Array, onChapterClicked: (Chapter) -> Unit, onDownloadChapter: ((List, 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, - chapters: List, - selectedPositions: Array, -): 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, chapters: List, - selectedPositions: Array, + 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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 357c497bc4..580ad04bb8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 06463c5ab3..693bdfd135 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -140,6 +140,8 @@ class MangaPresenter( val processedChapters: Sequence? get() = successState?.processedChapters + private val selectedPositions: Array = 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) { 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 }