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
This commit is contained in:
Andreas 2022-08-26 14:57:28 +02:00 committed by GitHub
parent 4b9a6541d1
commit 2453d1a886
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 657 additions and 479 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = "")
}
}
},
)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,10 +2,19 @@ package eu.kanade.tachiyomi.ui.manga
import android.app.Dialog import android.app.Dialog
import android.os.Bundle 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.bluelinelabs.conductor.Controller
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.pushController
@ -46,3 +55,48 @@ class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle)
.create() .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,
),
)
},
)
}

View file

@ -6,26 +6,26 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.OnBackPressedDispatcherOwner
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.data.chapter.NoChaptersException 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.domain.manga.model.toDbManga
import eu.kanade.presentation.components.ChangeCategoryDialog
import eu.kanade.presentation.components.ChapterDownloadAction import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.presentation.manga.MangaScreen import eu.kanade.presentation.manga.MangaScreen
import eu.kanade.presentation.manga.components.DeleteChaptersDialog
import eu.kanade.presentation.util.calculateWindowWidthSizeClass import eu.kanade.presentation.util.calculateWindowWidthSizeClass
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService 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.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController 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.library.LibraryController
import eu.kanade.tachiyomi.ui.main.MainActivity 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.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.info.MangaFullCoverDialog
import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.ui.manga.track.TrackSearchDialog 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.history.HistoryController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity 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.logcat
import eu.kanade.tachiyomi.util.system.toast 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.launch
import kotlinx.coroutines.runBlocking
import logcat.LogPriority import logcat.LogPriority
import eu.kanade.domain.chapter.model.Chapter as DomainChapter import eu.kanade.domain.chapter.model.Chapter as DomainChapter
class MangaController : class MangaController : FullComposeController<MangaPresenter> {
FullComposeController<MangaPresenter>,
ChangeMangaCategoriesDialog.Listener,
DownloadCustomChaptersDialog.Listener {
@Suppress("unused") @Suppress("unused")
constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
@ -112,9 +105,19 @@ class MangaController :
@Composable @Composable
override fun ComposeContent() { override fun ComposeContent() {
val state by presenter.state.collectAsState() 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) { if (state is MangaScreenState.Success) {
val successState = state as MangaScreenState.Success val successState = state as MangaScreenState.Success
val isHttpSource = remember { successState.source is HttpSource } val isHttpSource = remember { successState.source is HttpSource }
val scope = rememberCoroutineScope()
MangaScreen( MangaScreen(
state = successState, state = successState,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
@ -133,16 +136,67 @@ class MangaController :
onCoverClicked = this::openCoverDialog, onCoverClicked = this::openCoverDialog,
onShareClicked = this::shareManga.takeIf { isHttpSource }, onShareClicked = this::shareManga.takeIf { isHttpSource },
onDownloadActionClicked = this::runDownloadChapterAction.takeIf { !successState.source.isLocalOrStub() }, 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 }, onMigrateClicked = this::migrateManga.takeIf { successState.manga.favorite },
onMultiBookmarkClicked = presenter::bookmarkChapters, onMultiBookmarkClicked = presenter::bookmarkChapters,
onMultiMarkAsReadClicked = presenter::markChaptersRead, onMultiMarkAsReadClicked = presenter::markChaptersRead,
onMarkPreviousAsReadClicked = presenter::markPreviousChapterRead, onMarkPreviousAsReadClicked = presenter::markPreviousChapterRead,
onMultiDeleteClicked = this::deleteChaptersWithConfirmation, onMultiDeleteClicked = presenter::showDeleteChapterDialog,
onChapterSelected = presenter::toggleSelection, onChapterSelected = presenter::toggleSelection,
onAllChapterSelected = presenter::toggleAllSelection, onAllChapterSelected = presenter::toggleAllSelection,
onInvertSelection = presenter::invertSelection, 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 { } else {
LoadingScreen() LoadingScreen()
} }
@ -206,30 +260,10 @@ class MangaController :
} }
} }
private fun onFavoriteClick(checkDuplicate: Boolean = true) { private fun onFavoriteClick() {
presenter.toggleFavorite( presenter.toggleFavorite(
onRemoved = this::onFavoriteRemoved, onRemoved = this::onFavoriteRemoved,
onAdded = { activity?.toast(activity?.getString(R.string.manga_added_library)) }, 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. * 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>) { fun deleteChapters(chapters: List<DomainChapter>) {
if (chapters.isEmpty()) return if (chapters.isEmpty()) return
presenter.deleteChapters(chapters) presenter.deleteChapters(chapters)
@ -449,7 +440,7 @@ class MangaController :
DownloadAction.NEXT_5_CHAPTERS -> presenter.getUnreadChaptersSorted().take(5) DownloadAction.NEXT_5_CHAPTERS -> presenter.getUnreadChaptersSorted().take(5)
DownloadAction.NEXT_10_CHAPTERS -> presenter.getUnreadChaptersSorted().take(10) DownloadAction.NEXT_10_CHAPTERS -> presenter.getUnreadChaptersSorted().take(10)
DownloadAction.CUSTOM -> { DownloadAction.CUSTOM -> {
showCustomDownloadDialog() presenter.showDownloadCustomDialog()
return return
} }
DownloadAction.UNREAD_CHAPTERS -> presenter.getUnreadChapters() 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 // Chapters list - end
// Tracker sheet - start // Tracker sheet - start

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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