Bladeren bron

Move a few Dialogs to Compose (#7861)

* Move a few Dialogs to Compose

- Separating dialogs that are not needed in the PR for the move to Compose on the Browse Source screen
- ChangeMangaCategoriesDialog and AddDuplicateMangaDialog will be removed in the Browse Source screen PR

* Review changes
Andreas 2 jaren geleden
bovenliggende
commit
2453d1a886
20 gewijzigde bestanden met toevoegingen van 657 en 479 verwijderingen
  1. 55 0
      app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt
  2. 122 0
      app/src/main/java/eu/kanade/presentation/components/ChangeCategoryDialog.kt
  3. 78 0
      app/src/main/java/eu/kanade/presentation/components/DeleteLibraryMangaDialog.kt
  4. 3 0
      app/src/main/java/eu/kanade/presentation/library/LibraryState.kt
  5. 90 0
      app/src/main/java/eu/kanade/presentation/manga/components/DownloadCustomChaptersDialog.kt
  6. 39 0
      app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt
  7. 0 54
      app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt
  8. 48 33
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
  9. 10 2
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
  10. 54 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt
  11. 73 97
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
  12. 84 10
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
  13. 0 75
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt
  14. 0 125
      app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt
  15. 0 9
      app/src/main/res/drawable/ic_chevron_left_black_24dp.xml
  16. 0 9
      app/src/main/res/drawable/ic_chevron_left_double_black_24dp.xml
  17. 0 9
      app/src/main/res/drawable/ic_chevron_right_black_24dp.xml
  18. 0 9
      app/src/main/res/drawable/ic_chevron_right_double_black_24dp.xml
  19. 0 47
      app/src/main/res/layout/download_custom_amount.xml
  20. 1 0
      app/src/main/res/values/strings.xml

+ 55 - 0
app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt

@@ -0,0 +1,55 @@
+package eu.kanade.core.prefs
+
+import androidx.compose.ui.state.ToggleableState
+
+sealed class CheckboxState<T>(open val value: T) {
+    abstract fun next(): CheckboxState<T>
+
+    sealed class State<T>(override val value: T) : CheckboxState<T>(value) {
+        data class Checked<T>(override val value: T) : State<T>(value)
+        data class None<T>(override val value: T) : State<T>(value)
+
+        val isChecked: Boolean
+            get() = this is Checked
+
+        override fun next(): CheckboxState<T> {
+            return when (this) {
+                is Checked -> None(value)
+                is None -> Checked(value)
+            }
+        }
+    }
+    sealed class TriState<T>(override val value: T) : CheckboxState<T>(value) {
+        data class Include<T>(override val value: T) : TriState<T>(value)
+        data class Exclude<T>(override val value: T) : TriState<T>(value)
+        data class None<T>(override val value: T) : TriState<T>(value)
+
+        override fun next(): CheckboxState<T> {
+            return when (this) {
+                is Exclude -> None(value)
+                is Include -> Exclude(value)
+                is None -> Include(value)
+            }
+        }
+
+        fun asState(): ToggleableState {
+            return when (this) {
+                is Exclude -> ToggleableState.Indeterminate
+                is Include -> ToggleableState.On
+                is None -> ToggleableState.Off
+            }
+        }
+    }
+}
+
+inline fun <T> T.asCheckboxState(condition: (T) -> Boolean): CheckboxState.State<T> {
+    return if (condition(this)) {
+        CheckboxState.State.Checked(this)
+    } else {
+        CheckboxState.State.None(this)
+    }
+}
+
+inline fun <T> List<T>.mapAsCheckboxState(condition: (T) -> Boolean): List<CheckboxState.State<T>> {
+    return this.map { it.asCheckboxState(condition) }
+}

+ 122 - 0
app/src/main/java/eu/kanade/presentation/components/ChangeCategoryDialog.kt

@@ -0,0 +1,122 @@
+package eu.kanade.presentation.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.Text
+import androidx.compose.material3.TriStateCheckbox
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import eu.kanade.core.prefs.CheckboxState
+import eu.kanade.domain.category.model.Category
+import eu.kanade.presentation.category.visualName
+import eu.kanade.presentation.util.horizontalPadding
+import eu.kanade.tachiyomi.R
+
+@Composable
+fun ChangeCategoryDialog(
+    initialSelection: List<CheckboxState<Category>>,
+    onDismissRequest: () -> Unit,
+    onEditCategories: () -> Unit,
+    onConfirm: (List<Long>, List<Long>) -> Unit,
+) {
+    if (initialSelection.isEmpty()) {
+        AlertDialog(
+            onDismissRequest = onDismissRequest,
+            confirmButton = {
+                TextButton(
+                    onClick = {
+                        onDismissRequest()
+                        onEditCategories()
+                    },
+                ) {
+                    Text(text = stringResource(id = R.string.action_edit_categories))
+                }
+            },
+            title = {
+                Text(text = stringResource(id = R.string.action_move_category))
+            },
+            text = {
+                Text(text = stringResource(id = R.string.information_empty_category_dialog))
+            },
+        )
+        return
+    }
+    var selection by remember { mutableStateOf(initialSelection) }
+    AlertDialog(
+        onDismissRequest = onDismissRequest,
+        confirmButton = {
+            Row {
+                TextButton(onClick = {
+                    onDismissRequest()
+                    onEditCategories()
+                },) {
+                    Text(text = stringResource(id = R.string.action_edit))
+                }
+                Spacer(modifier = Modifier.weight(1f))
+                TextButton(onClick = onDismissRequest) {
+                    Text(text = stringResource(id = android.R.string.cancel))
+                }
+                TextButton(
+                    onClick = {
+                        onDismissRequest()
+                        onConfirm(
+                            selection.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }.map { it.value.id },
+                            selection.filter { it is CheckboxState.TriState.Exclude }.map { it.value.id },
+                        )
+                    },
+                ) {
+                    Text(text = stringResource(id = R.string.action_add))
+                }
+            }
+        },
+        title = {
+            Text(text = stringResource(id = R.string.action_move_category))
+        },
+        text = {
+            Column {
+                selection.forEach { checkbox ->
+                    Row(
+                        verticalAlignment = Alignment.CenterVertically,
+                    ) {
+                        val onCheckboxChange: (CheckboxState<Category>) -> Unit = {
+                            val index = selection.indexOf(it)
+                            val mutableList = selection.toMutableList()
+                            mutableList.removeAt(index)
+                            mutableList.add(index, it.next())
+                            selection = mutableList.toList()
+                        }
+                        when (checkbox) {
+                            is CheckboxState.TriState -> {
+                                TriStateCheckbox(
+                                    state = checkbox.asState(),
+                                    onClick = { onCheckboxChange(checkbox) },
+                                )
+                            }
+                            is CheckboxState.State -> {
+                                Checkbox(
+                                    checked = checkbox.isChecked,
+                                    onCheckedChange = { onCheckboxChange(checkbox) },
+                                )
+                            }
+                        }
+
+                        Text(
+                            text = checkbox.value.visualName,
+                            modifier = Modifier.padding(horizontal = horizontalPadding),
+                        )
+                    }
+                }
+            }
+        },
+    )
+}

