mirror of
https://git.mihon.tech/mihonapp/mihon
synced 2024-11-22 13:15:47 +03:00
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:
parent
4b9a6541d1
commit
2453d1a886
20 changed files with 657 additions and 479 deletions
55
app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt
Normal file
55
app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt
Normal 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) }
|
||||||
|
}
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -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))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
|
|
@ -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 -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue