Przeglądaj źródła

MangaScreen: Save selection state (#7560)

Ivan Iskandar 2 lat temu
rodzic
commit
00519e3b93

+ 47 - 119
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<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)
     }
 }

+ 3 - 0
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) {

+ 95 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt

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