+ 78 - 0
app/src/main/java/eu/kanade/presentation/components/DeleteLibraryMangaDialog.kt

@@ -0,0 +1,78 @@
+package eu.kanade.tachiyomi.ui.library
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.res.stringResource
+import eu.kanade.core.prefs.CheckboxState
+import eu.kanade.tachiyomi.R
+
+@Composable
+fun DeleteLibraryMangaDialog(
+    containsLocalManga: Boolean,
+    onDismissRequest: () -> Unit,
+    onConfirm: (Boolean, Boolean) -> Unit,
+) {
+    var list by remember {
+        mutableStateOf(
+            buildList<CheckboxState.State<Int>> {
+                add(CheckboxState.State.None(R.string.manga_from_library))
+                if (!containsLocalManga) {
+                    add(CheckboxState.State.None(R.string.downloaded_chapters))
+                }
+            },
+        )
+    }
+    AlertDialog(
+        onDismissRequest = onDismissRequest,
+        dismissButton = {
+            TextButton(onClick = onDismissRequest) {
+                Text(text = stringResource(id = android.R.string.cancel))
+            }
+        },
+        confirmButton = {
+            TextButton(
+                onClick = {
+                    onDismissRequest()
+                    onConfirm(
+                        list[0].isChecked,
+                        list.getOrElse(1) { CheckboxState.State.None(0) }.isChecked,
+                    )
+                },
+            ) {
+                Text(text = stringResource(id = android.R.string.ok))
+            }
+        },
+        title = {
+            Text(text = stringResource(id = R.string.action_remove))
+        },
+        text = {
+            Column {
+                list.forEach { state ->
+                    Row(verticalAlignment = Alignment.CenterVertically) {
+                        Checkbox(
+                            checked = state.isChecked,
+                            onCheckedChange = {
+                                val index = list.indexOf(state)
+                                val mutableList = list.toMutableList()
+                                mutableList.removeAt(index)
+                                mutableList.add(index, state.next() as CheckboxState.State<Int>)
+                                list = mutableList.toList()
+                            },
+                        )
+                        Text(text = stringResource(id = state.value))
+                    }
+                }
+            }
+        },
+    )
+}

+ 3 - 0
app/src/main/java/eu/kanade/presentation/library/LibraryState.kt

@@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import eu.kanade.domain.category.model.Category
 import eu.kanade.tachiyomi.data.database.models.LibraryManga
+import eu.kanade.tachiyomi.ui.library.LibraryPresenter
 
 @Stable
 interface LibraryState {
@@ -16,6 +17,7 @@ interface LibraryState {
     val selection: List<LibraryManga>
     val selectionMode: Boolean
     var hasActiveFilters: Boolean
+    var dialog: LibraryPresenter.Dialog?
 }
 
 fun LibraryState(): LibraryState {
@@ -29,4 +31,5 @@ class LibraryStateImpl : LibraryState {
     override var selection: List<LibraryManga> by mutableStateOf(emptyList())
     override val selectionMode: Boolean by derivedStateOf { selection.isNotEmpty() }
     override var hasActiveFilters: Boolean by mutableStateOf(false)
+    override var dialog: LibraryPresenter.Dialog? by mutableStateOf(null)
 }

+ 90 - 0
app/src/main/java/eu/kanade/presentation/manga/components/DownloadCustomChaptersDialog.kt

@@ -0,0 +1,90 @@
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ChevronLeft
+import androidx.compose.material.icons.outlined.ChevronRight
+import androidx.compose.material.icons.outlined.KeyboardDoubleArrowLeft
+import androidx.compose.material.icons.outlined.KeyboardDoubleArrowRight
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import eu.kanade.tachiyomi.R
+
+@Composable
+fun DownloadCustomAmountDialog(
+    maxAmount: Int,
+    onDismissRequest: () -> Unit,
+    onConfirm: (Int) -> Unit,
+) {
+    var amount by remember { mutableStateOf(0) }
+    AlertDialog(
+        onDismissRequest = onDismissRequest,
+        dismissButton = {
+            TextButton(onClick = onDismissRequest) {
+                Text(text = stringResource(id = android.R.string.cancel))
+            }
+        },
+        confirmButton = {
+            TextButton(
+                onClick = {
+                    onDismissRequest()
+                    onConfirm(amount.coerceIn(0, maxAmount))
+                },
+            ) {
+                Text(text = stringResource(id = android.R.string.ok))
+            }
+        },
+        title = {
+            Text(text = stringResource(id = R.string.custom_download))
+        },
+        text = {
+            val onChangeAmount: (Int) -> Unit = { amount = (amount + it).coerceIn(0, maxAmount) }
+            Row(
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                IconButton(
+                    onClick = { onChangeAmount(-10) },
+                    enabled = amount > 10,
+                ) {
+                    Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowLeft, contentDescription = "")
+                }
+                IconButton(
+                    onClick = { onChangeAmount(-1) },
+                    enabled = amount > 0,
+                ) {
+                    Icon(imageVector = Icons.Outlined.ChevronLeft, contentDescription = "")
+                }
+                BasicTextField(
+                    value = amount.toString(),
+                    onValueChange = { onChangeAmount(it.toIntOrNull() ?: 0) },
+                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+                )
+                IconButton(
+                    onClick = { onChangeAmount(1) },
+                    enabled = amount < maxAmount,
+                ) {
+                    Icon(imageVector = Icons.Outlined.ChevronRight, contentDescription = "")
+                }
+                IconButton(
+                    onClick = { onChangeAmount(10) },
+                    enabled = amount < maxAmount,
+                ) {
+                    Icon(imageVector = Icons.Outlined.KeyboardDoubleArrowRight, contentDescription = "")
+                }
+            }
+        },
+    )
+}

+ 39 - 0
app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt

@@ -0,0 +1,39 @@
+package eu.kanade.presentation.manga.components
+
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import eu.kanade.tachiyomi.R
+
+@Composable
+fun DeleteChaptersDialog(
+    onDismissRequest: () -> Unit,
+    onConfirm: () -> Unit,
+) {
+    AlertDialog(
+        onDismissRequest = onDismissRequest,
+        dismissButton = {
+            TextButton(onClick = onDismissRequest) {
+                Text(text = stringResource(id = android.R.string.cancel))
+            }
+        },
+        confirmButton = {
+            TextButton(
+                onClick = {
+                    onDismissRequest()
+                    onConfirm()
+                },
+            ) {
+                Text(text = stringResource(id = android.R.string.ok))
+            }
+        },
+        title = {
+            Text(text = stringResource(id = R.string.are_you_sure))
+        },
+        text = {
+            Text(text = stringResource(id = R.string.confirm_delete_chapters))
+        },
+    )
+}

+ 0 - 54
app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt

@@ -1,54 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.app.Dialog
-import android.os.Bundle
-import com.bluelinelabs.conductor.Controller
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import eu.kanade.domain.manga.model.Manga
-import eu.kanade.domain.manga.model.isLocal
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-
-class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
-    DialogController(bundle) where T : Controller, T : DeleteLibraryMangasDialog.Listener {
-
-    private var mangas = emptyList<Manga>()
-
-    constructor(target: T, mangas: List<Manga>) : this() {
-        this.mangas = mangas
-        targetController = target
-    }
-
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val canDeleteChapters = mangas.any { !it.isLocal() }
-        val items = when (canDeleteChapters) {
-            true -> listOf(
-                R.string.manga_from_library,
-                R.string.downloaded_chapters,
-            )
-            false -> listOf(R.string.manga_from_library)
-        }
-            .map { resources!!.getString(it) }
-            .toTypedArray()
-
-        val selected = items
-            .map { false }
-            .toBooleanArray()
-        return MaterialAlertDialogBuilder(activity!!)
-            .setTitle(R.string.action_remove)
-            .setMultiChoiceItems(items, selected) { _, which, checked ->
-                selected[which] = checked
-            }
-            .setPositiveButton(android.R.string.ok) { _, _ ->
-                val deleteFromLibrary = selected[0]
-                val deleteChapters = canDeleteChapters && selected[1]
-                (targetController as? Listener)?.deleteMangas(mangas, deleteFromLibrary, deleteChapters)
-            }
-            .setNegativeButton(android.R.string.cancel, null)
-            .create()
-    }
-
-    interface Listener {
-        fun deleteMangas(mangas: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean)
-    }
-}

+ 48 - 33
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -8,9 +8,11 @@ import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.platform.LocalContext
 import com.bluelinelabs.conductor.ControllerChangeHandler
 import com.bluelinelabs.conductor.ControllerChangeType
-import eu.kanade.domain.category.model.Category
+import eu.kanade.core.prefs.CheckboxState
 import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.model.isLocal
 import eu.kanade.domain.manga.model.toDbManga
+import eu.kanade.presentation.components.ChangeCategoryDialog
 import eu.kanade.presentation.library.LibraryScreen
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.toDomainManga
@@ -19,20 +21,16 @@ import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
 import eu.kanade.tachiyomi.ui.base.controller.RootController
 import eu.kanade.tachiyomi.ui.base.controller.pushController
 import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
+import eu.kanade.tachiyomi.ui.category.CategoryController
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.util.lang.launchIO
-import eu.kanade.tachiyomi.util.lang.withUIContext
 import eu.kanade.tachiyomi.util.system.toast
-import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
 import kotlinx.coroutines.cancel
 
 class LibraryController(
     bundle: Bundle? = null,
-) : FullComposeController<LibraryPresenter>(bundle),
-    RootController,
-    ChangeMangaCategoriesDialog.Listener,
-    DeleteLibraryMangasDialog.Listener {
+) : FullComposeController<LibraryPresenter>(bundle), RootController {
 
     /**
      * Sheet containing filter/sort/display items.
@@ -65,6 +63,36 @@ class LibraryController(
             onClickSelectAll = { presenter.selectAll(presenter.activeCategory) },
             onClickUnselectAll = ::clearSelection,
         )
+
+        val onDismissRequest = { presenter.dialog = null }
+        when (val dialog = presenter.dialog) {
+            is LibraryPresenter.Dialog.ChangeCategory -> {
+                ChangeCategoryDialog(
+                    initialSelection = dialog.initialSelection,
+                    onDismissRequest = onDismissRequest,
+                    onEditCategories = {
+                        presenter.clearSelection()
+                        router.pushController(CategoryController())
+                    },
+                    onConfirm = { include, exclude ->
+                        presenter.clearSelection()
+                        presenter.setMangaCategories(dialog.manga, include, exclude)
+                    },
+                )
+            }
+            is LibraryPresenter.Dialog.DeleteManga -> {
+                DeleteLibraryMangaDialog(
+                    containsLocalManga = dialog.manga.any(Manga::isLocal),
+                    onDismissRequest = onDismissRequest,
+                    onConfirm = { deleteManga, deleteChapter ->
+                        presenter.removeMangas(dialog.manga.map { it.toDbManga() }, deleteManga, deleteChapter)
+                        presenter.clearSelection()
+                    },
+                )
+            }
+            null -> {}
+        }
+
         LaunchedEffect(presenter.selectionMode) {
             val activity = (activity as? MainActivity) ?: return@LaunchedEffect
             // Could perhaps be removed when navigation is in a Compose world
@@ -169,53 +197,40 @@ class LibraryController(
     private fun showMangaCategoriesDialog() {
         viewScope.launchIO {
             // Create a copy of selected manga
-            val mangas = presenter.selection.toList()
+            val mangaList = presenter.selection.mapNotNull { it.toDomainManga() }.toList()
 
             // Hide the default category because it has a different behavior than the ones from db.
             val categories = presenter.categories.filter { it.id != 0L }
 
             // Get indexes of the common categories to preselect.
-            val common = presenter.getCommonCategories(mangas.mapNotNull { it.toDomainManga() })
+            val common = presenter.getCommonCategories(mangaList)
             // Get indexes of the mix categories to preselect.
-            val mix = presenter.getMixCategories(mangas.mapNotNull { it.toDomainManga() })
+            val mix = presenter.getMixCategories(mangaList)
             val preselected = categories.map {
                 when (it) {
-                    in common -> QuadStateTextView.State.CHECKED.ordinal
-                    in mix -> QuadStateTextView.State.INDETERMINATE.ordinal
-                    else -> QuadStateTextView.State.UNCHECKED.ordinal
+                    in common -> CheckboxState.State.Checked(it)
+                    in mix -> CheckboxState.TriState.Exclude(it)
+                    else -> CheckboxState.State.None(it)
                 }
-            }.toTypedArray()
-            withUIContext {
-                ChangeMangaCategoriesDialog(this@LibraryController, mangas.mapNotNull { it.toDomainManga() }, categories, preselected)
-                    .showDialog(router)
             }
+            presenter.dialog = LibraryPresenter.Dialog.ChangeCategory(mangaList, preselected)
         }
     }
 
     private fun downloadUnreadChapters() {
-        val mangas = presenter.selection.toList()
-        presenter.downloadUnreadChapters(mangas.mapNotNull { it.toDomainManga() })
+        val mangaList = presenter.selection.toList()
+        presenter.downloadUnreadChapters(mangaList.mapNotNull { it.toDomainManga() })
         presenter.clearSelection()
     }
 
     private fun markReadStatus(read: Boolean) {
-        val mangas = presenter.selection.toList()
-        presenter.markReadStatus(mangas.mapNotNull { it.toDomainManga() }, read)
+        val mangaList = presenter.selection.toList()
+        presenter.markReadStatus(mangaList.mapNotNull { it.toDomainManga() }, read)
         presenter.clearSelection()
     }
 
     private fun showDeleteMangaDialog() {
-        val mangas = presenter.selection.toList()
-        DeleteLibraryMangasDialog(this, mangas.mapNotNull { it.toDomainManga() }).showDialog(router)
-    }
-
-    override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
-        presenter.setMangaCategories(mangas, addCategories, removeCategories)
-        presenter.clearSelection()
-    }
-
-    override fun deleteMangas(mangas: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
-        presenter.removeMangas(mangas.map { it.toDbManga() }, deleteFromLibrary, deleteChapters)
-        presenter.clearSelection()
+        val mangaList = presenter.selection.mapNotNull { it.toDomainManga() }.toList()
+        presenter.dialog = LibraryPresenter.Dialog.DeleteManga(mangaList)
     }
 }

+ 10 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -11,6 +11,7 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.util.fastAny
 import com.jakewharton.rxrelay.BehaviorRelay
+import eu.kanade.core.prefs.CheckboxState
 import eu.kanade.core.prefs.PreferenceMutableState
 import eu.kanade.core.util.asFlow
 import eu.kanade.core.util.asObservable
@@ -610,13 +611,15 @@ class LibraryPresenter(
      * @param addCategories the categories to add for all mangas.
      * @param removeCategories the categories to remove in all mangas.
      */
-    fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
+    fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
         presenterScope.launchIO {
             mangaList.map { manga ->
                 val categoryIds = getCategories.await(manga.id)
+                    .map { it.id }
                     .subtract(removeCategories)
                     .plus(addCategories)
-                    .map { it.id }
+                    .toList()
+
                 setMangaCategories.await(manga.id, categoryIds)
             }
         }
@@ -715,4 +718,9 @@ class LibraryPresenter(
         val items = (loadedManga[category.id] ?: emptyList()).map { it.manga }
         state.selection = items.filterNot { it in selection }
     }
+
+    sealed class Dialog {
+        data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
+        data class DeleteManga(val manga: List<Manga>) : Dialog()
+    }
 }

+ 54 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt

@@ -2,10 +2,19 @@ package eu.kanade.tachiyomi.ui.manga
 
 import android.app.Dialog
 import android.os.Bundle
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import com.bluelinelabs.conductor.Controller
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import eu.kanade.domain.manga.model.Manga
 import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.ui.base.controller.DialogController
 import eu.kanade.tachiyomi.ui.base.controller.pushController
@@ -46,3 +55,48 @@ class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle)
             .create()
     }
 }
+
+@Composable
+fun DuplicateDialog(
+    onDismissRequest: () -> Unit,
+    onConfirm: () -> Unit,
+    onOpenManga: () -> Unit,
+    duplicateFrom: Source,
+) {
+    AlertDialog(
+        onDismissRequest = onDismissRequest,
+        confirmButton = {
+            Row {
+                TextButton(onClick = {
+                    onDismissRequest()
+                    onOpenManga()
+                },) {
+                    Text(text = stringResource(id = R.string.action_show_manga))
+                }
+                Spacer(modifier = Modifier.weight(1f))
+                TextButton(onClick = onDismissRequest) {
+                    Text(text = stringResource(id = android.R.string.cancel))
+                }
+                TextButton(
+                    onClick = {
+                        onDismissRequest()
+                        onConfirm()
+                    },
+                ) {
+                    Text(text = stringResource(id = R.string.action_add))
+                }
+            }
+        },
+        title = {
+            Text(text = stringResource(id = R.string.are_you_sure))
+        },
+        text = {
+            Text(
+                text = stringResource(
+                    id = R.string.confirm_manga_add_duplicate,
+                    duplicateFrom.name,
+                ),
+            )
+        },
+    )
+}

+ 73 - 97
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt

@@ -6,26 +6,26 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import androidx.activity.OnBackPressedDispatcherOwner
-import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarResult
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.core.os.bundleOf
 import com.bluelinelabs.conductor.ControllerChangeHandler
 import com.bluelinelabs.conductor.ControllerChangeType
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import eu.kanade.data.chapter.NoChaptersException
-import eu.kanade.domain.category.model.Category
-import eu.kanade.domain.manga.model.Manga
 import eu.kanade.domain.manga.model.toDbManga
+import eu.kanade.presentation.components.ChangeCategoryDialog
 import eu.kanade.presentation.components.ChapterDownloadAction
 import eu.kanade.presentation.components.LoadingScreen
 import eu.kanade.presentation.manga.DownloadAction
 import eu.kanade.presentation.manga.MangaScreen
+import eu.kanade.presentation.manga.components.DeleteChaptersDialog
 import eu.kanade.presentation.util.calculateWindowWidthSizeClass
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.DownloadService
@@ -41,11 +41,12 @@ import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
 import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
-import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
+import eu.kanade.tachiyomi.ui.category.CategoryController
 import eu.kanade.tachiyomi.ui.library.LibraryController
 import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.manga.MangaPresenter.Dialog
 import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersSettingsSheet
-import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog
+import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomAmountDialog
 import eu.kanade.tachiyomi.ui.manga.info.MangaFullCoverDialog
 import eu.kanade.tachiyomi.ui.manga.track.TrackItem
 import eu.kanade.tachiyomi.ui.manga.track.TrackSearchDialog
@@ -54,21 +55,13 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.ui.recent.history.HistoryController
 import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
 import eu.kanade.tachiyomi.ui.webview.WebViewActivity
-import eu.kanade.tachiyomi.util.lang.launchIO
-import eu.kanade.tachiyomi.util.lang.withUIContext
 import eu.kanade.tachiyomi.util.system.logcat
 import eu.kanade.tachiyomi.util.system.toast
-import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
-import eu.kanade.tachiyomi.widget.materialdialogs.await
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
 import logcat.LogPriority
 import eu.kanade.domain.chapter.model.Chapter as DomainChapter
 
-class MangaController :
-    FullComposeController<MangaPresenter>,
-    ChangeMangaCategoriesDialog.Listener,
-    DownloadCustomChaptersDialog.Listener {
+class MangaController : FullComposeController<MangaPresenter> {
 
     @Suppress("unused")
     constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
@@ -112,9 +105,19 @@ class MangaController :
     @Composable
     override fun ComposeContent() {
         val state by presenter.state.collectAsState()
+        val dialog by derivedStateOf {
+            when (val state = state) {
+                MangaScreenState.Loading -> null
+                is MangaScreenState.Success -> state.dialog
+            }
+        }
+
         if (state is MangaScreenState.Success) {
             val successState = state as MangaScreenState.Success
             val isHttpSource = remember { successState.source is HttpSource }
+
+            val scope = rememberCoroutineScope()
+
             MangaScreen(
                 state = successState,
                 snackbarHostState = snackbarHostState,
@@ -133,16 +136,67 @@ class MangaController :
                 onCoverClicked = this::openCoverDialog,
                 onShareClicked = this::shareManga.takeIf { isHttpSource },
                 onDownloadActionClicked = this::runDownloadChapterAction.takeIf { !successState.source.isLocalOrStub() },
-                onEditCategoryClicked = this::onCategoriesClick.takeIf { successState.manga.favorite },
+                onEditCategoryClicked = presenter::promptChangeCategories.takeIf { successState.manga.favorite },
                 onMigrateClicked = this::migrateManga.takeIf { successState.manga.favorite },
                 onMultiBookmarkClicked = presenter::bookmarkChapters,
                 onMultiMarkAsReadClicked = presenter::markChaptersRead,
                 onMarkPreviousAsReadClicked = presenter::markPreviousChapterRead,
-                onMultiDeleteClicked = this::deleteChaptersWithConfirmation,
+                onMultiDeleteClicked = presenter::showDeleteChapterDialog,
                 onChapterSelected = presenter::toggleSelection,
                 onAllChapterSelected = presenter::toggleAllSelection,
                 onInvertSelection = presenter::invertSelection,
             )
+
+            val onDismissRequest = { presenter.dismissDialog() }
+            when (val dialog = dialog) {
+                is Dialog.ChangeCategory -> {
+                    ChangeCategoryDialog(
+                        initialSelection = dialog.initialSelection,
+                        onDismissRequest = onDismissRequest,
+                        onEditCategories = {
+                            router.pushController(CategoryController())
+                        },
+                        onConfirm = { include, _ ->
+                            presenter.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include)
+                        },
+                    )
+                }
+                is Dialog.DeleteChapters -> {
+                    DeleteChaptersDialog(
+                        onDismissRequest = onDismissRequest,
+                        onConfirm = {
+                            deleteChapters(dialog.chapters)
+                        },
+                    )
+                }
+                is Dialog.DownloadCustomAmount -> {
+                    DownloadCustomAmountDialog(
+                        maxAmount = dialog.max,
+                        onDismissRequest = onDismissRequest,
+                        onConfirm = { amount ->
+                            val chaptersToDownload = presenter.getUnreadChaptersSorted().take(amount)
+                            if (chaptersToDownload.isNotEmpty()) {
+                                scope.launch { downloadChapters(chaptersToDownload) }
+                            }
+                        },
+                    )
+                }
+                is Dialog.DuplicateManga -> {
+                    DuplicateDialog(
+                        onDismissRequest = onDismissRequest,
+                        onConfirm = {
+                            presenter.toggleFavorite(
+                                onRemoved = {},
+                                onAdded = {},
+                                checkDuplicate = false,
+                            )
+                        },
+                        onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
+                        duplicateFrom = presenter.getSourceOrStub(dialog.duplicate),
+                    )
+                }
+                null -> {}
+            }
         } else {
             LoadingScreen()
         }
@@ -206,30 +260,10 @@ class MangaController :
         }
     }
 
-    private fun onFavoriteClick(checkDuplicate: Boolean = true) {
+    private fun onFavoriteClick() {
         presenter.toggleFavorite(
             onRemoved = this::onFavoriteRemoved,
             onAdded = { activity?.toast(activity?.getString(R.string.manga_added_library)) },
-            onDuplicateExists = if (checkDuplicate) {
-                {
-                    AddDuplicateMangaDialog(
-                        target = this,
-                        libraryManga = it,
-                        onAddToLibrary = { onFavoriteClick(checkDuplicate = false) },
-                    ).showDialog(router)
-                }
-            } else null,
-            onRequireCategory = { manga, categories ->
-                val ids = runBlocking { presenter.getMangaCategoryIds(manga) }
-                val preselected = categories.map {
-                    if (it.id in ids) {
-                        QuadStateTextView.State.CHECKED.ordinal
-                    } else {
-                        QuadStateTextView.State.UNCHECKED.ordinal
-                    }
-                }.toTypedArray()
-                showChangeCategoryDialog(manga, categories, preselected)
-            },
         )
     }
 
@@ -249,40 +283,6 @@ class MangaController :
         }
     }
 
-    private fun onCategoriesClick() {
-        viewScope.launchIO {
-            val manga = presenter.manga ?: return@launchIO
-            val categories = presenter.getCategories()
-
-            val ids = presenter.getMangaCategoryIds(manga)
-            val preselected = categories.map {
-                if (it.id in ids) {
-                    QuadStateTextView.State.CHECKED.ordinal
-                } else {
-                    QuadStateTextView.State.UNCHECKED.ordinal
-                }
-            }.toTypedArray()
-
-            withUIContext {
-                showChangeCategoryDialog(manga, categories, preselected)
-            }
-        }
-    }
-
-    private fun showChangeCategoryDialog(manga: Manga, categories: List<Category>, preselected: Array<Int>) {
-        ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
-            .showDialog(router)
-    }
-
-    override fun updateCategoriesForMangas(
-        mangas: List<Manga>,
-        addCategories: List<Category>,
-        removeCategories: List<Category>,
-    ) {
-        val changed = mangas.firstOrNull() ?: return
-        presenter.moveMangaToCategoriesAndAddToLibrary(changed, addCategories)
-    }
-
     /**
      * Perform a search using the provided query.
      *
@@ -427,15 +427,6 @@ class MangaController :
         }
     }
 
-    private fun deleteChaptersWithConfirmation(chapters: List<DomainChapter>) {
-        viewScope.launch {
-            val result = MaterialAlertDialogBuilder(activity!!)
-                .setMessage(R.string.confirm_delete_chapters)
-                .await(android.R.string.ok, android.R.string.cancel)
-            if (result == AlertDialog.BUTTON_POSITIVE) deleteChapters(chapters)
-        }
-    }
-
     fun deleteChapters(chapters: List<DomainChapter>) {
         if (chapters.isEmpty()) return
         presenter.deleteChapters(chapters)
@@ -449,7 +440,7 @@ class MangaController :
             DownloadAction.NEXT_5_CHAPTERS -> presenter.getUnreadChaptersSorted().take(5)
             DownloadAction.NEXT_10_CHAPTERS -> presenter.getUnreadChaptersSorted().take(10)
             DownloadAction.CUSTOM -> {
-                showCustomDownloadDialog()
+                presenter.showDownloadCustomDialog()
                 return
             }
             DownloadAction.UNREAD_CHAPTERS -> presenter.getUnreadChapters()
@@ -462,21 +453,6 @@ class MangaController :
         }
     }
 
-    private fun showCustomDownloadDialog() {
-        val availableChapters = presenter.processedChapters?.count() ?: return
-        DownloadCustomChaptersDialog(
-            this,
-            availableChapters,
-        ).showDialog(router)
-    }
-
-    override fun downloadCustomChapters(amount: Int) {
-        val chaptersToDownload = presenter.getUnreadChaptersSorted().take(amount)
-        if (chaptersToDownload.isNotEmpty()) {
-            viewScope.launch { downloadChapters(chaptersToDownload) }
-        }
-    }
-
     // Chapters list - end
 
     // Tracker sheet - start

+ 84 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt

@@ -4,6 +4,8 @@ import android.app.Application
 import android.content.Context
 import android.os.Bundle
 import androidx.compose.runtime.Immutable
+import eu.kanade.core.prefs.CheckboxState
+import eu.kanade.core.prefs.mapAsCheckboxState
 import eu.kanade.domain.category.interactor.GetCategories
 import eu.kanade.domain.category.interactor.SetMangaCategories
 import eu.kanade.domain.category.model.Category
@@ -61,6 +63,7 @@ import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.supervisorScope
 import kotlinx.coroutines.withContext
 import logcat.LogPriority
@@ -78,6 +81,7 @@ class MangaPresenter(
     val isFromSource: Boolean,
     private val preferences: PreferencesHelper = Injekt.get(),
     private val trackManager: TrackManager = Injekt.get(),
+    private val sourceManager: SourceManager = Injekt.get(),
     private val downloadManager: DownloadManager = Injekt.get(),
     private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(),
     private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
@@ -182,6 +186,7 @@ class MangaPresenter(
                     isRefreshingChapter = true,
                     isIncognitoMode = incognitoMode,
                     isDownloadedOnlyMode = downloadedOnlyMode,
+                    dialog = null,
                 )
             }
 
@@ -259,8 +264,7 @@ class MangaPresenter(
     fun toggleFavorite(
         onRemoved: () -> Unit,
         onAdded: () -> Unit,
-        onRequireCategory: (manga: DomainManga, availableCats: List<Category>) -> Unit,
-        onDuplicateExists: ((DomainManga) -> Unit)?,
+        checkDuplicate: Boolean = true,
     ) {
         val state = successState ?: return
         presenterScope.launchIO {
@@ -278,10 +282,16 @@ class MangaPresenter(
             } else {
                 // Add to library
                 // First, check if duplicate exists if callback is provided
-                if (onDuplicateExists != null) {
+                if (checkDuplicate) {
                     val duplicate = getDuplicateLibraryManga.await(manga.title, manga.source)
+
                     if (duplicate != null) {
-                        withUIContext { onDuplicateExists(duplicate) }
+                        _state.update { state ->
+                            when (state) {
+                                MangaScreenState.Loading -> state
+                                is MangaScreenState.Success -> state.copy(dialog = Dialog.DuplicateManga(manga, duplicate))
+                            }
+                        }
                         return@launchIO
                     }
                 }
@@ -308,7 +318,7 @@ class MangaPresenter(
                     }
 
                     // Choose a category
-                    else -> withUIContext { onRequireCategory(manga, categories) }
+                    else -> promptChangeCategories()
                 }
 
                 // Finally match with enhanced tracking when available
@@ -334,6 +344,26 @@ class MangaPresenter(
         }
     }
 
+    fun promptChangeCategories() {
+        val state = successState ?: return
+        val manga = state.manga
+        presenterScope.launch {
+            val categories = getCategories()
+            val selection = getMangaCategoryIds(manga)
+            _state.update { state ->
+                when (state) {
+                    MangaScreenState.Loading -> state
+                    is MangaScreenState.Success -> state.copy(
+                        dialog = Dialog.ChangeCategory(
+                            manga = manga,
+                            initialSelection = categories.mapAsCheckboxState { it.id in selection },
+                        ),
+                    )
+                }
+            }
+        }
+    }
+
     /**
      * Returns true if the manga has any downloads.
      */
@@ -365,13 +395,13 @@ class MangaPresenter(
      * @param manga the manga to get categories from.
      * @return Array of category ids the manga is in, if none returns default id
      */
-    suspend fun getMangaCategoryIds(manga: DomainManga): Array<Long> {
-        val categories = getCategories.await(manga.id)
-        return categories.map { it.id }.toTypedArray()
+    suspend fun getMangaCategoryIds(manga: DomainManga): List<Long> {
+        return getCategories.await(manga.id)
+            .map { it.id }
     }
 
-    fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List<Category>) {
-        moveMangaToCategories(categories)
+    fun moveMangaToCategoriesAndAddToLibrary(manga: DomainManga, categories: List<Long>) {
+        moveMangaToCategory(categories)
         if (!manga.favorite) {
             presenterScope.launchIO {
                 updateManga.awaitUpdateFavorite(manga.id, true)
@@ -387,6 +417,10 @@ class MangaPresenter(
      */
     private fun moveMangaToCategories(categories: List<Category>) {
         val categoryIds = categories.map { it.id }
+        moveMangaToCategory(categoryIds)
+    }
+
+    fun moveMangaToCategory(categoryIds: List<Long>) {
         presenterScope.launchIO {
             setMangaCategories.await(mangaId, categoryIds)
         }
@@ -994,6 +1028,45 @@ class MangaPresenter(
     }
 
     // Track sheet - end
+
+    fun getSourceOrStub(manga: DomainManga): Source {
+        return sourceManager.getOrStub(manga.source)
+    }
+
+    sealed class Dialog {
+        data class ChangeCategory(val manga: DomainManga, val initialSelection: List<CheckboxState<Category>>) : Dialog()
+        data class DeleteChapters(val chapters: List<DomainChapter>) : Dialog()
+        data class DuplicateManga(val manga: DomainManga, val duplicate: DomainManga) : Dialog()
+        data class DownloadCustomAmount(val max: Int) : Dialog()
+    }
+
+    fun dismissDialog() {
+        _state.update { state ->
+            when (state) {
+                MangaScreenState.Loading -> state
+                is MangaScreenState.Success -> state.copy(dialog = null)
+            }
+        }
+    }
+
+    fun showDownloadCustomDialog() {
+        val max = processedChapters?.count() ?: return
+        _state.update { state ->
+            when (state) {
+                MangaScreenState.Loading -> state
+                is MangaScreenState.Success -> state.copy(dialog = Dialog.DownloadCustomAmount(max))
+            }
+        }
+    }
+
+    fun showDeleteChapterDialog(chapters: List<DomainChapter>) {
+        _state.update { state ->
+            when (state) {
+                MangaScreenState.Loading -> state
+                is MangaScreenState.Success -> state.copy(dialog = Dialog.DeleteChapters(chapters))
+            }
+        }
+    }
 }
 
 sealed class MangaScreenState {
@@ -1012,6 +1085,7 @@ sealed class MangaScreenState {
         val isRefreshingChapter: Boolean = false,
         val isIncognitoMode: Boolean = false,
         val isDownloadedOnlyMode: Boolean = false,
+        val dialog: MangaPresenter.Dialog? = null,
     ) : MangaScreenState() {
 
         val processedChapters: Sequence<ChapterItem>

+ 0 - 75
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadCustomChaptersDialog.kt

@@ -1,75 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.app.Dialog
-import android.os.Bundle
-import androidx.core.os.bundleOf
-import com.bluelinelabs.conductor.Controller
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import eu.kanade.tachiyomi.widget.DialogCustomDownloadView
-
-/**
- * Dialog used to let user select amount of chapters to download.
- */
-class DownloadCustomChaptersDialog<T> : DialogController
-        where T : Controller, T : DownloadCustomChaptersDialog.Listener {
-
-    /**
-     * Maximum number of chapters to download in download chooser.
-     */
-    private val maxChapters: Int
-
-    /**
-     * Initialize dialog.
-     * @param maxChapters maximal number of chapters that user can download.
-     */
-    constructor(target: T, maxChapters: Int) : super(
-        // Add maximum number of chapters to download value to bundle.
-        bundleOf(KEY_ITEM_MAX to maxChapters),
-    ) {
-        targetController = target
-        this.maxChapters = maxChapters
-    }
-
-    /**
-     * Restore dialog.
-     * @param bundle bundle containing data from state restore.
-     */
-    @Suppress("unused")
-    constructor(bundle: Bundle) : super(bundle) {
-        // Get maximum chapters to download from bundle
-        val maxChapters = bundle.getInt(KEY_ITEM_MAX, 0)
-        this.maxChapters = maxChapters
-    }
-
-    /**
-     * Called when dialog is being created.
-     */
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        val activity = activity!!
-
-        // Initialize view that lets user select number of chapters to download.
-        val view = DialogCustomDownloadView(activity).apply {
-            setMinMax(0, maxChapters)
-        }
-
-        // Build dialog.
-        // when positive dialog is pressed call custom listener.
-        return MaterialAlertDialogBuilder(activity)
-            .setTitle(R.string.custom_download)
-            .setView(view)
-            .setPositiveButton(android.R.string.ok) { _, _ ->
-                (targetController as? Listener)?.downloadCustomChapters(view.amount)
-            }
-            .setNegativeButton(android.R.string.cancel, null)
-            .create()
-    }
-
-    interface Listener {
-        fun downloadCustomChapters(amount: Int)
-    }
-}
-
-// Key to retrieve max chapters from bundle on process death.
-private const val KEY_ITEM_MAX = "DownloadCustomChaptersDialog.int.maxChapters"

+ 0 - 125
app/src/main/java/eu/kanade/tachiyomi/widget/DialogCustomDownloadView.kt

@@ -1,125 +0,0 @@
-package eu.kanade.tachiyomi.widget
-
-import android.content.Context
-import android.text.InputFilter
-import android.text.SpannableStringBuilder
-import android.text.Spanned
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.view.View
-import android.widget.LinearLayout
-import androidx.core.text.isDigitsOnly
-import androidx.core.widget.doOnTextChanged
-import eu.kanade.tachiyomi.databinding.DownloadCustomAmountBinding
-import eu.kanade.tachiyomi.util.system.logcat
-import logcat.LogPriority
-
-/**
- * Custom dialog to select how many chapters to download.
- */
-class DialogCustomDownloadView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-    LinearLayout(context, attrs) {
-
-    /**
-     * Current amount of custom download chooser.
-     */
-    var amount: Int = 0
-        private set
-
-    /**
-     * Minimal value of custom download chooser.
-     */
-    private var min = 0
-
-    /**
-     * Maximal value of custom download chooser.
-     */
-    private var max = 0
-
-    private val binding: DownloadCustomAmountBinding
-
-    init {
-        binding = DownloadCustomAmountBinding.inflate(LayoutInflater.from(context), this, false)
-        addView(binding.root)
-    }
-
-    override fun onViewAdded(child: View) {
-        super.onViewAdded(child)
-
-        // Set download count to 0.
-        binding.myNumber.text = SpannableStringBuilder(getAmount(0).toString())
-        binding.myNumber.filters = arrayOf(DigitInputFilter())
-
-        // When user presses button decrease amount by 10.
-        binding.btnDecrease10.setOnClickListener {
-            binding.myNumber.text = SpannableStringBuilder(getAmount(amount - 10).toString())
-        }
-
-        // When user presses button increase amount by 10.
-        binding.btnIncrease10.setOnClickListener {
-            binding.myNumber.text = SpannableStringBuilder(getAmount(amount + 10).toString())
-        }
-
-        // When user presses button decrease amount by 1.
-        binding.btnDecrease.setOnClickListener {
-            binding.myNumber.text = SpannableStringBuilder(getAmount(amount - 1).toString())
-        }
-
-        // When user presses button increase amount by 1.
-        binding.btnIncrease.setOnClickListener {
-            binding.myNumber.text = SpannableStringBuilder(getAmount(amount + 1).toString())
-        }
-
-        // When user inputs custom number set amount equal to input.
-        binding.myNumber.doOnTextChanged { text, _, _, _ ->
-            try {
-                amount = getAmount(text.toString().toInt())
-            } catch (error: NumberFormatException) {
-                // Catch NumberFormatException to prevent parse exception when input is empty.
-                logcat(LogPriority.ERROR, error)
-            }
-        }
-    }
-
-    /**
-     * Set min max of custom download amount chooser.
-     * @param min minimal downloads
-     * @param max maximal downloads
-     */
-    fun setMinMax(min: Int, max: Int) {
-        this.min = min
-        this.max = max
-    }
-
-    /**
-     * Returns amount to download.
-     * if minimal downloads is less than input return minimal downloads.
-     * if Maximal downloads is more than input return maximal downloads.
-     *
-     * @return amount to download.
-     */
-    private fun getAmount(input: Int): Int {
-        return when {
-            input > max -> max
-            input < min -> min
-            else -> input
-        }
-    }
-}
-
-private class DigitInputFilter : InputFilter {
-
-    override fun filter(
-        source: CharSequence,
-        start: Int,
-        end: Int,
-        dest: Spanned,
-        dstart: Int,
-        dend: Int,
-    ): CharSequence {
-        return when {
-            source.toString().isDigitsOnly() -> source.toString()
-            else -> ""
-        }
-    }
-}

+ 0 - 9
app/src/main/res/drawable/ic_chevron_left_black_24dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z" />
-</vector>

+ 0 - 9
app/src/main/res/drawable/ic_chevron_left_double_black_24dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M18.41,7.41L17,6L11,12L17,18L18.41,16.59L13.83,12L18.41,7.41M12.41,7.41L11,6L5,12L11,18L12.41,16.59L7.83,12L12.41,7.41Z" />
-</vector>

+ 0 - 9
app/src/main/res/drawable/ic_chevron_right_black_24dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
-</vector>

+ 0 - 9
app/src/main/res/drawable/ic_chevron_right_double_black_24dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M5.59,7.41L7,6L13,12L7,18L5.59,16.59L10.17,12L5.59,7.41M11.59,7.41L13,6L19,12L13,18L11.59,16.59L16.17,12L11.59,7.41Z" />
-</vector>

+ 0 - 47
app/src/main/res/layout/download_custom_amount.xml

@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:gravity="center"
-    android:orientation="horizontal"
-    android:paddingVertical="8dp">
-
-    <com.google.android.material.button.MaterialButton
-        style="@style/Widget.Tachiyomi.Button.IconButton"
-        android:id="@+id/btn_decrease_10"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        app:icon="@drawable/ic_chevron_left_double_black_24dp" />
-
-    <com.google.android.material.button.MaterialButton
-        style="@style/Widget.Tachiyomi.Button.IconButton"
-        android:id="@+id/btn_decrease"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        app:icon="@drawable/ic_chevron_left_black_24dp" />
-
-    <eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText
-        android:id="@+id/myNumber"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:digits="0123456789"
-        android:inputType="number"
-        android:padding="8dp"
-        android:textStyle="bold" />
-
-    <com.google.android.material.button.MaterialButton
-        style="@style/Widget.Tachiyomi.Button.IconButton"
-        android:id="@+id/btn_increase"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        app:icon="@drawable/ic_chevron_right_black_24dp" />
-
-    <com.google.android.material.button.MaterialButton
-        style="@style/Widget.Tachiyomi.Button.IconButton"
-        android:id="@+id/btn_increase_10"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        app:icon="@drawable/ic_chevron_right_double_black_24dp" />
-
-</LinearLayout>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -653,6 +653,7 @@
     <string name="also_set_chapter_settings_for_library">Also apply to all manga in my library</string>
     <string name="set_chapter_settings_as_default">Set as default</string>
     <string name="no_chapters_error">No chapters found</string>
+    <string name="are_you_sure">Are you sure?</string>
 
     <!-- Tracking Screen -->
     <string name="tracker_anilist" translatable="false">AniList</string>